TCP_NODELAY是一个套接字选项,用于控制TCP套接字的延迟行为。当TCP_NODELAY选项被启用时,即设置为true,就会禁用Nagle算法,从而实现TCP套接字的无延迟传输。这意味着每次发送数据时都会立即发送,不会等待缓冲区的填充或等待确认。

TCP_NODELAY选项的演示

Socket服务端代码如下:

package com.morris.socket;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Socket 服务端,演示TCP_NODELAY
 *
 * @see java.net.SocketOptions
 */
public class TcpNoDelayServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8090);
        while (true) {
            Socket client = serverSocket.accept();
            System.out.println("accept: " + client.getRemoteSocketAddress());
            new Thread(() -> {
                try {
                    InputStream inputStream = client.getInputStream();

                    byte[] buffer = new byte[1024];
                    while (true) {
                        int len = inputStream.read(buffer);
                        if(-1 == len) {
                            System.out.println("close: " + client.getRemoteSocketAddress());
                            inputStream.close();
                            client.close();
                            break;
                        } else {
                            System.out.print("receive: " + new String(buffer, 0, len));
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Socket客户端代码如下:

package com.morris.socket;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * Socket 客户端,演示TCP_NODELAY
 *
 * @see java.net.SocketOptions
 */
public class TcpNoDelayClientDemo {
    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("192.168.1.11", 8099);
        socket.setTcpNoDelay(true);
        OutputStream outputStream = socket.getOutputStream();
        for (int i = 0; i < 10; i++) {
            outputStream.write((i+"\n").getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        }
        outputStream.close();
        socket.close();
    }
}

默认情况下,也就是TcpNoDelay为false,开启Nagle算法,运行结果如下:

accept: /192.168.1.10:4760
receive: 0
receive: 1
2
3
4
5
6
7
8
9

客户端发了10个报文,而服务端只收到2个,客户端在发送报文的过程中进行了合并。

下面是tcpdump抓到的服务端的报文,从中也可以清晰的看到,客户端往服务端发送了2个数据报文(Flags为PSH):

$ tcpdump tcp port 8099
19:37:53.914138 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [S], seq 3193331081, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:37:53.914182 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [S.], seq 2693275116, ack 3193331082, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:37:53.925761 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:37:53.927131 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:37:53.927172 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [.], ack 3, win 229, length 0
19:37:53.927977 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [FP.], seq 3:21, ack 1, win 516, length 18
19:37:53.928363 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [F.], seq 1, ack 22, win 229, length 0
19:37:53.940733 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

TcpNoDelay改为false后,也就是禁用Nagle算法,运行结果如下:

accept: /192.168.1.10:3307
receive: 0
receive: 1
receive: 2
receive: 3
4
receive: 5
6
receive: 7
receive: 8
9

客户端发了10个报文,而服务端收到了7个,客户端在发送报文的过程中允许小的数据包立即发送,尽量不合并。

下面是tcpdump抓到的服务端的报文,从中也可以清晰的看到,客户端往服务端发送了10个数据报文(Flags为PSH):

> tcpdump tcp port 8099
19:39:07.953002 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [S], seq 2087425409, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:39:07.953071 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [S.], seq 1383068300, ack 2087425410, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:39:07.961935 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:39:07.963806 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:39:07.963808 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 3:5, ack 1, win 516, length 2
19:39:07.963858 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 3, win 229, length 0
19:39:07.963867 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 5, win 229, length 0
19:39:07.963903 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 5:7, ack 1, win 516, length 2
19:39:07.963925 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 7, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 7:9, ack 1, win 516, length 2
19:39:07.963933 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 9, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 9:11, ack 1, win 516, length 2
19:39:07.963939 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 11, win 229, length 0
19:39:07.964809 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 11:13, ack 1, win 516, length 2
19:39:07.964810 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 13:15, ack 1, win 516, length 2
19:39:07.964811 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 15:17, ack 1, win 516, length 2
19:39:07.964812 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 17:19, ack 1, win 516, length 2
19:39:07.964813 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 19:21, ack 1, win 516, length 2
19:39:07.964861 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 13, win 229, length 0
19:39:07.964868 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 15, win 229, length 0
19:39:07.964871 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 17, win 229, length 0
19:39:07.964874 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 19, win 229, length 0
19:39:07.964878 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 21, win 229, length 0
19:39:07.964923 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [F.], seq 21, ack 1, win 516, length 0
19:39:07.965196 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [F.], seq 1, ack 22, win 229, length 0
19:39:07.974934 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

Nagle算法

TCP协议是网络编程中最重要的协议之一,TCP协议将上层的数据附上TCP报头等信息,封装成一个个报文段(segment),然后交由下层网络层去处理。TCP协议定义了TCP报文段的结构,如下图所示:

【计算机网络】Socket的TCP_NODELAY选项与Nagle算法_计算机网络

可以看出,TCP每个报文段的首部大小至少是20字节的数据,因此若用户数据为1字节,再加上网络层IP包头20字节,则整个IP数据包的大小为41字节,那么整个IP数据包的负荷率为1/41。这显然是不划算的,会降低网络的传输效率,当网络都充斥着这种IP数据包的时候,可想而知整个网络几乎都在传输一些无用的包头信息,这种问题被称为小包问题。特别是在Telnet协议中,当用户远程登录到一个主机,他的每一次键盘敲击实际上都会产生一个携带用户数据量小的数据包,这是典型的小包问题。

为了解决这种问题,出现了Nagle’s Algorithms,这个算法是John Nagle为解决实际过程中出现的小包问题而发明的。它的思想很朴素,就是将多个即将发送的小段的用户数据,缓存并合并成一个大段数据时,一次性一并发送出去。特别的是,只要当发送者还没有收到前一次发送TCP报文段的的ACK(即连接中还存在未回执ACK的TCP报文段)时,发送方就应该一直缓存数据直到数据达到可以发送的大小,然后再统一合并到一起发送出去,如果收到上一次发送的TCP报文段的ACK则立马将缓存的数据发送出去。

与之相呼应的还有一个网络优化的机制叫做TCP延迟确认,这个是针对接收方来讲的机制,由于ACK包属于有效数据比较少的小包,因此延迟确认机制就会导致接收方将多个收到数据包的ACK打包成一个回复包返回给发送方。这样就可以避免导致只包含ACK的TCP报文段过多导致网络额外的开销(前面提到的小包问题)。延迟确认机制有一个超时机制,就是当收到每一个TCP报文段后,如果该TCP报文段的ACK超过一定时间还未发送就启动超时机制,立刻将该ACK发送出去。因此延迟确认机制会可能会带来500ms的ACK延迟确认时间。

延迟确认机制和Nigle算法几乎是在同一时期提出来的,但是是由不同的组提出的。这两种机制在某种程度上的确对网络传输进行了优化,在通常的协议栈实现中,这两种机制是默认开启的。

但是,这两种机制结合起来的时候会产生一些负面的影响,可能会导致应用程序的性能下降。

TCP_NODELAY使用注意事项

Nagle算法是应用在发送端的,简而言之就是,对发送端而言:

  • 当第一次发送数据时不用等待,就算是1byte的小包也立即发送
  • 后面发送数据时需要累积数据包直到满足下面的条件之一才会继续发送数据:
  • 数据包达到最大段大小MSS
  • 接收端收到之前数据包的确认ACK

MTU是指在网络通信中能够传输的最大数据包大小。MSS一般会小于等于MTU,因为数据包中还包含有TCP/IP协议头部的额外开销。一般情况下,IPv4网络中的MTU大小为1500字节,而IPv6网络中的MTU大小为1280字节。

网卡的信息上有mtu的大小:

$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.24.104.61  netmask 255.255.240.0  broadcast 172.24.111.255
        inet6 fe80::215:5dff:fe6f:5634  prefixlen 64  scopeid 0x20<link>
        ether 00:15:5d:6f:56:34  txqueuelen 1000  (Ethernet)
        RX packets 213794  bytes 293979318 (293.9 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 106646  bytes 8514354 (8.5 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

禁用Nagle算法可以降低延迟,适用于实时性要求高的应用,如实时音视频传输或实时游戏。

禁用Nagle算法可能会导致更频繁的网络传输,增加网络开销。因此,在传输大量小数据包的情况下,禁用Nagle算法可能会降低网络效率。

TCP_NODELAY选项通常在创建套接字后设置,对于已经建立的连接,可能需要重新创建套接字并设置选项。

需要根据具体的需求和场景来决定是否使用TCP_NODELAY选项,权衡延迟和网络效率之间的关系。