Linux32 网络编程TCP通信(缓冲区问题)

本文章主要讲解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()返回 -1errno=EAGAINEWOULDBLOCK(缓冲区已满,无法写入);

  • 辅助验证:netstat -an | grep 端口 查看 Send-Q(发送队列积压字节数),持续大于 0 且增长,说明阻塞。

核心原因

  1. 接收端接收窗口过小:接收缓冲区满(应用层recv()不及时),接收端告知发送端 "窗口 = 0",发送端无法继续发送;

  2. 网络拥塞:拥塞窗口(cwnd)被内核缩小,导致发送窗口 = min (接收窗口,拥塞窗口) 变小,数据滞留在发送缓冲区;

  3. 发送缓冲区配置过小:系统默认缓冲区(如 Linux 默认tcp_wmem最小 4KB)无法满足批量发送需求,频繁触发阻塞;

  4. 应用层盲目发送:非阻塞模式下未监听 "可写事件",反复调用send()导致缓冲区溢出。

解决方案

  • 应用层优化:

非阻塞 + I/O 多路复用:用epoll/select/poll监听套接字 "可写事件"(EPOLLOUT),仅当缓冲区有空闲时才send()

批量发送数据:合并小数据包(如累计到 4KB 再发送),减少send()调用次数,降低缓冲区占用;

处理EAGAIN重试:非阻塞模式下收到EAGAIN时,不要立即重试,等待 "可写事件" 触发后再试。

问题 2:接收缓冲区积压 / 数据读取不及时

现象

  • 应用层recv()获取的数据滞后,或偶尔丢失(实际是积压导致 "看似丢失");

  • netstat -an | grep 端口 查看 Recv-Q(接收队列积压字节数),持续大于 0 且增长;

  • 抓包显示接收端已收到数据,但应用层未及时读取,导致接收端发送 "窗口 = 0" 报文。

核心原因

  1. 应用层recv()频率过低:业务逻辑阻塞(如 sleep、数据库慢查询),导致接收缓冲区数据无法及时消费;

  2. 接收缓冲区配置过小:默认缓冲区(如 Linuxtcp_rmem默认 4KB)无法容纳突发数据(如一次性接收 100KB 数据);

  3. 接收端系统负载过高: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 字节流特性,与缓冲区联动相关,需应用层自行定义消息边界

  1. 固定消息长度:如每次发送 1024 字节,接收端每次recv()固定长度;

  2. 分隔符标识:用特殊字符(如\n\0)作为消息结束标志,接收端按分隔符拆分;

  3. 消息头 + 消息体:消息头存储消息总长度(如 4 字节 int),接收端先读消息头,再按长度读消息体。

问题 5:缓冲区配置过大导致的问题

现象

  • 内存浪费:大缓冲区长期占用内核内存,尤其高并发场景(如 1000 个连接,每个缓冲区 100KB,共占用 100MB);

  • 加剧网络拥塞:大缓冲区允许发送端一次性发送大量数据,涌入网络后导致链路拥塞,反而降低吞吐量。

解决方案

  • 缓冲区大小适配场景:

    • 小数据高频传输(如 RPC 调用):缓冲区设置为 4KB~16KB,避免浪费;

    • 大数据批量传输(如文件传输):缓冲区设置为 64KB~256KB,提升吞吐量;

问题6:抓包分析(定位根因)

用 Wireshark 或tcpdump抓包,重点关注:

  • 窗口大小字段:接收端是否发送 "窗口 = 0";

  • ACK 报文:是否有 ACK 丢失(导致发送端重传);

  • 报文序号:是否有重复报文(重传导致);

  • 传输速率:发送端发送速率是否远超接收端读取速率。

总结

TCP 缓冲区是 "可靠传输" 与 "性能效率" 的平衡支点:过小会导致阻塞、丢包或频繁系统调用;过大则浪费内存或加剧拥塞。实际应用中需结合业务场景(如高吞吐量、低延迟),通过系统参数调整和应用层逻辑优化,实现缓冲区的最优配置。

相关推荐
NiKo_W2 小时前
Linux 重定向与Cookie
linux·运维·服务器·前端·网络·线程·协议
HLJ洛神千羽2 小时前
Linux下程序设计综合实验报告——图书管理系统(黑龙江大学)
linux·运维·服务器
观北海2 小时前
网络安全等保测评实践指南:从理论到技术实现
网络·安全·web安全
云盾安全防护2 小时前
DNS防护:企业网络稳定性的第一道隐形防线
网络
学渣676562 小时前
个人笔记|单臂路由,子接口,VLAN标签
网络·笔记·智能路由器
Arvin6272 小时前
Jenkins Jobs 备份与恢复
linux·运维·服务器
Fnetlink13 小时前
智网案例精选|光联云网融合智驱,重塑金融数字化转型新格局
网络·信息与通信
焦糖码奇朵、3 小时前
移动通信网络建设-实验2:5G站点选型与设备部署
网络·数据库·人工智能·5g·信号处理·基带工程
Albert Edison4 小时前
【项目设计】基于正倒排索引的Boost搜索引擎
linux·网络·c++·后端·http·搜索引擎