现在的位置: 首页 > 综合 > 正文

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

2013年04月11日 ⁄ 综合 ⁄ 共 4547字 ⁄ 字号 评论关闭

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

Java代码  收藏代码
  1. write(head);  
  2. write(body);  
  3. read(response);  

 

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

Java代码  收藏代码
  1. read(request);  
  2. process(request);  
  3. 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连接的处理不一样。

    服务器源码:

Java代码  收藏代码
  1. package net.fnil.nagle;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.InputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStream;  
  7. import java.net.InetSocketAddress;  
  8. import java.net.ServerSocket;  
  9. import java.net.Socket;  
  10.   
  11.   
  12. public class Server {  
  13.     public static void main(String[] args) throws Exception {  
  14.         ServerSocket serverSocket = new ServerSocket();  
  15.         serverSocket.bind(new InetSocketAddress(8000));  
  16.         System.out.println("Server startup at 8000");  
  17.         for (;;) {  
  18.             Socket socket = serverSocket.accept();  
  19.             InputStream in = socket.getInputStream();  
  20.             OutputStream out = socket.getOutputStream();  
  21.   
  22.             while (true) {  
  23.                 try {  
  24.                     BufferedReader reader = new BufferedReader(new InputStreamReader(in));  
  25.                     String line = reader.readLine();  
  26.                     out.write((line + "\r\n").getBytes());  
  27.                 }  
  28.                 catch (Exception e) {  
  29.                     break;  
  30.                 }  
  31.             }  
  32.         }  
  33.     }  
  34. }  

 

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

客户端代码:

Java代码  收藏代码
  1. package net.fnil.nagle;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.InputStream;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStream;  
  7. import java.net.InetSocketAddress;  
  8. import java.net.Socket;  
  9.   
  10.   
  11. public class Client {  
  12.   
  13.     public static void main(String[] args) throws Exception {  
  14.         // 是否分开写head和body  
  15.         boolean writeSplit = false;  
  16.         String host = "localhost";  
  17.         if (args.length >= 1) {  
  18.             host = args[0];  
  19.         }  
  20.         if (args.length >= 2) {  
  21.             writeSplit = Boolean.valueOf(args[1]);  
  22.         }  
  23.   
  24.         System.out.println("WriteSplit:" + writeSplit);  
  25.   
  26.         Socket socket = new Socket();  
  27.   
  28.         socket.connect(new InetSocketAddress(host, 8000));  
  29.         InputStream in = socket.getInputStream();  
  30.         OutputStream out = socket.getOutputStream();  
  31.   
  32.         BufferedReader reader = new BufferedReader(new InputStreamReader(in));  
  33.   
  34.         String head = "hello ";  
  35.         String body = "world\r\n";  
  36.         for (int i = 0; i < 10; i++) {  
  37.             long label = System.currentTimeMillis();  
  38.             if (writeSplit) {  
  39.                 out.write(head.getBytes());  
  40.                 out.write(body.getBytes());  
  41.             }  
  42.             else {  
  43.                 out.write((head + body).getBytes());  
  44.             }  
  45.             String line = reader.readLine();  
  46.             System.out.println("RTT:" + (System.currentTimeMillis() - label) + " ,receive:" + line);  
  47.         }  
  48.         in.close();  
  49.         out.close();  
  50.         socket.close();  
  51.     }  
  52.   
  53. }  

 

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

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

Java代码 

抱歉!评论已关闭.