「一切皆 Socket」。現代網路程式幾乎全建立在 BSD Socket API 之上。本文系統梳理 Socket 核心概念、基礎 API 全家桶、TCP 三次握手/四次揮手與 Socket 函數的對應關係、完整 Client/Server 範例、併發模型演進方向。
(IP, 協議, Port) 唯一標識網路進程socket():建立描述符
int socket(int domain, int type, int protocol);
// domain: AF_INET(IPv4), AF_INET6, AF_UNIX(本地)
// type: SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW
// protocol: 0=自動選預設, IPPROTO_TCP, IPPROTO_UDP
// 返回: socket fd (非負整數), -1 失敗
bind():綁定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockaddr_in (IPv4): sin_family, sin_port(網路序), sin_addr
// sockaddr_in6 (IPv6): sin6_family, sin6_port, sin6_addr, sin6_scope_id
// sockaddr_un (Unix): sun_family, sun_path
// 伺服器必須 bind 知名埠口;客戶端通常不 bind(連線時自動分配)
// 主機序 → 網路序 (大端)
uint16_t port_net = htons(6666); // host to network short
uint32_t ip_net = htonl(INADDR_ANY); // host to network long
// 網路序 → 主機序
uint16_t port_host = ntohs(addr.sin_port);
uint32_t ip_host = ntohl(addr.sin_addr.s_addr);
鐵律:所有進出網路的整數(埠口、IP、長度)必須轉網路序,勿假設主機序即網路序。
listen() / connect():連線建立
int listen(int sockfd, int backlog); // backlog: 已完成連線佇列上限
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// listen 將主動 socket 轉為被動監聽
// connect 發起主動連線(阻塞直到三次握手完成/失敗)
accept():接受連線
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 從已完成佇列取出連線,返回**新 fd**(已連線 socket)
// 原 sockfd 繼續監聽(監聽 socket)
// 典型模式:while ((connfd = accept(listenfd, ...)) > 0) { 處理 connfd; close(connfd); }
| 函數對 | 特點 |
|---|---|
read/write | 通用檔案描述符,阻塞 |
recv/send | Socket 專用,支援 flags (MSG_OOB, MSG_PEEK, MSG_DONTWAIT) |
readv/writev | 分散/聚合 I/O,單次系統調用多緩衝區 |
recvmsg/sendmsg | 最通用,支援控制資料 (cmsg)、檔案描述符傳遞 |
recvfrom/sendto | 無連線 (UDP) 指定對端地址 |
推薦:recvmsg/sendmsg 統一介面,其餘可視為特例。
close():關閉描述符shutdown(fd, SHUT_WR):主動發 FIN,但仍可讀(半關閉)SO_LINGER 選項控制關閉行為
Client: connect() → 發 SYN → 阻塞
Server: accept() → 收 SYN → 回 SYN+ACK → 阻塞
Client: 收 SYN+ACK → 回 ACK → connect() 返回
Server: 收 ACK → accept() 返回
關鍵點:connect() 在**第二次握手**返回,accept() 在**第三次握手**返回。
主動關閉方: close() → 發 FIN → 進 FIN_WAIT_1
被動方: 收 FIN → 回 ACK → 進 CLOSE_WAIT → 應用 close() → 發 FIN → 進 LAST_ACK
主動方: 收 FIN → 回 ACK → 進 TIME_WAIT (2MSL) → CLOSED
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 6666
#define MAXLINE 4096
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, 10);
printf("====== waiting for client ======\n");
while (1) {
int connfd = accept(listenfd, NULL, NULL);
char buf[MAXLINE];
int n = recv(connfd, buf, MAXLINE, 0);
buf[n] = '\0';
printf("recv: %s\n", buf);
close(connfd);
}
close(listenfd);
return 0;
}
int main(int argc, char** argv) {
if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); }
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
printf("send msg: ");
char sendline[MAXLINE];
fgets(sendline, MAXLINE, stdin);
send(sockfd, sendline, strlen(sendline), 0);
close(sockfd);
return 0;
}
| 模型 | 適用場景 | 缺點 |
|---|---|---|
| 迭代 (範例) | 教學、極低併發 | 串行處理,吞吐極低 |
| fork 多進程 | 簡單隔離 | 進程開銷大,IPC 複雜 |
| pthread 多執行緒 | 高併發、共享記憶體 | 鎖競爭、堆疊記憶體 |
| I/O 多路複用 (select/poll/epoll) | 單執行緒高併發 (C10K) | 邏輯複雜、阻塞調用難處理 |
| 非阻塞 + 事件驅動 (libevent, libuv, io_uring) | 極高併發 (C10M) | 學習曲線陡峭 |
需求:
Server 綁定 192.168.100.168:6666,僅接受來自 192.168.100.2 的連線。
Client 依序發送:"Client Query test"、"Client Query"、"Client Query Quit"後退出。
Server 收到含"Client Query"字串則列印"Receive Query"。
提示:Server 需在 accept() 後檢查 addr.sin_addr.s_addr,Client 需 send() 三次並間隔 sleep(1)。
Socket 程式設計是系統級開發的入場券。掌握 API 語義、TCP 狀態機、位元組序、併發模型,才能寫出健壯、高效的網路軟體。