大家好,这里是物联网心球。
今天我们来讨论一个比较热门的话题TCP Nagle算法。关于TCP Nagle算法,大家或多或少接触过。但是,大家未必能够把TCP Nagle算法讲清楚。面试时,考官也喜欢问Nagle算法相关的问题。所以我们很有必要把这个问题搞清楚。
1.什么是Nagle算法
Nagle算法是一种网络优化算法,由John Nagle在1984年提出,旨在减少网络中小数据包的数量,从而降低网络拥塞风险并提升传输效率。其核心思想是:通过延迟发送小数据包,合并多个小包为一个较大数据包后再发送。

图1 Nagle算法
如图1所示,用户程序调用send函数发送TCP数据段时,首先会从TCP套接字发送缓冲区获取到一个skb,接着判断skb是否能缓存TCP数据段,如果skb不能再存储数据,则申请一个新的skb。
Nagle算法的核心在于理解立即发送条件,立即发送条件有以下几种情况:
数据包长度达到 MSS(最大分段大小,通常1460字节),立即发送。
数据包包含TCP连接的终止标志 FIN,立即发送。
启用TCP_NODELAY选项,完全禁用Nagle算法,所有数据立即发送。
所有已发送的小数据包(长度< MSS)均已收到 ACK 确认,允许发送新数据。
未满足以上条件但等待超过 200ms,强制发送缓存数据。
开启Nagle算法后,未满足以上任何一个条件,数据包将延时发送,如果能够和后续数据包合并,则将多个小包合并为大的数据包再发送。
Nagle算法适用于Telnet、SSH等交互式应用场景,但对于实时性要求很高的场景,如在线游戏、实时通信等场景,通常需要关闭Nagle算法。
2.TCP_NODELAY和TCP_CORK套接字选项
默认情况下,Linux是开启Nagle算法的,我们可以通过TCP_NODELAY和TCP_CORK套接字选项来开启和关闭Nagle算法。
TCP_NODELAY选项用来开启和关闭Nagle算法,TCP_CORK与TCP_NODELAY是互斥的,用于强制累积数据至大块后再发送,适用于文件传输等场景。
二者使用示例代码如下:
int opt = 1; /* 1:开启;0:关闭 */
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
int opt1 = 1; /* 1:开启;0:关闭 */
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &opt1, sizeof(opt1));
TCP_NODELAY和TCP_CORK两个选项一定要放在一起讲解,内核实现原理如图2所示。

图2 TCP_NODELAY和TCP_CORK套接字选项
对于内核来说,用户程序不管是设置TCP_NODELAY,还是设置TCP_CORK,最后设置的都是TCP套接字的nonagle成员。nonagle是一个8位整型数,内核将nonagle低三位分别用来表示:TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF。
三者定义如下:
#define TCP_NAGLE_OFF 1
#define TCP_NAGLE_CORK 2
#define TCP_NAGLE_PUSH 4
TCP_NAGLE_OFF:完全禁用Nagle算法。
TCP_NAGLE_CORK:启用"塞子"模式,强制延迟发送以积累更大数据块。
TCP_NAGLE_PUSH:绕过Nagle算法的延迟限制,立即发送数据。
开启TCP_NODELAY选项后,nonagle的TCP_NAGLE_PUSH和TCP_NAGLE_OFF标志将被设置。关闭TCP_NODELAY选项后,nonagle的TCP_NAGLE_OFF标志将被清除。
开启TCP_CORK选项后,nonagle的TCP_NAGLE_CORK标志将被设置。关闭TCP_CORK选项后,nonagle的TCP_NAGLE_CORK标志将被清除。如果TCP_NODELAY选项已开启,则重置TCP_NAGLE_PUSH标志。
nonagle的设置逻辑比较绕,初学者理解起来比较困难,我们把所有的情况都以表格的形式来表示,见表1。

表1 nonagle标志的几种情况
前面,我们花了比较多的篇幅来讲解TCP_NODELAY和TCP_CORK内核实现原理,目的是为下面的内容做铺垫。
用户程序调用send函数发送TCP数据段时,内核会根据nonagle的TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF标志决定是立即发送数据还是累积数据包,如图3所示。

图3 nonagle三个标志优先级
三个标志的优先级从高到低分别为:TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF。首先,判断nonagle是否设置TCP_NAGLE_PUSH标志。如果置位,则强制发送数据;否则,则继续判断TCP_NAGLE_CORK标志。如果TCP_NAGLE_CORK置位,则强制积累数据包;否则,则继续判断TCP_NAGLE_OFF标志。如果TCP_NAGLE_OFF置位,则关闭Nagle算法,强制发送数据包;否则,则启用Nagle算法,延迟发送数据包。
小结一下,TCP套接字是否延迟发送数据受nonagle的TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF标志的影响,开启和关闭TCP_NODELAY和TCP_CORK选项只是对这三个标志进行修改,想要TCP套接字按照我们预定的方式发送数据,可以参考表1来设置TCP_NODELAY和TCP_CORK。
3.TCP_CORK覆盖TCP_NODELAY问题分析
前面,我们已经对TCP_CORK和TCP_NODELAY做了非常详细的讲解,最后,还有一个问题,如果同时开启TCP_NODELAY和TCP_CORK会怎样?
这个问题对表1的情况4,TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF同时置位。按照图3的原理分析,应该是TCP_NAGLE_PUSH生效,即立即发送数据。然而实际情况是TCP_NAGLE_CORK生效,也就是TCP_CORK将TCP_NODELAY覆盖了。为什么会这样呢?通过对内核源码的剖析,我们发现了一点端倪。内核源码如下:
/*
将skb插入发送缓冲区队尾
*/
void tcp_skb_entail(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
tcb->seq = tcb->end_seq = tp->write_seq;
tcb->tcp_flags = TCPHDR_ACK;
__skb_header_release(skb);
tcp_add_write_queue_tail(sk, skb);
sk_wmem_queued_add(sk, skb->truesize);
sk_mem_charge(sk, skb->truesize);
if (tp->nonagle & TCP_NAGLE_PUSH)/* 如果nonagle TCP_NAGLE_PUSH置位,则清除该标志*/
tp->nonagle &= ~TCP_NAGLE_PUSH;
tcp_slow_start_after_idle_check(sk);
}
通过源码分析得知,TCP套接字发送数据时,如果申请的skb插入发送缓冲区队尾,那么nonagle的TCP_NAGLE_PUSH将被清除,此时生效的则是TCP_NAGLE_CORK标志。这也就解释了为什么TCP_CORK会覆盖TCP_NODELAY。
