《UNIX網絡編程卷1:套接字聯網API》第5章 TCP客戶/服務器程序示例
5.1 本章目標與示例程序概述
本章通過一個完整的TCP回射(Echo)客戶/服務器程序,深入解析TCP套接字編程的核心流程與關鍵問題。示例程序的功能為:客戶端發送文本至服務器,服務器將文本原樣返回。通過此案例,讀者將掌握:
- TCP通信全流程:從套接字創建到連接終止;
- 并發服務器設計:多進程/多線程模型實現;
- 健壯性處理:應對網絡異常與資源管理;
- 調試技巧:使用工具分析協議交互。
5.2 服務器端程序實現
5.2.1 主函數框架
#include "unp.h"int main(int argc, char **argv) {int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;// 創建TCP套接字listenfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化服務器地址結構bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定所有接口servaddr.sin_port = htons(SERV_PORT); // 服務端口號// 綁定與監聽Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));Listen(listenfd, LISTENQ); // LISTENQ定義連接隊列最大長度for (;;) {clilen = sizeof(cliaddr);connfd = Accept(listenfd, (SA *)&cliaddr, &clilen); // 阻塞等待連接// 并發處理if ((childpid = Fork()) == 0) { // 子進程Close(listenfd); // 子進程關閉監聽套接字str_echo(connfd); // 處理客戶端請求exit(0);}Close(connfd); // 父進程關閉已連接套接字}
}
關鍵點:
INADDR_ANY
允許服務器監聽所有網絡接口;fork()
實現并發處理,父進程繼續監聽新連接,子進程處理當前連接。
5.2.2 數據回射函數str_echo
void str_echo(int sockfd) {ssize_t n;char buf[MAXLINE];
again:while ((n = Read(sockfd, buf, MAXLINE)) > 0)Writen(sockfd, buf, n); // 回射數據if (n < 0 && errno == EINTR) // 處理中斷goto again;else if (n < 0)err_sys("str_echo: read error"); // 包裹函數處理錯誤
}
注意:TCP是字節流協議,需處理部分讀寫與粘包問題。
5.3 客戶端程序實現
5.3.1 主函數框架
#include "unp.h"int main(int argc, char **argv) {int sockfd;struct sockaddr_in servaddr;if (argc != 2)err_quit("usage: tcpcli <IPaddress>");// 創建TCP套接字sockfd = Socket(AF_INET, SOCK_STREAM, 0);// 初始化服務器地址bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons(SERV_PORT);Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);// 發起連接Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));// 處理用戶輸入與服務器響應str_cli(stdin, sockfd); exit(0);
}
關鍵點:
Connect
觸發三次握手,需處理ETIMEDOUT
(超時)和ECONNREFUSED
(拒絕連接)等錯誤。
5.3.2 用戶交互函數str_cli
void str_cli(FILE *fp, int sockfd) {char sendline[MAXLINE], recvline[MAXLINE];while (Fgets(sendline, MAXLINE, fp) != NULL) { // 讀取標準輸入Writen(sockfd, sendline, strlen(sendline)); // 發送至服務器if (Readline(sockfd, recvline, MAXLINE) == 0) // 讀取響應err_quit("str_cli: server terminated prematurely");Fputs(recvline, stdout); // 輸出響應}
}
說明:Readline
需正確處理部分讀與緩沖區管理(參考第3章字節流處理)。
5.4 并發服務器模型與僵尸進程處理
5.4.1 多進程模型的缺陷
- 僵尸進程:子進程終止后未調用
wait
,導致進程表中殘留條目; - 資源泄漏:未關閉套接字可能耗盡文件描述符。
5.4.2 解決方案:信號處理
void sig_chld(int signo) {pid_t pid;int stat;while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)printf("child %d terminated\n", pid);return;
}// 主函數中注冊信號處理
Signal(SIGCHLD, sig_chld); // 使用包裹函數處理信號
作用:捕獲SIGCHLD
信號,回收子進程資源。
5.5 異常場景分析與處理
5.5.1 服務器主機崩潰
- 現象:客戶端
read
阻塞,TCP持續重傳數據,最終返回ETIMEDOUT
; - 處理:設置超時機制或使用心跳包檢測連接狀態。
5.5.2 服務器主機重啟
- 現象:客戶端收到
ECONNRESET
錯誤; - 處理:重連機制或優雅終止程序。
5.5.3 客戶端非正常終止
- 現象:服務器子進程
read
返回0,觸發正常關閉流程; - 處理:確保
close
釋放資源,避免文件描述符泄漏。
5.6 測試與調試技巧
5.6.1 使用netstat
監控連接狀態
netstat -ant | grep 9999 # 查看端口9999的TCP連接狀態
輸出示例:
LISTEN
:監聽狀態;ESTABLISHED
:已建立連接;TIME_WAIT
:連接終止等待。
5.6.2 tcpdump
抓包分析
tcpdump -i lo port 9999 # 監聽本地回環接口的9999端口
關鍵字段:
SYN
/ACK
:三次握手過程;FIN
:四次揮手過程。
5.6.3 使用ps
查看進程狀態
ps -ef | grep tcpserv # 查看服務器進程狀態
狀態說明:
S
:睡眠狀態(等待I/O);Z
:僵尸進程。
5.7 性能優化與擴展
5.7.1 線程池模型
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 線程分離
pthread_create(&tid, &attr, handle_client, (void *)connfd);
優勢:避免頻繁創建/銷毀線程的開銷。
5.7.2 I/O復用(select
/epoll
)
- 適用場景:高并發連接,減少進程/線程切換開銷;
- 實現要點:事件驅動模型,非阻塞I/O。
- 參見相關文章:epoll函數使用實戰詳解
5.8 本章小結與進階習題
小結:本章通過Echo程序完整演示了TCP客戶/服務器開發流程,涵蓋并發模型、異常處理與調試技巧,為復雜網絡應用開發奠定基礎。
習題:
- 實現UDP版本的Echo程序,對比TCP/UDP編程差異;
- 修改服務器為線程池模型,測試并發性能;
- 使用
Wireshark
分析TCP握手與揮手過程,提交抓包分析報告。
付費用戶專屬資源:
- 完整代碼工程(含Makefile與測試腳本);
- TCP狀態轉換圖(矢量圖);
- 擴展閱讀:《UNIX網絡編程中的并發模型演進》。
通過本章學習,讀者將掌握TCP套接字編程的核心技術,并具備開發高可靠性、高并發網絡服務的能力。