簡單的 TCP 伺服端與客戶端
就像我們學習任何程式語言時從 Hello World 開始一樣,在 socket 程式設計中,我們也將實作一個簡單的 Hello World 程式。
在這篇文章中,我們將實作一個簡單的 TCP 伺服端與客戶端。
- 伺服端會在特定的 IP 位址與埠號上進行監聽。
- 客戶端會連線到伺服端,並傳送資料給伺服端。
- 伺服端收到資料後,會回傳回應給客戶端。
- 客戶端收到回應後,將其印出。
程式流程圖

image
來源:Socket Programming in C/C++ - GeeksforGeeks
伺服端範例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* 伺服端 IP 位址 */
#define PORT 43866 /* 埠號 */
#define STRING_LEN_16 16
#define STRING_LEN_64 64
#define BACKLOG 10
char *_inet_ntoa(struct in_addr *addr, char *ipAddr, int len)
{
// 將二進位 IP 位址轉換為可讀 IP 位址
if (NULL == addr || NULL == ipAddr || 16 > len)
{
printf("invalid param\n");
return NULL;
}
unsigned char *tmp = (unsigned char *)addr;
snprintf(ipAddr, len, "%d.%d.%d.%d", tmp[0], tmp[1], tmp[2], tmp[3]);
return ipAddr;
}
int main(int argc, char *argv[])
{
// 1. 建立 socket socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // TCP socket
if (sockfd == -1)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// 手動建構 sockaddr_in 結構體
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 將結構體初始化為 0
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(PORT); // 埠號
inet_aton(IPADDR, &addr.sin_addr); // 將可讀 IP 位址轉換為二進位 IP 位址
// inet_aton 目前已不建議使用,因為它不支援 IPv6
// 2. 將 socket 與特定的 IP 位址和埠號綁定 bind()
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call bind, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. 讓 socket 進入被動監聽狀態 listen()
if (listen(sockfd, BACKLOG) == -1)
{
printf("fail to call listen, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
printf("server listen on %s:%u\n", IPADDR, PORT);
// 4. 當 socket 處於監聽狀態時,可透過 accept 函式接收客戶端的請求 accept()
struct sockaddr_in peerAddr;
socklen_t peerAddrLen = sizeof(struct sockaddr_in);
int connfd = accept(sockfd, (struct sockaddr *)&peerAddr, &peerAddrLen);
if (connfd == -1)
{
printf("fail to call accept, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
char peerIPAddr[STRING_LEN_16];
_inet_ntoa(&peerAddr.sin_addr, peerIPAddr, STRING_LEN_16);
printf("peer client address [%s:%u]\n", peerIPAddr, ntohs(peerAddr.sin_port));
while (1)
{
// 5. 讀取客戶端傳送的資料 recv()
char buf[STRING_LEN_64];
int n = recv(connfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
if (0 == n) // n 為 0 表示對端關閉
{
printf("peer close\n");
break;
}
printf("recv msg from client : %s\n", buf);
sleep(2);
// 6. 傳送資料給客戶端 send()
char str[] = "recved!";
printf("send msg to client : %s\n", str);
send(connfd, str, strlen(str), 0);
}
// 7. 互動結束,關閉 socket close()
close(connfd);
close(sockfd);
return 0;
}
客戶端範例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#define IPADDR "127.0.0.1" /* 伺服端 IP 位址 */
#define PORT 43866 /* 伺服端埠號 */
#define STRING_LEN_64 64
int main()
{
// 1. 建立 TCP socket socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sockfd = %d\n", sockfd);
if (-1 == sockfd)
{
printf("fail to call socket, errno[%d, %s]\n", errno, strerror(errno));
exit(0);
}
// 2. 將 socket 與特定的 IP 位址和埠號建立連線 connect()
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_aton(IPADDR, &addr.sin_addr);
if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
printf("fail to call connect, errno[%d, %s]\n", errno, strerror(errno));
close(sockfd);
exit(0);
}
// 3. 透過 socket 傳送資料給伺服端 send()
char str[] = "hello world";
printf("send msg to server : %s\n", str);
send(sockfd, str, strlen(str), 0);
// 4. 透過 socket 從伺服端接收資料 recv()
char buf[STRING_LEN_64];
int n = recv(sockfd, buf, STRING_LEN_64 - 1, 0);
buf[n] = '\0';
printf("recv msg from server : %s\n", buf);
// 5. 互動結束,關閉 socket close()
close(sockfd);
}
編譯與執行
Compiling:
gcc client.c -o client
gcc server.c -o server
輸出結果
$ ./server # 在伺服端終端機執行伺服器
server listen on 127.0.0.1:43866
peer client address [127.0.0.1:33844]
recv msg from client : hello world
send msg to client : recved!
peer close
$ ./client # 在客戶端終端機執行客戶端
sockfd = 3
send msg to server : hello world
recv msg from server : recved!
潛在問題
- 伺服器程式一次只能服務一個客戶端。
- 程式碼不支援 IPv6。(inetntoa, inetaton)
- 該程式沒有任何安全措施。不會對客戶端進行身分驗證或加密通訊,容易受到欺騙或竊聽等攻擊。
解決方案
- 使用 select() 處理多個客戶端。
- 使用 getaddrinfo(), ntop() 與 pton() 支援 IPv6。
- 使用 SSL/TLS 加密通訊。