45、TCP_CORK选项:TCP_CORK与Nagle区别、数据累积发送

说实话,TCP_CORK 这个选项,我刚开始接触的时候也觉得挺绕的。它和 Nagle 算法看起来都在做「延迟发送」这件事,但背后的设计思路完全不同。今天咱们就把这两个东西掰开揉碎了讲清楚。

先说说 Nagle 算法

Nagle 算法,说白了就是为了解决「小包问题」的。你想想看,如果每次 send 一个字节,网络层就发一个包,那效率得多低?TCP 头部就 20 字节,数据才 1 字节,这比例太离谱了。

Nagle 算法的核心规则很简单:

  • 如果已发送的数据还没收到 ACK,就把新数据攒着
  • 等收到 ACK 了,或者攒够一个 MSS(最大报文段长度),再发出去

嗯,这里要注意一点:Nagle 算法是 TCP 协议栈默认开启的。你写个简单的 socket 程序,它就在后台默默工作着。

Nagle 算法的本质:减少网络中微小报文的数量,以牺牲一点延迟换取带宽利用率。

TCP_CORK 是什么?

TCP_CORK 这个选项,名字挺形象——就像给 TCP 输出管道塞了个软木塞。开启后,数据会被「堵」在发送缓冲区里,直到你主动拔掉塞子(关闭 CORK),或者缓冲区满了,才会一次性发出去。

我个人习惯把 TCP_CORK 叫做「手动挡的 Nagle」。为什么这么说?因为 Nagle 是自动的,你控制不了它什么时候发;而 CORK 给了你完全的控制权。

我的经验:在做 HTTP 响应头 + 响应体的拼接发送时,我特别喜欢用 TCP_CORK。先把所有头部数据 write 进去,再把 body 数据 write 进去,最后关闭 CORK,内核会把这些数据合并成一个 TCP 段发出去。这样能避免头部和 body 分开发送导致的多个小包。

核心区别对比

对比项 Nagle 算法 TCP_CORK
触发条件 未收到 ACK 且数据不足 MSS 手动开启/关闭
控制粒度 自动,应用层无法干预 完全由应用层控制
释放时机 收到 ACK 或攒够 MSS 关闭 CORK 或缓冲区满
适用场景 通用场景,默认开启 需要批量发送的场景
延迟影响 可能增加几十毫秒延迟 延迟由你决定

你看这个表格就清楚了。Nagle 是「被动攒」,CORK 是「主动攒」。一个由内核决定什么时候发,一个由你决定。

数据累积发送的实战代码

我记得有一次做文件传输服务,客户端需要先发送文件元信息(文件名、大小等),再发送文件内容。如果不用 CORK,这两个数据很可能分开发送,导致接收端要读两次。

来看看代码怎么写:

#include <sys/socket.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int enable_cork(int fd) {
    int optval = 1;
    return setsockopt(fd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
}

int disable_cork(int fd) {
    int optval = 0;
    return setsockopt(fd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));
}

void send_with_cork(int fd) {
    char header[] = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
    char body[] = "Hello";

    // 开启 CORK,开始累积数据
    if (enable_cork(fd) < 0) {
        perror("enable_cork");
        return;
    }

    // 先写入头部
    write(fd, header, strlen(header));
    // 再写入 body
    write(fd, body, strlen(body));

    // 关闭 CORK,一次性发送
    if (disable_cork(fd) < 0) {
        perror("disable_cork");
        return;
    }

    printf("数据已累积发送,共 %zu 字节\n", strlen(header) + strlen(body));
}

我曾经踩过的坑:有一次我开启了 CORK 后,忘记关闭了。结果对端一直收不到数据,直到缓冲区满了才发出去。排查了半天才发现是 CORK 没关。所以一定要记得成对使用——开启后务必在合适时机关闭。

什么时候该用 CORK?

我总结了几种典型场景:

  • HTTP 响应拼接:头部和 body 一起发,减少小包
  • 自定义协议头+负载:协议头和数据一起发送
  • 批量日志上报:攒够一批日志再发送
  • 文件传输:元信息和文件内容合并发送

但也不是所有场景都适合。比如实时性要求高的场景(游戏、视频通话),用 CORK 反而会引入不必要的延迟。这时候你可能需要关闭 Nagle(设置 TCP_NODELAY),而不是用 CORK。

两者能同时用吗?

这个问题我经常被问到。答案是:可以,但没必要。因为 CORK 开启时,Nagle 算法实际上被覆盖了——CORK 的优先级更高。你开启 CORK 后,Nagle 的规则就不起作用了,直到你关闭 CORK。

说白了,CORK 是 Nagle 的「加强版」。Nagle 帮你做决定,CORK 让你自己做决定。

SVG 流程图:TCP_CORK 数据累积发送流程

TCP_CORK 数据累积发送流程 应用层 write(header) write(body) TCP_CORK 开启:数据被堵在发送缓冲区 缓冲区累积:header + body 合并存储 关闭 CORK(拔掉塞子) 一次性发送:合并后的 TCP 段 减少小包,提高网络利用率 对比 Nagle:CORK 由应用层主动控制

避坑指南

最后分享几个我实战中遇到的坑:

  • 不要长时间开启 CORK:如果数据一直不够,对端会一直收不到。设置一个超时机制,或者定期关闭再开启。
  • 注意平台兼容性:TCP_CORK 是 Linux 特有的选项。macOS 和 FreeBSD 上对应的是 TCP_NOPUSH,行为类似但名字不同。
  • 和 Nagle 的交互:开启 CORK 后,Nagle 自动失效。但关闭 CORK 后,Nagle 会恢复。如果你不想用 Nagle,记得同时设置 TCP_NODELAY。
  • 性能测试:我建议你在实际使用前做一下压测。CORK 在吞吐量上通常有提升,但延迟会变高。看你的业务更看重哪个。

我的建议:如果你刚开始用 CORK,先从简单的 HTTP 响应拼接开始。等熟悉了行为模式,再应用到更复杂的协议中。记住一句话:CORK 是工具,不是银弹。

好了,关于 TCP_CORK 和 Nagle 的区别,以及数据累积发送的实现,就讲到这里。这东西用好了,能显著减少网络小包,提升整体吞吐量。但用不好,也可能引入不必要的延迟。关键还是理解它的工作原理,然后根据场景做选择。


公众号:蓝海资料掘金营,微信deep3321