网络编程中Nagle算法和Delayed ACK的测试

简介:
Nagle算法的立意是良好的,避免网络中充塞小封包,提高网络的利用率。但是当Nagle算法遇到 delayed ACK悲剧就发生了。Delayed ACK的本意也是为了提高TCP性能,跟应答数据捎带上ACK,同时避免 糊涂窗口综合症,也可以一个ack确认多个段来节省开销。
    悲剧发生在这种情况,假设一端发送数据并等待另一端应答,协议上分为头部和数据,发送的时候不幸地选择了write-write,然后再read,也就是先发送头部,再发送数据,最后等待应答。发送端的伪代码是这样
write(head);
write(body);
read(response);

接收端的处理代码类似这样:
read(request);
process(request);
write(response);

   这里假设head和body都比较小,当默认启用nagle算法,并且是第一次发送的时候,根据nagle算法,第一个段head可以立即发送,因为没有等待确认的段;接收端收到head,但是包不完整,继续等待body达到并延迟ACK;发送端继续写入body,这时候nagle算法起作用了,因为head还没有被ACK,所以body要延迟发送。这就造成了发送端和接收端都在等待对方发送数据的现象,发送端等待接收端ACK head以便继续发送body,而接收端在等待发送方发送body并延迟ACK,悲剧的无以言语。这种时候只有等待一端超时并发送数据才能继续往下走。

   正因为nagle算法和delayed ack的影响,再加上这种write-write-read的编程方式造成了很多网贴在讨论为什么自己写的网络程序性能那么差。然后很多人会在帖子里建议禁用Nagle算法吧,设置TCP_NODELAY为true即可禁用nagle算法。但是这真的是解决问题的唯一办法和最好办法吗?

   其实问题不是出在nagle算法身上的,问题是出在write-write-read这种应用编程上。禁用nagle算法可以暂时解决问题,但是禁用nagle算法也带来很大坏处,网络中充塞着小封包,网络的利用率上不去,在极端情况下,大量小封包导致网络拥塞甚至崩溃。因此,能不禁止还是不禁止的好,后面我们会说下什么情况下才需要禁用nagle算法。对大多数应用来说,一般都是连续的请求——应答模型,有请求同时有应答,那么请求包的ACK其实可以延迟到跟响应一起发送,在这种情况下,其实你只要避免write-write-read形式的调用就可以避免延迟现象,利用writev做聚集写或者将head和body一起写,然后再read,变成write-read-write-read的形式来调用,就无需禁用nagle算法也可以做到不延迟。

   writev是系统调用,在Java里是用到 GatheringByteChannel.write(ByteBuffer[] srcs, int offset, int length)方法来做聚集写。这里可能还有一点值的提下,很多同学看java nio框架几乎都不用这个writev调用,这是有原因的。主要是因为Java的write本身对ByteBuffer有做临时缓存,而writev没有做缓存,导致测试来看write反而比writev更高效,因此通常会更推荐用户将head和body放到同一个Buffer里来避免调用writev。

   下面我们将做个实际的代码测试来结束讨论。这个例子很简单,客户端发送一行数据到服务器,服务器简单地将这行数据返回。客户端发送的时候可以选择分两次发,还是一次发送。分两次发就是write-write-read,一次发就是write-read-write-read,可以看看两种形式下延迟的差异。 注意,在windows上测试下面的代码,客户端和服务器必须分在两台机器上,似乎winsock对loopback连接的处理不一样。

    服务器源码:
package  net.fnil.nagle;

import  java.io.BufferedReader;
import  java.io.InputStream;
import  java.io.InputStreamReader;
import  java.io.OutputStream;
import  java.net.InetSocketAddress;
import  java.net.ServerSocket;
import  java.net.Socket;


public   class  Server {
    
public   static   void  main(String[] args)  throws  Exception {
        ServerSocket serverSocket 
=   new  ServerSocket();
        serverSocket.bind(
new  InetSocketAddress( 8000 ));
        System.out.println(
" Server startup at 8000 " );
        
for  (;;) {
            Socket socket 
=  serverSocket.accept();
            InputStream in 
=  socket.getInputStream();
            OutputStream out 
=  socket.getOutputStream();

            
while  ( true ) {
                
try  {
                    BufferedReader reader 
=   new  BufferedReader( new  InputStreamReader(in));
                    String line 
=  reader.readLine();
                    out.write((line 
+   " \r\n " ).getBytes());
                }
                
catch  (Exception e) {
                    
break ;
                }
            }
        }
    }
}

服务端绑定到本地8000端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。

客户端代码:
package  net.fnil.nagle;

import  java.io.BufferedReader;
import  java.io.InputStream;
import  java.io.InputStreamReader;
import  java.io.OutputStream;
import  java.net.InetSocketAddress;
import  java.net.Socket;


public   class  Client {

    
public   static   void  main(String[] args)  throws  Exception {
        
//  是否分开写head和body
         boolean  writeSplit  =   false ;
        String host 
=   " localhost " ;
        
if  (args.length  >=   1 ) {
            host 
=  args[ 0 ];
        }
        
if  (args.length  >=   2 ) {
            writeSplit 
=  Boolean.valueOf(args[ 1 ]);
        }

        System.out.println(
" WriteSplit: "   +  writeSplit);

        Socket socket 
=   new  Socket();

        socket.connect(
new  InetSocketAddress(host,  8000 ));
        InputStream in 
=  socket.getInputStream();
        OutputStream out 
=  socket.getOutputStream();

        BufferedReader reader 
=   new  BufferedReader( new  InputStreamReader(in));

        String head 
=   " hello  " ;
        String body 
=   " world\r\n " ;
        
for  ( int  i  =   0 ; i  <   10 ; i ++ ) {
            
long  label  =  System.currentTimeMillis();
            
if  (writeSplit) {
                out.write(head.getBytes());
                out.write(body.getBytes());
            }
            
else  {
                out.write((head 
+  body).getBytes());
            }
            String line 
=  reader.readLine();
            System.out.println(
" RTT: "   +  (System.currentTimeMillis()  -  label)  +   "  ,receive: "   +  line);
        }
        in.close();
        out.close();
        socket.close();
    }

}


   客户端通过一个writeSplit变量来控制是否分开写head和body,如果为true,则先写head再写body,否则将head加上body一次写入。客户端的逻辑也很简单,连上服务器,发送一行,等待应答并打印RTT,循环10次最后关闭连接。

   首先,我们将writeSplit设置为true,也就是分两次写入一行,在我本机测试的结果,我的机器是ubuntu 11.10:
WriteSplit: true
RTT:
8  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
39  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world
RTT:
40  ,receive:hello world

    可以看到,每次请求到应答的时间间隔都在40ms,除了第一次。linux的delayed ack是40ms,而不是原来以为的200ms。第一次立即ACK,似乎跟linux的quickack mode有关,这里我不是特别清楚,有比较清楚的同学请指教。

     接下来,我们还是将writeSplit设置为true,但是客户端禁用nagle算法,也就是客户端代码在connect之前加上一行:
        Socket socket  =   new  Socket();
        socket.setTcpNoDelay(
true );
        socket.connect(
new  InetSocketAddress(host,  8000 ));

    再跑下测试:
WriteSplit: true
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
1  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world

   这时候就正常多了,大部分RTT时间都在1毫秒以下。果然禁用Nagle算法可以解决延迟问题。
   如果我们不禁用nagle算法,而将writeSplit设置为false,也就是将head和body一次写入,再次运行测试(记的将setTcpNoDelay这行删除):
WriteSplit: false
RTT:
7  ,receive:hello world
RTT:
1  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world
RTT:
0  ,receive:hello world


   结果跟禁用nagle算法的效果类似。既然这样,我们还有什么理由一定要禁用nagle算法呢?通过我在xmemcached的压测中的测试,启用nagle算法在小数据的存取上甚至有一定的效率优势,memcached协议本身就是个连续的请求应答的模型。上面的测试如果在windows上跑,会发现RTT最大会在200ms以上,可见winsock的delayed ack超时是200ms。

   最后一个问题,什么情况下才应该禁用nagle算法?当你的应用不是这种连续的请求——应答模型,而是需要实时地单向发送很多小数据的时候或者请求是有间隔的,则应该禁用nagle算法来提高响应性。一个最明显是例子是telnet应用,你总是希望敲入一行数据后能立即发送给服务器,然后马上看到应答,而不是说我要连续敲入很多命令或者等待200ms才能看到应答。

   上面是我对nagle算法和delayed ack的理解和测试,有错误的地方请不吝赐教。

文章转自庄周梦蝶  ,原文发布时间 2011-06-30

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
目录
相关文章
|
22天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于GRU网络的MQAM调制信号检测算法matlab仿真,对比LSTM
本研究基于MATLAB 2022a,使用GRU网络对QAM调制信号进行检测。QAM是一种高效调制技术,广泛应用于现代通信系统。传统方法在复杂环境下性能下降,而GRU通过门控机制有效提取时间序列特征,实现16QAM、32QAM、64QAM、128QAM的准确检测。仿真结果显示,GRU在低SNR下表现优异,且训练速度快,参数少。核心程序包括模型预测、误检率和漏检率计算,并绘制准确率图。
86 65
基于GRU网络的MQAM调制信号检测算法matlab仿真,对比LSTM
|
4天前
|
机器学习/深度学习 存储 算法
基于MobileNet深度学习网络的活体人脸识别检测算法matlab仿真
本内容主要介绍一种基于MobileNet深度学习网络的活体人脸识别检测技术及MQAM调制类型识别方法。完整程序运行效果无水印,需使用Matlab2022a版本。核心代码包含详细中文注释与操作视频。理论概述中提到,传统人脸识别易受非活体攻击影响,而MobileNet通过轻量化的深度可分离卷积结构,在保证准确性的同时提升检测效率。活体人脸与非活体在纹理和光照上存在显著差异,MobileNet可有效提取人脸高级特征,为无线通信领域提供先进的调制类型识别方案。
|
13天前
|
机器学习/深度学习 数据采集 算法
基于PSO粒子群优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真
本项目展示了基于PSO优化的CNN-LSTM-SAM网络时间序列预测算法。使用Matlab2022a开发,完整代码含中文注释及操作视频。算法结合卷积层提取局部特征、LSTM处理长期依赖、自注意力机制捕捉全局特征,通过粒子群优化提升预测精度。适用于金融市场、气象预报等领域,提供高效准确的预测结果。
|
4天前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
12天前
|
监控 算法 安全
公司电脑网络监控场景下 Python 广度优先搜索算法的深度剖析
在数字化办公时代,公司电脑网络监控至关重要。广度优先搜索(BFS)算法在构建网络拓扑、检测安全威胁和优化资源分配方面发挥重要作用。通过Python代码示例展示其应用流程,助力企业提升网络安全与效率。未来,更多创新算法将融入该领域,保障企业数字化发展。
37 10
|
27天前
|
机器学习/深度学习 数据采集 算法
基于WOA鲸鱼优化的CNN-GRU-SAM网络时间序列回归预测算法matlab仿真
本项目基于MATLAB 2022a实现时间序列预测,采用CNN-GRU-SAM网络结构,结合鲸鱼优化算法(WOA)优化网络参数。核心代码含操作视频,运行效果无水印。算法通过卷积层提取局部特征,GRU层处理长期依赖,自注意力机制捕捉全局特征,全连接层整合输出。数据预处理后,使用WOA迭代优化,最终输出最优预测结果。
|
15天前
|
缓存 监控 算法
基于 C# 网络套接字算法的局域网实时监控技术探究
在数字化办公与网络安全需求增长的背景下,局域网实时监控成为企业管理和安全防护的关键。本文介绍C#网络套接字算法在局域网实时监控中的应用,涵盖套接字创建、绑定监听、连接建立和数据传输等操作,并通过代码示例展示其实现方式。服务端和客户端通过套接字进行屏幕截图等数据的实时传输,保障网络稳定与信息安全。同时,文章探讨了算法的优缺点及优化方向,如异步编程、数据压缩与缓存、错误处理与重传机制,以提升系统性能。
35 2
|
21天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于机器学习的人脸识别算法matlab仿真,对比GRNN,PNN,DNN以及BP四种网络
本项目展示了人脸识别算法的运行效果(无水印),基于MATLAB2022A开发。核心程序包含详细中文注释及操作视频。理论部分介绍了广义回归神经网络(GRNN)、概率神经网络(PNN)、深度神经网络(DNN)和反向传播(BP)神经网络在人脸识别中的应用,涵盖各算法的结构特点与性能比较。
|
19天前
|
机器学习/深度学习 数据采集 算法
基于GA遗传优化的CNN-LSTM-SAM网络时间序列回归预测算法matlab仿真
本项目使用MATLAB 2022a实现时间序列预测算法,完整程序无水印。核心代码包含详细中文注释和操作视频。算法基于CNN-LSTM-SAM网络,融合卷积层、LSTM层与自注意力机制,适用于金融市场、气象预报等领域。通过数据归一化、种群初始化、适应度计算及参数优化等步骤,有效处理非线性时间序列,输出精准预测结果。
|
3天前
|
机器学习/深度学习 数据采集 算法
基于yolov2和googlenet网络的疲劳驾驶检测算法matlab仿真
本内容展示了基于深度学习的疲劳驾驶检测算法,包括算法运行效果预览(无水印)、Matlab 2022a 软件版本说明、部分核心程序(完整版含中文注释与操作视频)。理论部分详细阐述了疲劳检测原理,通过对比疲劳与正常状态下的特征差异,结合深度学习模型提取驾驶员面部特征变化。具体流程包括数据收集、预处理、模型训练与评估,使用数学公式描述损失函数和推理过程。课题基于 YOLOv2 和 GoogleNet,先用 YOLOv2 定位驾驶员面部区域,再由 GoogleNet 分析特征判断疲劳状态,提供高准确率与鲁棒性的检测方法。

热门文章

最新文章