TCP(Transmission Control Protocol,传输控制协议)是互联网协议族中面向连接、可靠的字节流传输协议。本文将从 TCP 通信的核心流程、API 接口、与 UDP 的差异,到粘包问题解决,最终深入讲解 TCP 并发服务器的实现思路与 Linux IO 模型,帮助开发者全面掌握 TCP 编程核心知识点。
一、TCP 通信核心流程
TCP 通信基于三次握手建立连接 、四次挥手关闭连接,分为客户端(发端)和服务端(收端)两个角色,核心操作流程如下:
1.1 TCP 客户端(发端)流程
c
运行
socket() // 创建套接字
connect() // 发起三次握手,连接服务端
send() // 发送数据
recv() // 接收数据
close() // 关闭套接字,发起四次挥手
1.2 TCP 服务端(收端)流程
c
运行
socket() // 创建套接字
bind() // 绑定IP和端口
listen() // 监听连接请求
accept() // 接受连接(阻塞等待)
recv() // 接收客户端数据
send() // 向客户端发送数据
close() // 关闭套接字
二、TCP 核心函数接口详解
2.1 基础套接字操作
1. socket () - 创建套接字
c
运行
int socket(int domain, int type, int protocol);
// 示例:创建TCP套接字
int tcpsockfd = socket(AF_INET, SOCK_STREAM, 0);
- domain :地址族,
AF_INET表示 IPv4 - type :套接字类型,
SOCK_STREAM表示 TCP(流式),SOCK_DGRAM表示 UDP - protocol:协议类型,0 表示默认
2. listen () - 监听连接请求
c
运行
int listen(int sockfd, int backlog);
- 功能:监听三次握手请求,将未处理的连接放入队列
- backlog:未处理连接的最大排队数(如 5、10)
- 返回值:成功 0,失败 - 1
3. accept () - 接受连接
c
运行
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:处理连接队列中的第一个请求,建立客户端与服务端的连接
- addr:输出参数,存储客户端的 IP 和端口
- 返回值:成功返回新的套接字描述符(用于与该客户端通信),失败 - 1
4. connect () - 发起连接
c
运行
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:向服务端发送三次握手请求
- addr:服务端的 IP 和端口
- 返回值:成功 0,失败 - 1
2.2 数据收发操作
1. send () - 发送数据
c
运行
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- flags:默认为 0(阻塞发送)
- 返回值:成功返回发送的字节数,失败 - 1
2. recv () - 接收数据
c
运行
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 返回值:成功返回接收字节数,失败 - 1,对方关闭连接返回 0
三、TCP 与 UDP 核心差异
表格
| 特性 | TCP | UDP |
|---|---|---|
| 资源开销 | 大(包头至少 20 字节) | 小(包头仅 8 字节) |
| 连接特性 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传、有序) | 不可靠(无确认机制) |
| 传输方式 | 流式传输 | 数据报传输 |
| 机制复杂度 | 复杂(流量 / 拥塞控制、断线检测) | 简单(无额外控制机制) |
四、TCP 粘包问题及解决
4.1 粘包原因
TCP 是流式传输,无数据边界,发送方多次发送的小数据包可能被内核合并为一个数据包发送,接收方一次读取到多个数据包的内容,即 "粘包"。
4.2 解决方法
核心思路:设定数据边界,让接收方能够拆分连续数据,常见方案:
- 固定长度:约定每个数据包长度(如每次发送 1024 字节),接收方按固定长度读取;
- 分隔符 :在数据包末尾添加特殊分隔符(如
\n、|),接收方按分隔符拆分; - 头部标识长度:数据包头部添加长度字段(如 4 字节表示数据长度),接收方先读长度再读数据。
示例(分隔符方案):
c
运行
// 发送方:数据末尾加换行符
char buf[] = "hello world\n";
send(sockfd, buf, strlen(buf), 0);
// 接收方:按换行符拆分数据
char recv_buf[1024] = {0};
int n = recv(sockfd, recv_buf, sizeof(recv_buf)-1, 0);
char *p = strtok(recv_buf, "\n"); // 按换行符拆分
while(p != NULL) {
printf("收到数据:%s\n", p);
p = strtok(NULL, "\n");
}
五、TCP 并发服务器实现
5.1 并发服务器的核心问题
单线程服务端中,accept()(等待新连接)和recv()(等待数据)都是阻塞 IO,无法同时处理多个客户端的连接和数据收发,导致服务端只能串行处理客户端请求。
5.2 Linux IO 模型基础
解决并发问题的核心是理解 Linux 的 4 种 IO 模型:
表格
| IO 模型 | 特点 |
|---|---|
| 阻塞 IO | 数据未就绪时,进程阻塞等待,不占用 CPU 资源(默认的accept/recv均为阻塞 IO) |
| 非阻塞 IO | 数据未就绪时立即返回,需轮询检查,CPU 开销大(通过fcntl设置) |
| 异步 IO | 内核监测到 IO 事件后主动向应用层发信号,无需轮询 |
| 多路复用 IO | 一个接口监听多个文件描述符,任一就绪则返回(select/poll/epoll) |
关键函数:fcntl(设置非阻塞 IO)
c
运行
int fcntl(int fd, int cmd, ...);
// 将套接字设置为非阻塞
int flag = fcntl(fd, F_GETFL); // 获取当前属性
fcntl(fd, F_SETFL, flag | O_NONBLOCK); // 添加非阻塞属性
关键函数:select(多路复用 IO)
c
运行
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 辅助宏
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 将fd加入集合
FD_CLR(int fd, fd_set *set); // 从集合删除fd
FD_ISSET(int fd, fd_set *set); // 判断fd是否在就绪集合中
- nfds:监听的最大文件描述符 + 1
- timeout:超时时间(NULL 表示永久阻塞)
- 返回值:就绪的文件描述符个数,超时返回 0,失败返回 - 1
5.3 TCP 并发服务器实现方案
方案 1:线程 / 进程模型(简单易实现)
- 思路:服务端主线程调用
accept()接受连接,每建立一个连接就创建一个子线程 / 子进程,专门处理该客户端的recv/send。 - 优点:实现简单,新手易上手;
- 缺点:资源消耗大,连接数过多时线程 / 进程切换开销高。
方案 2:多路复用模型(高性能)
- 思路:用
select/epoll监听所有文件描述符(包括监听套接字和已连接套接字),根据就绪事件处理连接或数据收发。 - 示例(select 实现简单并发):




运行
六、抓包调试技巧(Wireshark)
开发中可通过 Wireshark 抓取 TCP/UDP 包调试:
- 启动 Wireshark:
sudo wireshark - 过滤规则:
- 过滤 TCP 包:
tcp - 过滤指定端口:
tcp.port == 50000或udp.port == 50000 - 过滤指定 IP:
ip.addr == 192.168.0.165
- 过滤 TCP 包:
总结
- TCP 通信是面向连接的可靠传输,核心流程为 "客户端 connect 连接,服务端 listen/accept 接收连接,双方 send/recv 收发数据";
- TCP 粘包源于流式传输无边界,解决核心是 "约定数据边界(固定长度 / 分隔符 / 长度头)";
- TCP 并发服务器可通过 "线程 / 进程模型(简单)" 或 "多路复用 IO 模型(高性能)" 实现,多路复用是高并发场景的首选方案。