本文從零構建三個版本的 TCP 回聲服務器,展示 Socket 編程的演進:
accept + fork 多進程並發;select I/O 多路復用單進程並發;getaddrinfo 協議無關地址解析 + IPv4/IPv6 雙棧。int main(int argc, char *argv[]) {
int lfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in srv = { .sin_family=AF_INET, .sin_addr.s_addr=htonl(INADDR_ANY), .sin_port=htons(port) };
bind(lfd, (struct sockaddr*)&srv, sizeof(srv));
listen(lfd, 5);
signal(SIGCHLD, sig_handler); // 回收僵屍進程
while (1) {
int cfd = accept(lfd, (struct sockaddr*)&cli, &len);
if (fork() == 0) { // 子進程處理客戶端
close(lfd);
while (read(cfd, buf, sizeof(buf)) > 0) {
// 轉大寫回射
for (i=0; i
fork() 複製文件描述符表,**父子各自 close 不需要的 fd**;SIGCHLD + waitpid(-1, &stat, WNOHANG) 非阻塞回收僵屍進程;accept 被信號中斷返回 EINTR 需重試。fd_set master, read_fds;
FD_ZERO(&master); FD_SET(lfd, &master);
int fdmax = lfd;
while (1) {
read_fds = master;
select(fdmax+1, &read_fds, NULL, NULL, NULL);
for (int i=0; i<=fdmax; i++) if (FD_ISSET(i, &read_fds)) {
if (i == lfd) { // 新連接
int cfd = accept(lfd, ...);
FD_SET(cfd, &master);
if (cfd > fdmax) fdmax = cfd;
} else { // 客戶端數據
int n = read(i, buf, sizeof(buf));
if (n <= 0) { close(i); FD_CLR(i, &master); }
else { /* 處理並 write 回射 */ }
}
}
}
FD_SETSIZE (通常 1024);struct addrinfo hints = {
.ai_family = AF_UNSPEC, // IPv4/IPv6 皆可
.ai_socktype = SOCK_STREAM,
.ai_flags = AI_PASSIVE // 服務端綁定通配地址
};
struct addrinfo *res;
getaddrinfo(NULL, "1234", &hints, &res);
for (struct addrinfo *p = res; p; p = p->ai_next) {
int fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
bind(fd, p->ai_addr, p->ai_addrlen);
listen(fd, 10);
break; // 取第一個成功的
}
freeaddrinfo(res);
void *get_in_addr(struct sockaddr *sa) {
return (sa->sa_family == AF_INET) ?
(void*)&(((struct sockaddr_in*)sa)->sin_addr) :
(void*)&(((struct sockaddr_in6*)sa)->sin6_addr);
}
inet_ntop(sa->sa_family, get_in_addr(sa), ipstr, sizeof(ipstr));
int main(int argc, char *argv[]) {
struct addrinfo hints = { .ai_family=AF_UNSPEC, .ai_socktype=SOCK_STREAM }, *res;
getaddrinfo(argv[1], argv[2], &hints, &res);
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(fd, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
// 循環 read(STDIN) -> write(fd) -> read(fd) -> write(STDOUT)
}
| 版本 | 並發模型 | 優點 | 缺點 |
|---|---|---|---|
| fork | 多進程 | 簡單、隔離性好 | 進程開銷大、難共享數據 |
| select | 單進程多路復用 | 單線程、無鎖、支援數千連接 | FD_SETSIZE 限制、O(N) 掃描 |
| poll/epoll/kqueue | 單進程多路復用 | 無 fd 上限、O(1) 就緒返回 | API 較複雜、平台差異 |
epoll (Linux) / kqueue (BSD/macOS) / IOCP (Windows) 邊緣觸發模式;libevent, libuv, asio;OpenSSL) 與應用層協議設計 (HTTP, WebSocket, 自定義二進制協議)。Socket 編程是系統程序員的分水嶺。從阻塞 accept 到事件驅動架構,每一步都在解決「如何高效管理成千上萬個並發連接」這一核心問題。