《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 jianshu.com/p/ccafdeda0b95 「王路飞的故事」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

Nagle算法描述

Socket编程中,TCP_NODELAY选项是用来控制是否开启Nagle算法,该算法是为了提高较慢的广域网传输效率,减小小分组的报文个数,完整描述:

该算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到来之前,不能发送其他小分组。

这里的小分组指的是报文长度小于MSS(Max Segment Size)长度的分组(MSS是在TCP握手的时候在报文选项里面进行通告的大小,主要是用来限制另一端发送数据的长度,防止IP数据包被分段,提高效率,一般是链路层的传输最大传输单元大小减去IP首部与TCP首部大小)。

如果小分组的确认ACK一直没有回来,那么就可能会触发TCP超时重传的定时器。

下面是一个简单的示意图,开启了Nagle算法与没有开启:

nagle

抓包分析

默认开启Nagle算法

由于局域网内延迟低,不容易看到开启Nagle算法的效果,所以专门整个腾讯云的服务器测试,延迟在40毫秒左右。

ping

Java代码与Unix C的Socket接口类似,这里使用Java代码作为示例简单一点。默认情况下Nagle算法是开启的,即socket.getTcpNoDelay()返回的数值为false,我们先分析这种场景。

Receiver的代码:

try (ServerSocket serverSocket = new ServerSocket()) {
serverSocket.bind(new InetSocketAddress(10086));//wildcard ip
Socket socket = serverSocket.accept();
System.out.println("Accept New Socket");
System.out.println("Tcp No Delay : " + socket.getTcpNoDelay());
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
int result;
while((result = is.read()) != -1) {
System.out.println((char)result);
}
TimeUnit.MINUTES.sleep(1);
}

Sender的代码:

try(Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("212.64.20.XX", 10086));
System.out.println("Tcp No Delay : " + socket.getTcpNoDelay());
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
for (byte c : "TCP_NO_DELAY".getBytes()) {
TimeUnit.MILLISECONDS.sleep(10);
os.write(c);
os.flush();
}
TimeUnit.MINUTES.sleep(1);
} catch (IOException e) {
e.printStackTrace();
}

与服务器的延时在40毫秒左右,所以Sender这里每隔10毫秒就发送一次就可以演示出累计的小分组在收到ACK后才发送。注意如果是TCP发送的数据延迟还包含链路来回的延迟与Receiver捎带确认的延迟。

这里抓包工具使用的是tcpdump,导出pcap文件后再使用wireshark观察发送与接收数据的过程。

$ sudo tcpdump -v port 10086 -w TCP_DELAY.pcap

cap

  • 首先第一行到第三行是TCP三次握手的报文,可以看到双方都各自通告的MSS大小,发送端的报文小于这个大小就可以理解为小分组
  • 第四行是Sender向Receiver发送的第一个字符’T’,对应的Len=1
  • 第五行是Receiver回来对第四行发送消息的确认ACK
  • 第六行,前面使用ping测试的延迟在40毫秒左右,而我们每10毫秒就会一个字符写到OS维护的发送缓冲区,所以确认ACK回来后,就已经累计了4个字符”CP_N”,发送的数据就是这4个字符
  • 之后的流程和上面的类似,可能会出现发送不是4个字符的情况,出现的原因就是延迟可能小于或者大于40毫秒

下面是使用wireshark导出的时序图帮助进一步帮助理解这个流程。

flow

关闭Nagle算法

只需要在发送数据之前对Socket调用一个简单的方法就可以关闭Nagle算法:

socket.setTcpNoDelay(true);

直接抓包,看下报文:

cap

可以看到,在Sender每10毫秒发送一个字符,不需要等到Receiver发送确认ACK,就继续发送,没有将数据放到OS维护的缓冲区。

下面是使用wireshark导出的时序图:

flow

总结

这个选项应该根据适合的场景进行判断关闭与否,例如实时性要求比较高的场景,类似用户鼠标操作,键盘输入,触摸屏事件输入,状态更新等这种连续的小分组数据,需要在对端立刻呈现,让用户尽可能感受不到延迟。但是如果网络延迟比较高,采用这种方式,那么会导致网路利用率下降。

一般类似HTTP协议请求响应的模型的场景不太需要考虑禁用这个算法,因为在一条TCP连接上发送小报文,不管多小都代表了服务端任务执行的指示,完成了这个请求之后才能继续执行下一个请求,即使Sender端提前发送过去也没有作用,所以开启Nagle算法是能够优化网络传输的,并且在Receiver端有捎带延迟确认,省掉单独的ACK确认进一步优化小分组传输。

另外HTTP2与HTTP协议不同,HTTP2是在一条TCP连接上进行所有HTTP请求,并且请求头部是压缩的就进一步加大了请求小分组的可能性,多个小分组HTTP请求并且分组大小的和小于MSS就会导致有延迟的现象,所以HTTP2的实现TCP_NODELAY选项是默认开启的。关于这点可以参考HTTP2对TCP_NODELAY的描述

文章目录
  1. 1. Nagle算法描述
  2. 2. 抓包分析
    1. 2.1. 默认开启Nagle算法
    2. 2.2. 关闭Nagle算法
  3. 3. 总结