本文章主要讲解TCP通信流程 缓冲区问题
Linux30 网络编程TCP流程我们根据上次文章:Linux30 网络编程TCP流程建立TCP通信
缓冲区问题引入前提
服务器端代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//TCP套接字,文件描述符
if( sockfd == -1)
{
exit(1);
}
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
exit(1);
}
res = listen(sockfd,5);//设置监听队列的大小
if(res == -1)
{
exit(1);
}
while( 1 )
{
int len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//没有客户端连接,accept阻塞
if( c < 0 )
{
continue;
}
printf("accept c=%d\n",c);
while(1){
char buff[128] = {0};
int n = recv(c,buff,127,0);//read()
if(n<=0){
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);//write()
}
close(c);
}
}
客户端代码:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1)
{
exit(1);
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//连接服务器,三次握手
if( res == -1 )
{
printf("connect err\n");
exit(1);
}
printf("connect success\n");
while(1){
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0){
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}
运行结果:

根据运行结果可知可以实现客户端发送数据到服务器 服务器收到后向客户端发送ok.
缓冲区问题
我们对以上代码进行修改,将服务器每次接受一个字节的内容:
cpp
char buff[128] = {0};
int n = recv(c,buff,1,0);//read()
if(n<=0){
printf("cil close\n");
break;
}
我们第一次输入1234,收到1个ok

我们利用netstat -natp可以查看发送缓冲区内容字节大小

缓冲区详解
TCP 的缓冲区是保障其 "可靠、高效" 传输的核心组件,分为发送缓冲区 和接收缓冲区,涉及流量控制、拥塞控制、性能优化等关键机制。
1. 发送缓冲区
-
作用:
-
存储待发送的数据(应用层
send后的数据先进入发送缓冲区,由 TCP 内核决定何时发送)。 -
存储已发送但未收到 ACK 的数据(用于超时重传)。
-
-
大小影响:
-
过小:需频繁触发
send系统调用,增加 CPU 开销;且易因发送窗口不足导致数据滞留。 -
过大:可能加剧网络拥塞(大量数据同时涌入网络),且占用过多内存。
-
2. 接收缓冲区
-
作用:
-
存储已接收但未被应用层
recv的数据(TCP 先将数据放入接收缓冲区,应用层按需读取)。 -
实现流量控制(通过 "窗口大小" 字段告知发送端自己的剩余容量)。
-
-
大小影响:
-
过小:易因应用层读取不及时导致缓冲区满,触发发送端窗口关闭(数据无法继续传输)。
-
过大:可提升吞吐量(减少因窗口不足导致的等待),但占用内存较多。
-

缓冲区相关的核心机制
1. 滑动窗口与缓冲区的关联
TCP 的 "滑动窗口" 本质是发送缓冲区与接收缓冲区的协同机制:
-
发送窗口 ≤ 接收端的接收窗口(由接收缓冲区剩余容量决定)。
-
发送端仅能在 "发送窗口" 内发送数据,窗口随 ACK 确认而滑动(已确认的数据从发送缓冲区释放)。
2. 零窗口与窗口探测
-
当接收缓冲区满时,接收端会发送 "窗口大小 = 0" 的报文,发送端进入零窗口等待状态。
-
为避免长时间等待(如接收端恢复后未发窗口更新),发送端会周期性发送窗口探测报文(携带 1 字节数据),接收端若已恢复则返回新的窗口大小。
3. 缓冲区与拥塞控制的联动
发送缓冲区的可用空间还受拥塞窗口(cwnd) 限制:
-
发送窗口 = min (接收窗口,拥塞窗口)。
-
拥塞控制通过调整 cwnd,避免发送缓冲区数据过多导致网络拥塞。
常见缓冲区问题及排查
问题 1:发送缓冲区阻塞 / 溢出(最常见)
现象:
阻塞模式:
send()调用后卡住,长时间不返回(内核等待缓冲区有空闲空间);非阻塞模式:
send()返回-1,errno=EAGAIN或EWOULDBLOCK(缓冲区已满,无法写入);辅助验证:
netstat -an | grep 端口查看Send-Q(发送队列积压字节数),持续大于 0 且增长,说明阻塞。
核心原因
接收端接收窗口过小:接收缓冲区满(应用层
recv()不及时),接收端告知发送端 "窗口 = 0",发送端无法继续发送;网络拥塞:拥塞窗口(cwnd)被内核缩小,导致发送窗口 = min (接收窗口,拥塞窗口) 变小,数据滞留在发送缓冲区;
发送缓冲区配置过小:系统默认缓冲区(如 Linux 默认
tcp_wmem最小 4KB)无法满足批量发送需求,频繁触发阻塞;应用层盲目发送:非阻塞模式下未监听 "可写事件",反复调用
send()导致缓冲区溢出。
解决方案
- 应用层优化:
非阻塞 + I/O 多路复用:用
epoll/select/poll监听套接字 "可写事件"(EPOLLOUT),仅当缓冲区有空闲时才send();批量发送数据:合并小数据包(如累计到 4KB 再发送),减少
send()调用次数,降低缓冲区占用;处理
EAGAIN重试:非阻塞模式下收到EAGAIN时,不要立即重试,等待 "可写事件" 触发后再试。
问题 2:接收缓冲区积压 / 数据读取不及时
现象
应用层
recv()获取的数据滞后,或偶尔丢失(实际是积压导致 "看似丢失");
netstat -an | grep 端口查看Recv-Q(接收队列积压字节数),持续大于 0 且增长;抓包显示接收端已收到数据,但应用层未及时读取,导致接收端发送 "窗口 = 0" 报文。
核心原因
应用层
recv()频率过低:业务逻辑阻塞(如 sleep、数据库慢查询),导致接收缓冲区数据无法及时消费;接收缓冲区配置过小:默认缓冲区(如 Linux
tcp_rmem默认 4KB)无法容纳突发数据(如一次性接收 100KB 数据);接收端系统负载过高:CPU / 内存耗尽,内核无法及时将缓冲区数据交付应用层。
解决方案
及时消费数据:避免
recv()后做长时间阻塞操作,可将数据放入业务队列异步处理;增大单次
recv()读取长度:设置足够大的缓冲区(如 8KB/16KB),减少recv()调用次数;监听 "可读事件":用
epoll/select监听EPOLLIN事件,数据到达后立即读取,避免积压。
问题 3:零窗口与窗口探测(缓冲区满导致的传输暂停)
现象
发送端
send()阻塞或返回EAGAIN,抓包显示接收端发送 "窗口大小 = 0" 的报文;发送端周期性发送 1 字节的 "窗口探测报文",接收端无响应或响应 "窗口 = 0",传输长时间暂停。
核心原因
接收缓冲区完全满(
Recv-Q达到缓冲区上限),接收端告知发送端 "停止发送",但后续未及时更新窗口(如应用层仍未recv())。
解决方案
紧急处理:优先排查接收端
recv()逻辑,确保数据及时消费,释放接收缓冲区;启用窗口探测:Linux 默认启用窗口探测(
net.ipv4.tcp_window_scaling=1),无需额外配置,若接收端恢复,会返回新的窗口大小;避免长时间零窗口:应用层需设置 "接收超时",若长时间零窗口,可主动关闭连接并重连。
问题 4:缓冲区导致的粘包 / 分包(字节流特性 + 缓冲区联动)
现象
粘包:发送端连续发送
"Hello"和"World",接收端recv()一次读到"HelloWorld";分包:发送端发送 10KB 数据,接收端
recv()两次才读完(如第一次读 4KB,第二次读 6KB)。
核心原因
粘包:发送端缓冲区合并小数据包(减少网络传输次数),或接收端缓冲区未及时读取,多个数据包积压后一起交付;
分包:发送端缓冲区不足,或网络 MTU 限制(默认 1500 字节),TCP 将大数据拆分为多个报文段发送。
解决方案
粘包 / 分包是 TCP 字节流特性,与缓冲区联动相关,需应用层自行定义消息边界:
固定消息长度:如每次发送 1024 字节,接收端每次
recv()固定长度;分隔符标识:用特殊字符(如
\n、\0)作为消息结束标志,接收端按分隔符拆分;消息头 + 消息体:消息头存储消息总长度(如 4 字节 int),接收端先读消息头,再按长度读消息体。
问题 5:缓冲区配置过大导致的问题
现象
内存浪费:大缓冲区长期占用内核内存,尤其高并发场景(如 1000 个连接,每个缓冲区 100KB,共占用 100MB);
加剧网络拥塞:大缓冲区允许发送端一次性发送大量数据,涌入网络后导致链路拥塞,反而降低吞吐量。
解决方案
缓冲区大小适配场景:
小数据高频传输(如 RPC 调用):缓冲区设置为 4KB~16KB,避免浪费;
大数据批量传输(如文件传输):缓冲区设置为 64KB~256KB,提升吞吐量;
问题6:抓包分析(定位根因)
用 Wireshark 或tcpdump抓包,重点关注:
-
窗口大小字段:接收端是否发送 "窗口 = 0";
-
ACK 报文:是否有 ACK 丢失(导致发送端重传);
-
报文序号:是否有重复报文(重传导致);
-
传输速率:发送端发送速率是否远超接收端读取速率。
总结
TCP 缓冲区是 "可靠传输" 与 "性能效率" 的平衡支点:过小会导致阻塞、丢包或频繁系统调用;过大则浪费内存或加剧拥塞。实际应用中需结合业务场景(如高吞吐量、低延迟),通过系统参数调整和应用层逻辑优化,实现缓冲区的最优配置。