Nagle法的立意是良好的,避免|络中充塞小包Q提高网l的利用率。但是当Nagle法遇到delayed ACK(zhn)剧发生了。Delayed ACK的本意也是ؓ了提高TCP性能Q跟应答数据捎带上ACKQ同旉?a >p涂H口l合?/a>Q也可以一个ack认多个D|节省开销?br /> (zhn)剧发生在这U情况,假设一端发送数据ƈ{待另一端应{,协议上分为头部和数据Q发送的时候不q地选择了write-writeQ然后再readQ也是先发送头部,再发送数据,最后等待应{。发送端的伪代码是这?br />write(head);
write(body);
read(response);
接收端的处理代码cMq样Q?br />read(request);
process(request);
write(response);
q里假设head和body都比较小Q当默认启用nagle法Qƈ且是W一ơ发送的时候,Ҏ(gu)nagle法Q第一个段head可以立即发送,因ؓ没有{待认的段Q接收端收到headQ但是包不完_l箋{待body辑ֈqgqACKQ发送端l箋写入bodyQ这时候nagle法起作用了Q因为headq没有被ACKQ所以body要gq发送。这造成了发送端和接收端都在{待Ҏ(gu)发送数据的现象Q发送端{待接收端ACK head以便l箋发送bodyQ而接收端在等待发送方发送bodyqgqACKQ?zhn)剧的无以a语。这U时候只有等待一端超时ƈ发送数据才能l往下走?br />
正因为nagle法和delayed ack的媄响,再加上这Uwrite-write-read的编E方式造成了很多网贴在讨论Z么自己写的网l程序性能那么差。然后很多h会在帖子里徏议禁用Nagle法吧,讄TCP_NODELAY为true卛_用nagle法。但是这真的是解决问题的唯一办法和最好办法吗Q?br />
其实问题不是出在nagle法w上的,问题是出在write-write-readq种应用~程上。禁用nagle法可以暂时解决问题Q但是禁用nagle法也带来很大坏处,|络中充塞着封包,|络的利用率上不去,在极端情况下Q大量小包D|络拥塞甚至崩溃。因此,能不止q是不禁止的好,后面我们会说下什么情况下才需要禁用nagle法。对大多数应用来_一般都是连l的h——应答模型Q有h同时有应{,那么h包的ACK其实可以延迟到跟响应一起发送,在这U情况下Q其实你只要避免write-write-read形式的调用就可以避免延迟现象Q利用writev做聚集写或者将head和body一起写Q然后再readQ变成write-read-write-read的Ş式来调用Q就无需用nagle法也可以做C延迟?br />
writev是系l调用,在Java里是用到GatheringByteChannel.write(ByteBuffer[] srcs, int offset, int length)Ҏ(gu)来做聚集写。这里可能还有一点值的提下Q很多同学看java nio框架几乎都不用这个writev调用Q这是有原因的。主要是因ؓJava的write本n对ByteBuffer有做临时~存Q而writev没有做缓存,D试来看write反而比writev更高效,因此通常会更推荐用户head和body攑ֈ同一个Buffer里来避免调用writev?br />
下面我们做个实际的代码试来结束讨论。这个例子很单,客户端发送一行数据到服务器,服务器简单地这行数据返回。客L发送的时候可以选择分两ơ发Q还是一ơ发送。分两次发就是write-write-readQ一ơ发是write-read-write-readQ可以看看两UŞ式下延迟的差异?strong>注意Q在windows上测试下面的代码Q客L和服务器必须分在两台机器上,gwinsock对loopbackq接的处理不一栗?/strong>
服务器源码:
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端口Qƈ监听q接Q连上来的时候就dd一行数据,q将数据q回l客L?br />
客户端代码:
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和bodyQ如果ؓtrueQ则先写head再写bodyQ否则将head加上body一ơ写入。客L的逻辑也很单,q上服务器,发送一行,{待应答q打印RTTQ@?0ơ最后关闭连接?br />
首先Q我们将writeSplit讄为trueQ也是分两ơ写入一行,在我本机试的结果,我的机器是ubuntu 11.10Q?br />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
可以看到Q每ơ请求到应答的时间间隔都?0msQ除了第一ơ。linux的delayed ack?0msQ而不是原来以为的200ms。第一ơ立即ACKQ似乎跟linux的quickack mode有关Q这里我不是特别清楚Q有比较清楚的同学请指教?br />
接下来,我们q是writeSplit讄为trueQ但是客L用nagle法Q也是客户端代码在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
q时候就正常多了Q大部分RTT旉都在1毫秒以下。果然禁用Nagle法可以解决延迟问题?br /> 如果我们不禁用nagle法Q而将writeSplit讄为falseQ也是head和body一ơ写入,再次q行试Q记的将setTcpNoDelayq行删除Q:
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
l果跟禁用nagle法的效果类伹{既然这P我们q有什么理׃定要用nagle法呢?通过我在xmemcached的压中的测试,启用nagle法在小数据的存取上甚至有一定的效率优势Qmemcached协议本n是个连l的h应答的模型。上面的试如果在windows上跑Q会发现RTT最大会?00ms以上Q可见winsock的delayed ack时?00ms?br />
最后一个问题,什么情况下才应该禁用nagle法Q当你的应用不是q种q箋的请?#8212;—应答模型Q而是需要实时地单向发送很多小数据的时候或者请求是有间隔的Q则应该用nagle法来提高响应性。一个最明显是例子是telnet应用Q你L希望敲入一行数据后能立卛_送给服务器,然后马上看到应答Q而不是说我要q箋敲入很多命o或者等?00ms才能看到应答?br />
上面是我对nagle法和delayed ack的理解和试Q有错误的地方请不吝赐教?br />
转蝲h明出处:http://www.aygfsteel.com/killme2008/archive/2011/06/30/353441.html

]]>