write(head); write(body); read(response);
read(request); process(request); write(response);
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 ; } } } } }
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 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
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
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 算法呢?通过我在 的压测中的测试,启用 Nagle 算法在小数据的存取上甚至有一定的效率优势,memcached 协议本身就是个连续的请求应答的模型。上面的测试如果在 windows 上跑,会发现 RTT 最大会在 200ms 以上,可见 winsock 的delayed ack 超时是 200ms 。
最后一个问题,什么情况下才应该禁用 Nagle 算法?当你的应用不是这种连续的请求应答模型,而是需要实时地单向发送很多小数据的时候或者请求是有间隔的,则应该禁用 Nagle 算法来提高响应性。一个最明显是例子是 telnet 应用,你总是希望敲入一行数据后能立即发送给服务器,然后马上看到应答,而不是说我要连续敲入很多命令或者等待 200ms才能看到应答。 上面是我对 Nagle 算法和 delayed ACK 的理解和测试,有错误的地方请不吝赐教。