《UNIX网络编程卷1:套接字联网API》第4章 基本TCP套接字编程
4.1 TCP套接字编程核心流程
TCP套接字编程遵循客户-服务器模型,其核心流程可分解为以下步骤(图4-1):
服务器端:
- socket():创建监听套接字
- bind():绑定本地IP与端口
- listen():开启监听模式
- accept():接受客户端连接请求
- read()/write():与客户端通信
- close():关闭连接
客户端:
- socket():创建通信套接字
- connect():发起连接请求
- read()/write():与服务器通信
- close():关闭连接
c
服务端流程:
开始
↓
socket() 创建监听套接字
↓
bind() 绑定本地IP与端口
↓
listen() 开启监听模式
↓
accept() 接受客户端连接请求
↓
read()/write() 与客户端通信
↓
close() 关闭连接
结束
客户端流程:
开始
↓
socket() 创建通信套接字
↓
connect() 发起连接请求
↓
read()/write() 与服务器通信
↓
close() 关闭连接
结束

4.2 套接字创建与协议选择
4.2.1 socket()函数详解
c
#include <sys/socket.h>
int socket(int family, int type, int protocol);
-
参数解析:
family
:协议族(如AF_INET
、AF_INET6
、AF_UNIX
);type
:套接字类型(SOCK_STREAM
、SOCK_DGRAM
);protocol
:通常为0(自动选择默认协议)。
-
返回值:
- 成功:返回非负套接字描述符;
- 失败:返回-1,设置
errno
(如EAFNOSUPPORT
协议不支持)。
示例:创建IPv4 TCP套接字
c
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd < 0)
err_sys("socket error");
4.2.2 协议选择的工程实践
- IPv4与IPv6兼容性设计 :优先使用
AF_UNSPEC
配合getaddrinfo
(详见第11章); - TCP与SCTP对比:TCP适合文件传输,SCTP适合多流场景(如5G信令)。
4.3 绑定地址与端口:bind()函数
4.3.1 bind()函数原型
c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
关键参数:
addr
:指向特定协议地址结构(如sockaddr_in
);addrlen
:地址结构长度。
-
常见错误:
EADDRINUSE
:端口被占用;EACCES
:绑定特权端口(<1024)权限不足。
示例:绑定IPv4地址到套接字
c
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 通配地址
servaddr.sin_port = htons(9999); // 端口号
if (bind(listenfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
err_sys("bind error");
4.3.2 通配地址与特定地址
- INADDR_ANY:允许套接字监听所有本地接口;
- 指定IP :仅监听特定接口(如
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr)
)。
4.4 监听与连接队列:listen()函数
4.4.1 listen()函数原型
c
int listen(int sockfd, int backlog);
- backlog参数 :
定义内核维护的**未完成连接队列(SYN_RCVD状态)与 已完成连接队列(ESTABLISHED状态)**的总长度上限。- 传统BSD实现:backlog为两个队列总和;
- Linux 4.3+:通过
/proc/sys/net/core/somaxconn
动态调整。
最佳实践:
c
const int BACKLOG = 1024;
if (listen(listenfd, BACKLOG) < 0)
err_sys("listen error");
4.4.2 连接队列溢出处理
- 客户端收到
ECONNREFUSED
错误; - 服务器可通过
netstat -s | grep overflow
监控溢出次数。
4.5 接受连接:accept()函数
4.5.1 accept()函数原型
c
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
- 参数特性 :
cliaddr
:返回客户端地址信息;addrlen
:值-结果参数(需初始化为缓冲区大小)。
示例:获取客户端IP与端口
c
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(listenfd, (SA *)&cliaddr, &clilen);
if (connfd < 0)
err_sys("accept error");
char client_ip[INET_ADDRSTRLEN];
printf("Client connected from %s:%d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, client_ip, sizeof(client_ip)),
ntohs(cliaddr.sin_port));
4.5.2 非阻塞accept与EAGAIN
- 若监听套接字设为非阻塞且无连接到达,返回
EAGAIN
错误; - 需结合I/O复用(如
select
或epoll
)处理。
4.6 发起连接:connect()函数
4.6.1 connect()函数原型
c
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
- 连接过程 :
- 客户端发送SYN;
- 等待服务器SYN+ACK;
- 发送ACK完成三次握手(图4-2)。
示例:客户端连接服务器
c
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9999);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);
if (connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) < 0)
err_sys("connect error");
4.6.2 超时与错误处理
- ETIMEDOUT:未收到SYN+ACK(可能服务器宕机);
- ECONNREFUSED:目标端口无监听服务;
- ENETUNREACH:路由不可达。
4.7 数据传输:read()与write()的局限性
TCP为字节流协议,需处理粘包 与部分读写问题:
- 粘包 :多次
write
可能被合并为一个TCP段; - 部分读写 :
read
可能返回小于请求长度的数据。
解决方案:
- 定义应用层协议(如固定长度头+变长体);
- 使用
readn
/writen
包裹函数(见第3章)。
4.8 连接终止:close()与shutdown()
4.8.1 close()函数
- 减少套接字引用计数,计数为0时发送FIN;
- 可能因
SO_LINGER
选项改变行为(详见第7章)。
4.8.2 shutdown()函数
c
int shutdown(int sockfd, int howto);
- howto参数 :
SHUT_RD
:关闭读端;SHUT_WR
:关闭写端(发送FIN);SHUT_RDWR
:全双工关闭。
优势:允许半关闭连接(如HTTP/1.1持久连接)。
4.9 并发服务器模型
4.9.1 多进程模型
c
pid_t pid;
int connfd;
for (;;) {
connfd = accept(listenfd, NULL, NULL);
if ((pid = fork()) == 0) { // 子进程
close(listenfd); // 关闭监听套接字
// 处理客户端请求
exit(0);
}
close(connfd); // 父进程关闭连接套接字
}
4.9.2 多线程模型
c
#include <pthread.h>
void *handle_client(void *arg) {
int connfd = *((int *)arg);
// 处理客户端请求
close(connfd);
return NULL;
}
int main() {
pthread_t tid;
int connfd;
for (;;) {
connfd = accept(listenfd, NULL, NULL);
pthread_create(&tid, NULL, handle_client, (void *)&connfd);
}
}
4.9.3 I/O复用模型
c
// 使用select实现(详见第6章)
fd_set readset;
FD_ZERO(&readset);
FD_SET(listenfd, &readset);
select(listenfd + 1, &readset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &readset)) {
connfd = accept(listenfd, NULL, NULL);
// 处理连接
}
4.10 实战:完整Echo服务器与客户端
4.10.1 Echo服务器代码
c
#include "unp.h"
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");
}
int main() {
int listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 绑定与监听(代码同前)
for (;;) {
int connfd = accept(listenfd, NULL, NULL);
if (fork() == 0) {
close(listenfd);
str_echo(connfd);
exit(0);
}
close(connfd);
}
}
4.10.2 Echo客户端代码
c
#include "unp.h"
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);
}
}
int main() {
int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
// 连接服务器(代码同前)
str_cli(stdin, sockfd);
exit(0);
}
4.11 本章小结与习题
小结:本章系统讲解了TCP套接字编程的核心API与并发模型,通过Echo案例展示了完整开发流程。
习题:
- 实现支持多客户端的UDP Echo服务器,对比TCP/UDP编程差异;
- 修改Echo服务器为线程池模型,测试并发性能;
- 使用
tcpdump
抓取三次握手与四次挥手数据包,分析字段含义。
付费用户专属资源:
- 完整Echo工程(含Makefile与压力测试脚本);
- TCP状态转换图(矢量图);
- 扩展阅读:《TCP协议栈内核实现剖析》。
4.11 本章小结与习题
小结:本章系统讲解了TCP套接字编程的核心API与并发模型,通过Echo案例展示了完整开发流程。
习题:
- 实现支持多客户端的UDP Echo服务器,对比TCP/UDP编程差异;
- 修改Echo服务器为线程池模型,测试并发性能;
- 使用
tcpdump
抓取三次握手与四次挥手数据包,分析字段含义。
付费用户专属资源:
- 完整Echo工程(含Makefile与压力测试脚本);
- TCP状态转换图(矢量图);
- 扩展阅读:《TCP协议栈内核实现剖析》。
通过本章学习,读者将掌握TCP套接字编程的核心技术,并具备开发高并发网络服务的能力。