【转载】网络编程中 Nagle 算法和 Delayed ACK 的测试

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

?


1

2

3

write(head);

write(body);

read(response);

接收端的处理代码类似这样: 

?


1

2

3

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 算法也可以做到不延迟。 

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

服务器源码: 

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

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 端口,并监听连接,连上来的时候就阻塞读取一行数据,并将数据返回给客户端。 

客户端代码: 

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

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: 

?


1

2

3

4

5

6

7

8

9

10

11

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 之前加上一行: 

?


1

2

3

Socket socket =  new  Socket();

socket.setTcpNoDelay( true );

socket.connect( new  InetSocketAddress(host, 8000));

      再跑下测试: 

?


1

2

3

4

5

6

7

8

9

10

11

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 这行删除): 

?


1

2

3

4

5

6

7

8

9

10

11

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 的理解和测试,有错误的地方请不吝赐教。 

时间: 2024-11-03 04:03:30

【转载】网络编程中 Nagle 算法和 Delayed ACK 的测试的相关文章

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

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

多线程在Visual C#网络编程中的应用

visual|编程|多线程|网络 网络应用程序的一般都会或多或少的使用到线程,甚至可以说,一个功能稍微强大的网络应用程序总会在其中开出或多或少的线程,如果应用程序中开出的线程数目大于二个,那么就可以把这个程序称之为多线程应用程序.那么为什么在网络应用程序总会和线程交缠在一起呢?这是因为网络应用程序在执行的时候,会遇到很多意想不到的问题,其中最常见的是网络阻塞和网络等待等. 程序在处理这些问题的时候往往需要花费很多的时间,如果不使用线程则程序在执行时的就会表现出如运行速度慢,执行时间长,容易出现错

正则表达式在网络编程中的运用(1)

编程|网络|正则 [前言:]在我们编写WEB程序时,经常会判断一个字符串的有效性,如:一个串是否是数字.是否是有效的Email地址等等.如果不使用正则表达式,那么判断的程序会很长,并且容易出错,如果使用正则表达式,这些判断就是一件很轻松的工作了.本文全面介绍正则表达式的慨念.格式.并以在PHP.ASP中的应用实例增加读者的感性认识.正则表达式的应用很广,需要大家在学习和实践中不断的总结. 正则表达式简介 简单的说,正则表达式是一种可以用于模式匹配和替换的强有力的工具.在网络编程中应用广泛,如PH

正则表达式在网络编程中的运用(3)

编程|网络|正则 应用实例 在对正则表达式有了较为全面的了解之后,就可以在Perl,PHP,以及ASP等程式中使用正则表达式了. 下面以PHP语言为例,使用验证用户在线输入的邮件地址以及网址的格式是否正确.PHP 提供了eregi()或ereg()资料处理函数实现字串比对剖析的模式匹配操作ereg()函数的使用格式如下: ereg (pattern, string) 其中,pattern代表正则表达式的模式:而string则是执行查找替换操作的目标对象,如Email地址值.本函式以 patter

服务器-网络编程中并未建立连接的端口却能往套接字中写,发送数据,困惑

问题描述 网络编程中并未建立连接的端口却能往套接字中写,发送数据,困惑 拜托各位了,碰上一个难以理解的困惑 情况如下 我在qt中编写了一个客户端(抛开qt,也可以理解为别的写的),在这个应用程序中有一个对象是专门用来建立连接并且处理和服务器的相关的通信. 服务器的话是在linux下c写的,简单的可以看做这种模式accept等待连接,连接成功后阻塞读写 while(1) { clientfd = accept(lfd, null, null): printf("new connectn"

服务器-关于java网络编程中获取输入流中数据的问题?

问题描述 关于java网络编程中获取输入流中数据的问题? //服务器端接收消息的类.定制端口号为8888 serviceSocket = new ServerSocket(10000); //获取socket.这个方法是阻塞式的 socket = serviceSocket.accept(); inputStream = socket.getInputStream(); byte buf[] = new byte[1024]; int len = 0; len =inputStream.read

java网络编程中IO数据输入输出阻塞

问题描述 java网络编程中IO数据输入输出阻塞 服务端代码如下: public class Server { public static void main(String[] args) throws IOException { ServerSocket ss = new ServerSocket(30000); Socket socket = ss.accept(); PrintStream ps = new PrintStream(socket.getOutputStream()); ps

请问各位,在C#网络编程中,如果要实现不在一个局域网内的两台电脑的通信,我该怎么实现?

问题描述 请问各位,在C#网络编程中,如果要实现不在一个局域网内的两台电脑的通信,我该怎么实现?初学者的问题,谢谢了 解决方案 解决方案二:没有人,自己顶起来解决方案三:不能沉啊,自己顶起来,拜托各位大侠了解决方案四:你可以了解下NAT穿透

java网络编程-Java网络编程中不可以使用缓存吗??

问题描述 Java网络编程中不可以使用缓存吗?? 我使用缓存总是什么都不显示,但是又不抛出异常,也没有什么错! 解决方案 java网络编程之缓存java网络编程之缓存(三)对使用Java Socket网络编程的详细讲解 解决方案二: http://blog.sina.com.cn/s/blog_616e189f0100s3px.html 解决方案三: 是不是其他问题啊?可以使用缓存的,可以贴出主要代码 解决方案四: import java.io.BufferedOutputStream; imp