我們每天瀏覽網頁、使用即時通訊軟體,背後都仰賴網路進程間的通訊。瀏覽器進程如何與 Web 伺服器溝通?QQ 進程如何傳遞訊息給好友?核心關鍵在於 Socket。本文將系統性介紹 Socket 概念、類型、核心 API、TCP 三次握手與四次揮手機制,並提供完整的客戶端/伺服器端範例代碼。
本地進程間通訊(IPC)有管道、訊息佇列、共享記憶體、訊號量等方式,但網路環境下,如何唯一標識一個進程是首要問題。本地可用 PID,但網路中無效。TCP/IP 協議族解決了這個問題:
利用三元組 (IP 位址, 協議, 連接埠) 即可標識網路進程,實現跨主機通訊。現代應用幾乎皆採用 UNIX BSD Socket API,故有「一切皆 Socket」之說。
Socket 起源於 Unix,呼應「一切皆檔案」哲學:以 open → read/write → close 模式操作。Socket 是一種特殊檔案,Socket 函數即對其進行開啟、讀寫、關閉等操作。歷史上,Socket 一詞首見於 1970 年 IETF RFC 33,早於 BSD Socket 定義約 12 年。
int socket(int domain, int type, int protocol);
對應檔案的 open,返回唯一識別 Socket 的描述符。三個參數:
AF_INET (IPv4)、AF_INET6 (IPv6)、AF_LOCAL/AF_UNIX (本地通訊) 等,決定位址類型。SOCK_STREAM (串流/TCP)、SOCK_DGRAM (資料報/UDP)、SOCK_RAW 等。IPPROTO_TCP、IPPROTO_UDP 等。通常設 0 讓系統依 type 自動選預設協議。注意:type 與 protocol 不可任意組合(如 SOCK_STREAM 不能搭配 IPPROTO_UDP)。新建 Socket 無具體位址,需 bind() 指定,否則 connect()/listen() 時由系統隨機分配。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
將特定位址(IP + 連接埠)指派給 Socket。sockaddr 結構隨協議族不同:
struct sockaddr_in(含 sin_family, sin_port, sin_addr)struct sockaddr_in6struct sockaddr_un(路徑名稱)關鍵:網路位元組序。主機位元組序(大端/小端)因 CPU 而異,網路傳輸統一用大端(Network Byte Order)。綁定前務必用 htons(), htonl() 轉換,否則會引發難以除錯的錯誤。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen() 將主動 Socket 轉為被動監聽狀態,backlog 指定佇列最大連線數。connect() 由客戶端呼叫,向伺服器發起 TCP 連線請求。int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
伺服器調用 accept() 取得客戶端連線。關鍵區別:
socket() 建立,生命週期贯穿伺服器)連線建立後進行資料收發。常用函數組:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
推薦使用 recvmsg()/sendmsg(),功能最完整,可替代其他函數。read() 返回 0 表示對端關閉,<0 表示錯誤(EINTR 中斷、ECONNRESET 連線重置)。write() 返回值 >0 為寫入位元組數,<0 為錯誤(EPIPE 對端已關閉)。
結束通訊時關閉描述符,釋放資源,類似 fclose()。

SYN_SENT 狀態SYN_RCVDESTABLISHED
FIN_WAIT_1CLOSE_WAIT;主動端進入 FIN_WAIT_2LAST_ACKTIME_WAIT(等待 2MSL 後關閉)#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 4096
int main() {
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
int n;
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("create socket error");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
perror("bind socket error");
exit(EXIT_FAILURE);
}
if (listen(listenfd, 10) == -1) {
perror("listen socket error");
exit(EXIT_FAILURE);
}
printf("====== waiting for client's request ======\n");
while (1) {
if ((connfd = accept(listenfd, NULL, NULL)) == -1) {
perror("accept socket error");
continue;
}
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connfd);
}
close(listenfd);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 4096
int main(int argc, char** argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s \n", argv[0]);
exit(EXIT_FAILURE);
}
int sockfd;
char sendline[MAXLINE];
struct sockaddr_in servaddr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("create socket error");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
perror("inet_pton error");
exit(EXIT_FAILURE);
}
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("connect error");
exit(EXIT_FAILURE);
}
printf("send msg to server: \n");
fgets(sendline, MAXLINE, stdin);
if (send(sockfd, sendline, strlen(sendline), 0) < 0) {
perror("send msg error");
exit(EXIT_FAILURE);
}
close(sockfd);
return 0;
}
提示:此為迭代伺服器,一次只能處理一個請求。生產環境需使用 fork()、執行緒池或 I/O 多路複用(select/poll/epoll)實現並發。
請嘗試編寫:伺服器監聽 192.168.100.168,接收來自 192.168.100.2 的訊息;若收到 "Client Query" 則印出 "Receive Query"。客戶端依序發送 "Client Query test"、"Client Query"、"Client Query Quit" 後退出。