Author basilguo@163.com

Date Aug. 23, 2023

Description TCP_CORK与TCP_NODELAY以及测试

1. 分析

前些天在看NIST BGP SRx的源码的时候,发现它有在用TCP_CORK,一时之间有些懵逼,不知道这是啥,就学习一下吧。

在NIST源码中,它是如下的,实际有用的就是setsockopt()

int sockopt_cork (int sock, int onoff)
{
#ifdef TCP_CORK
  return setsockopt (sock, IPPROTO_TCP, TCP_CORK, &onoff, sizeof(onoff));
#else
  return 0;
#endif
}

TCP_CORK在Linux内核中于2.2版本引入,TCP_CORK基本上是Linux系统特定的,而其它类UNIX系统可能可以实现类似功能的有例如BSD系统的TCP_NOPUSH。在Linux Man page中的解释是:

If set, don't send out partial frames. All queued partial frames are sent when the option is cleared again. This is useful for prepending headers before calling sendfile(2), or for throughput optimization. As currently implemented, there is a 200 millisecond ceiling on the time for which output is corked by TCP_CORK. If this ceiling is reached, then queued data is automatically transmitted. This option can be combined with TCP_NODELAY only since Linux 2.5.71. This option should not be used in code intended to be portable.

翻译:如果设置了TCP_CORK,那么将不会发送部分帧(这里应该是指小于MSS的帧)。所有队列中的部分帧将会在TCP_CORK option清除之后发送。这对于调用sendfile(2)之前的前置头文件或者吞吐量优化非常有用。根据目前的实现,TCP_CORK对输出进行阻塞的时间上限是200ms,如果达到了这个上限,那么队列数据将会自动传输。TCP_CORK option只能在Linux 2.5.71之后与TCP_NODELAY联合使用。此选项不适合用于可移植代码中。

实际上如果帧超过了MSS大小,或者socket被关闭了,TCP也会将数据立刻发送出去。

sendfile()是用于在不同的文件描述符之间传输数据的,是在内核完成,而不经过用户空间,所以它很快,比read/write要快,而且可以用于加速disk和network之间的数据传输。其函数原型是:

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t * offset, size_t count);

TCP默认启用的是Nagle算法,由John Nagle于1984年实现用于拥塞控制,在RFC896对这个以后命名为Nagle算法的拥塞控制算法有所描述。在RFC1122第4.2.3.4节中有简略描述:

The Nagle algorithm is generally as follows:

  If there is unacknowledged data (i.e., SND.NXT >
  SND.UNA), then the sending TCP buffers all user
  data (regardless of the PSH bit), until the
  outstanding data has been acknowledged or until
  the TCP can send a full-sized segment (Eff.snd.MSS
  bytes; see Section 4.2.2.6).

也就是本端每发送一个数据,就需要peer回复一个ACK,收到ACK本端才会发送下一个数据;或者就是凑成一个满了的帧(指帧大小等于MSS了)之后发送下一个数据,也即Nagle算法是用于减少网络中小包的数量的。

网络数据包基本可以分为两部分,一部分是header,一部分是body(data),不管如何,有用的总是data部分,头部占比太大,会使得网络效率和吞吐就特别低,例如一个常用的命令行程序敲击一个回车,就会发送一个数据包,这个数据包头部为40字节,但是只包含1字节的有用数据(回车),这个有效载荷比很小。

使用Nagle算法的TCP,默认的数据传输模式是阻塞形式的,这样就会增加TCP发送数据的延迟,低延迟应用就需要使用TCP_NODELAY禁用Nagle算法:

int optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &optval, sizeof(optval));

OK,但有时候,并没有那么急迫的要把数据发送出去,而是需要在有大量数据后再一次性发送出去,此时就可以使用TCP_CORK了:

int optval = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_CORK, &optval, sizeof(optval));

总结来说,使用TCP_CORK的场景就是要一次性发送大量的数据,而这些数据不急迫而且size很小,例如HTTP的响应,相反则使用TCP_NODELAY,两者是互斥的。但真实世界中的多数流行程序并没有真的这样去实现,例如Eric Allman的sendmail程序,完全没有使用tcp options;Apache HTTPD程序的所有socket都使用了TCP_NODELAY,用户对其性能也很满意。这是因为不同操作系统实现细节上的不同。但建议是最好遵从上述的原则。

2. 测试

虽然有说可以使用packetdrill去测试,但是我不会使用。写一个简单的程序测试一下吧,源代码附在了最后。在这个S/C程序中,通过client来发送数据,server接收展示数据。client展示了默认情况、使用TCP_NODELAY以及使用TCP_CORK的区别。最终没有看出来使用TCP_NODELAY和完全不设置的区别,而设置了TCP_CORK之后,必然至少会把整个buf的26次循环以及最后一行分隔符整合成一个数据包发送。理想情况下的输出应该有类似如下的server打印输出。不用自己手动输入测试是因为手工输入很难不超出200ms的限制,这就测不出来啥了,只能是程序循环。

[1] a
[2] ababc
[3] abcd
[4] abcde
[5] abcdef
[6] abcdefg
[7] abcdefgh
[8] abcdefghi
[9] abcdefghij
[10] abcdefghijk
[11] abcdefghijkl
[12] abcdefghijklm
[13] abcdefghijklmn
[14] abcdefghijklmno
[15] abcdefghijklmnop
[16] abcdefghijklmnopq
[17] abcdefghijklmnopqr
[18] abcdefghijklmnopqrs
[19] abcdefghijklmnopqrst
[20] abcdefghijklmnopqrstu
[21] abcdefghijklmnopqrstuv
[22] abcdefghijklmnopqrstuvw
[23] abcdefghijklmnopqrstuvwx
[24] abcdefghijklmnopqrstuvwxy
[25] abcdefghijklmnopqrstuvwxyz
[26] ====================
[27] a
[28] ab
[29] abc
[30] abcd
[31] abcde
[32] abcdef
[33] abcdefg
[34] abcdefgh
[35] abcdefghi
[36] abcdefghij
[37] abcdefghijk
[38] abcdefghijkl
[39] abcdefghijklm
[40] abcdefghijklmn
[41] abcdefghijklmno
[42] abcdefghijklmnop
[43] abcdefghijklmnopq
[44] abcdefghijklmnopqr
[45] abcdefghijklmnopqrs
[46] abcdefghijklmnopqrst
[47] abcdefghijklmnopqrstu
[48] abcdefghijklmnopqrstuv
[49] abcdefghijklmnopqrstuvw
[50] abcdefghijklmnopqrstuvwx
[51] abcdefghijklmnopqrstuvwxy
[52] abcdefghijklmnopqrstuvwxyz
[53] ====================
[54] aababcabcdabcdeabcdefabcdefgabcdefghabcdefghiabcdefghijabcdefghijkabcdefghijklabcdefghijklmabcdefghijklmnabcdefghijklmnoabcdefghijklmnopabcdefghijklmnopqabcdefghijklmnopqrabcdefghijklmnopqrsabcdefghijklmnopqrstabcdefghijklmnopqrstuabcdefghijklmnopqrstuvabcdefghijklmnopqrstuvwabcdefghijklmnopqrstuvwxabcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxyz====================

2.1. 程序代码

2.1.1. server.c

/******************************************************
 * File Name:    tcp_cork_test.c
 * Author:       basilguo@163.com
 * Created Time: 2023-08-23 03:38:43
 * Description:
 ******************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <string.h>

#define LISTEN_PORT 55555
#define LISTEN_BACKLOG 10


int main(int argc, char *argv[])
{
    struct sockaddr_in sockaddr, peer_addr;
    socklen_t peer_addr_size;
    int ret = 0;
    int sfd = -1, cfd = -1;
    int state = 1, n = 0;
    char buf[BUFSIZ];

    sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (sfd < 0)
    {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &state, sizeof(int));

    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(LISTEN_PORT);
    sockaddr.sin_addr.s_addr = INADDR_ANY;
    if ((ret = bind(sfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr))) < 0)
    {
        perror("bind()");
        close(sfd);
        exit(EXIT_FAILURE);
    }
    if ((ret = listen(sfd, LISTEN_BACKLOG)) < 0)
    {
        perror("listen()");
        close(sfd);
        exit(EXIT_FAILURE);
    }

    peer_addr_size = sizeof(peer_addr);
    while (cfd = accept(sfd, (struct sockaddr*)&peer_addr, &peer_addr_size))
    {
        setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, &state, sizeof(state));

        int times = 0;
        while (n = recv(cfd, buf, BUFSIZ, 0))
        {
            times ++;
            printf("[%d] %s\n", times, buf);
            bzero(buf, BUFSIZ);
        }

        close(cfd);
    }

    close(sfd);

    return 0;
}

2.1.2. client.c

/******************************************************
 * File Name:    tcp_cork_client.c
 * Author:       basilguo@163.com
 * Created Time: 2023-08-23 03:42:34
 * Description:
 ******************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#define SERVER_PORT 55555

void setsock(int cfd, int flag)
{
    int state = 1;

    switch (flag)
    {
        case 1:
            setsockopt(cfd, IPPROTO_TCP, TCP_CORK, &state, sizeof(state));
            break;
        case 2:
            setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, &state, sizeof(state));
            break;
        default:
            state = 0;
            setsockopt(cfd, IPPROTO_TCP, TCP_CORK, &state, sizeof(state));
            setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, &state, sizeof(state));
            break;
    }
}

int main(int argc, char *argv[])
{
    int cfd = -1, ret = 0, state = 0;
    struct sockaddr_in sockaddr;
    int n = 0;
    char buf[BUFSIZ];

    if ((cfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
    {
        perror("socket()");
        exit(EXIT_FAILURE);
    }

    sockaddr.sin_family = AF_INET;
    sockaddr.sin_port = htons(SERVER_PORT);
    sockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if ((ret = connect(cfd, (struct sockaddr*)&sockaddr, sizeof(sockaddr))) < 0)
    {
        perror("connect()");
        close(cfd);
        exit(EXIT_FAILURE);
    }


    struct timespec sts, ets;
    int i = 0;

    for (i=0; i<26; ++i)
    {
        buf[i] = 'a' + i;
    }

    for(i=30; i<50; ++i)
    {
        buf[i] = '=';
    }

    printf("NONE\t\t");
    setsock(cfd, 0);
    timespec_get(&sts, TIME_UTC);
    for (i=0; i<26; ++i)
    {
        send(cfd, buf, i+1, 0);
    }
    timespec_get(&ets, TIME_UTC);
    printf("% 20ld\n", (ets.tv_sec-sts.tv_sec)*1000000000 + (ets.tv_nsec-sts.tv_nsec));
    send(cfd, &buf[30], 20, 0);

    printf("TCP_NODELAY\t");
    setsock(cfd, 2);
    timespec_get(&sts, TIME_UTC);
    for (i=0; i<26; ++i)
    {
        send(cfd, buf, i+1, 0);
    }
    timespec_get(&ets, TIME_UTC);
    printf("% 20ld\n", (ets.tv_sec-sts.tv_sec)*1000000000 + (ets.tv_nsec-sts.tv_nsec));
    send(cfd, &buf[30], 20, 0);

    printf("TCP_CORK\t");
    setsock(cfd, 1);
    timespec_get(&sts, TIME_UTC);
    for (i=0; i<26; ++i)
    {
        send(cfd, buf, i+1, 0);
    }
    timespec_get(&ets, TIME_UTC);
    printf("% 20ld\n", (ets.tv_sec-sts.tv_sec)*1000000000 + (ets.tv_nsec-sts.tv_nsec));
    send(cfd, &buf[30], 20, 0);

    close(cfd);

    return 0;
}

3. 参考

  1. tcp(7): TCP protocol - Linux man page
  2. TCP_CORK: More than you ever wanted to know
  3. Nagle 算法与 TCP socket 选项 TCP_CORK
  4. TCP/IP options for high-performance data transmission
  5. TCP协议细节系列(1):Nagle和Cork算法 - 知乎