一文搞懂TCP Nagle算法

一文搞懂TCP Nagle算法

目录

大家好,这里是物联网心球。

今天我们来讨论一个比较热门的话题TCP Nagle算法。关于TCP Nagle算法,大家或多或少接触过。但是,大家未必能够把TCP Nagle算法讲清楚。面试时,考官也喜欢问Nagle算法相关的问题。所以我们很有必要把这个问题搞清楚。

1.什么是Nagle算法

Nagle算法是一种网络优化算法,由John Nagle在1984年提出,旨在减少网络中小数据包的数量,从而降低网络拥塞风险并提升传输效率。其核心思想是:通过延迟发送小数据包,合并多个小包为一个较大数据包后再发送。

一文搞懂TCP Nagle算法 图1

图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所示。

一文搞懂TCP Nagle算法 图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。

一文搞懂TCP Nagle算法 图3

表1 nonagle标志的几种情况

前面,我们花了比较多的篇幅来讲解TCP_NODELAY和TCP_CORK内核实现原理,目的是为下面的内容做铺垫。

用户程序调用send函数发送TCP数据段时,内核会根据nonagle的TCP_NAGLE_PUSH、TCP_NAGLE_CORK、TCP_NAGLE_OFF标志决定是立即发送数据还是累积数据包,如图3所示。

一文搞懂TCP Nagle算法 图4

图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。

← 返回文章列表