TCP連接中啟用和禁用TCP_NODELAY有什麼影響?


30年前,陽澄湖的蟹農老王,每抓一隻大閘蟹,就僱傭機器人開著卡車送到上海,然後再帶著賣蟹的錢回來,老王想,這樣好處是回款快(延遲小)。

但是,30年前的鄉間小路,很快被一輛輛的卡車所堵塞,結果三天三夜也沒有到達上海,老王的如意算盤泡湯了。

痛定思痛,老王覺得,沒有必要一個螃蟹一輛車,可以將上午的所有螃蟹裝在一輛卡車,上午發車;下午抓的螃蟹裝在另一輛卡車裡,下午發車。這樣即使一天抓1000個螃蟹,也只需要兩卡車,而不需要1000輛卡車

這樣,鄉間的小路也不會造成擁堵,上午發貨,不一會兒,賣蟹的錢會被卡車運回來。

以上就是Nagle演算法的通俗解釋。

70-80年代,一些遠程互動式軟體,如Rlogin,客戶端將用戶輸入的每一個字元獨立傳輸到伺服器端,伺服器端再將這一個字元發回來,rlogin再顯示到用戶屏幕上。這樣一個位元組的字元卻需要20位元組的IP+ 20位元組的TCP頭,這樣的傳輸效率非常低下,只有 1/41 = 2.43%

更要命的是,那時網路帶寬特別窄,這樣的傳輸模式很容易將窄窄的帶寬擠滿而丟包,再重傳、再丟包的惡性循環。

於是Nagle發明了一個演算法,針對互動式應用,將用戶敲入的字元緩存一下,聚集了幾個字元放在一起發送,這樣傳輸效率則高得多,唯一的不足是,可能會有一些延遲。

為了避免延遲過大,等待用戶時間由定時器控制,比如100-200毫秒,定時器到了,立馬將緩衝區的數據發送出去。

但記住一點,Nagle演算法是時代的產物,因為當時網路帶寬有限。而當前的區域網、廣域網的帶寬則寬裕得多,所以目前的TCP/IP協議棧默認將Nagle演算法關閉,即通過SO_NODELAY = 1

這就好比,現在老王用一輛卡車運一隻螃蟹,走沿海高速也不會堵,儘管這聽起來很荒誕…


參考tcp(7): TCP protocol

TCP_NODELAY
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a sufficient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.

TCP/IP協議中針對TCP默認開啟了Nagle演算法。Nagle演算法通過減少需要傳輸的數據包,來優化網路。關於Nagle演算法,@郭無心 同學的答案已經說了不少了。在內核實現中,數據包的發送和接受會先做緩存,分別對應於寫緩存和讀緩存。
那麼針對題主的問題,我們來分析一下。
啟動TCP_NODELAY,就意味著禁用了Nagle演算法,允許小包的發送。對於延時敏感型,同時數據傳輸量比較小的應用,開啟TCP_NODELAY選項無疑是一個正確的選擇。比如,對於SSH會話,用戶在遠程敲擊鍵盤發出指令的速度相對於網路帶寬能力來說,絕對不是在一個量級上的,所以數據傳輸非常少;而又要求用戶的輸入能夠及時獲得返回,有較低的延時。如果開啟了Nagle演算法,就很可能出現頻繁的延時,導致用戶體驗極差。當然,你也可以選擇在應用層進行buffer,比如使用java中的buffered stream,儘可能地將大包寫入到內核的寫緩存進行發送;vectored I/O(writev介面)也是個不錯的選擇。
對於關閉TCP_NODELAY,則是應用了Nagle演算法。數據只有在寫緩存中累積到一定量之後,才會被發送出去,這樣明顯提高了網路利用率(實際傳輸數據payload與協議頭的比例大大提高)。但是這由不可避免地增加了延時;與TCP delayed ack這個特性結合,這個問題會更加顯著,延時基本在40ms左右。當然這個問題只有在連續進行兩次寫操作的時候,才會暴露出來。
我們看一下摘自Wikipedia的Nagle演算法的偽碼實現:

if there is new data to send
if the window size &>= MSS and available data is &>= MSS
send complete MSS segment now
else
if there is unconfirmed data still in the pipe
enqueue data in the buffer until an acknowledge is received
else
send data immediately
end if
end if
end if

通過這段偽碼,很容易發現連續兩次寫操作出現問題的原因。而對於讀-寫-讀-寫這種模式下的操作,關閉TCP_NODELAY並不會有太大問題。

The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

連續進行多次對小數據包的寫操作,然後進行讀操作,本身就不是一個好的網路編程模式;在應用層就應該進行優化。
對於既要求低延時,又有大量小數據傳輸,還同時想提高網路利用率的應用,大概只能用UDP自己在應用層來實現可靠性保證了。好像企鵝家就是這麼乾的。

參考資料:
1. 神秘的40毫秒延遲與 TCP_NODELAY
2. The Caveats of TCP_NODELAY


有兩篇博客對TCP_NODELAY有清晰的描述
網路編程之nagle演算法和TCP_NODELAY
寫socket的「靈異事件」
blog當中的例子大概是這樣

package mina.client;

import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

import mina.common.BaseConfig;

public class Client {

public static void main(String[] args) throws Exception {

Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", BaseConfig.PORT));

for (int id = 0; id &< 10; id++) { String idstr = Integer.toString(id); sendMessageWithDelimiter(socket, idstr); } Thread.sleep(1000000); } public static void sendMessageWithDelimiter(Socket sock, String message) throws Exception { OutputStream output = sock.getOutputStream(); output.write(message.getBytes()); output.write(new byte[]{" " , " "}); } }

package mina.common;

public class BaseConfig {
// 伺服器埠
public static final int PORT = 9123;
}

package mina.server;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import mina.common.BaseConfig;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class MinaTimeServer {
private static final int PORT = BaseConfig.PORT;

public static void main(String[] args) throws IOException {

IoAcceptor acceptor = new NioSocketAcceptor();
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
acceptor.getFilterChain().addLast(
"codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset
.forName("UTF-8"))));

// acceptor.getFilterChain().addLast(
// "codec",
// new ProtocolCodecFilter(new PrefixedStringCodecFactory(Charset
// .forName("UTF-8"))));
acceptor.setHandler(new TimeServerHandler());

/*
* Sets the size of the read buffer that I/O processor allocates per
* each read. It"s unusual to adjust this property because it"s often
* adjusted automatically by the I/O processor 一次最多讀取這麼多位元組,不足也返回
*/
acceptor.getSessionConfig().setReadBufferSize(2048);

// 設置空轉時間
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);

acceptor.bind(new InetSocketAddress(PORT));

}
}

package mina.server;

import java.util.Date;

import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;

public class TimeServerHandler implements IoHandler {

@Override
public void exceptionCaught(IoSession arg0, Throwable arg1)
throws Exception {
arg1.printStackTrace();

}

@Override
public void messageReceived(IoSession session, Object message)
throws Exception {

String str = message.toString();

System.out.println("接受到的消息:" + str);

if (str.trim().equalsIgnoreCase("quit")) {
session.close(true);
return;
}
Date date = new Date();
session.write(date.toString());
System.out.println("Message written...");
}

@Override
public void messageSent(IoSession arg0, Object arg1) throws Exception {
// TODO Auto-generated method stub
System.out.println("發送信息:" + arg1.toString());
}

@Override
public void sessionClosed(IoSession session) throws Exception {
// TODO Auto-generated method stub
System.out.println("IP:" + session.getRemoteAddress().toString()
+ "斷開連接");
}

@Override
public void sessionCreated(IoSession session) throws Exception {
// TODO Auto-generated method stub
System.out.println("IP:" + session.getRemoteAddress().toString());
}

@Override
public void sessionIdle(IoSession session, IdleStatus status)
throws Exception {
// TODO Auto-generated method stub
System.out.println("IDLE " + session.getIdleCount(status));
}

@Override
public void sessionOpened(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
}
}

log4j.appender.MINA=org.apache.log4j.ConsoleAppender
log4j.appender.MINA.layout=org.apache.log4j.PatternLayout
log4j.appender.MINA.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %m%n

#to console#
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %m%n
#to file#
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=./log/minademos.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %l %m%n
#error/warn/info/debug#
log4j.rootLogger=debug, stdout,MINA,file

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/D:/slf4j-log4j12-1.6.2.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/E:/jar/slf4j-nop-1.6.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
2016-04-08 13:48:49,808 CREATED
2016-04-08 13:48:49,808 CREATED
IP:/127.0.0.1:62056
2016-04-08 13:48:49,810 OPENED
2016-04-08 13:48:49,810 OPENED
2016-04-08 13:48:49,812 RECEIVED: HeapBuffer[pos=0 lim=30 cap=2048: 30 0D 0A 31 0D 0A 32 0D 0A 33 0D 0A 34 0D 0A 35...]
2016-04-08 13:48:49,812 RECEIVED: HeapBuffer[pos=0 lim=30 cap=2048: 30 0D 0A 31 0D 0A 32 0D 0A 33 0D 0A 34 0D 0A 35...]
2016-04-08 13:48:49,813 Processing a MESSAGE_RECEIVED for session 1
2016-04-08 13:48:49,813 Processing a MESSAGE_RECEIVED for session 1
接受到的消息:0
Message written...
接受到的消息:1
Message written...
接受到的消息:2
Message written...
接受到的消息:3
Message written...
接受到的消息:4
Message written...
接受到的消息:5
Message written...
接受到的消息:6
Message written...
接受到的消息:7
Message written...
接受到的消息:8
Message written...
接受到的消息:9
Message written...
2016-04-08 13:48:49,821 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,821 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,822 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,822 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,822 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,822 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,823 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,823 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,823 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,823 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,824 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,824 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,824 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,824 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,825 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,825 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,825 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,825 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016
2016-04-08 13:48:49,826 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
2016-04-08 13:48:49,826 SENT: HeapBuffer[pos=0 lim=0 cap=0: empty]
發送信息:Fri Apr 08 13:48:49 CST 2016

確實我在windows上實驗的結果是沒有40ms的延遲的

------------------------------------------------------
以下是第一篇博文的摘錄

之前寫過一篇blog ,描述了用mina的時候寫socket發現的一個詭異現象,當時將多個小數據寫操作合併成一個寫操作,問題就沒了。Chenshuo同學還建議我設置TCP_NODELAY,只是後來因為事情忙,也就沒有再深究下去。


現在大概明白,是由於nagle演算法在搗亂。
TCP/IP協議中,無論發送多少數據,總是要在數據前面加上協議頭,同時,對方接收到數據,也需要發送ACK表示確認。為了儘可能的利用網路帶寬,TCP總是希望儘可能的發送足夠大的數據。(一個連接會設置MSS參數,因此,TCP/IP希望每次都能夠以MSS尺寸的數據塊來發送數據)。

Nagle演算法就是為了儘可能發送大塊數據,避免網路中充斥著許多小數據塊。

Nagle演算法的基本定義是任意時刻,最多只能有一個未被確認的小段。 所謂「小段」,指的是小於MSS尺寸的數據塊,所謂「未被確認」,是指一個數據塊發送出去後,沒有收到對方發送的ACK確認該數據已收到。

舉個例子,比如之前的blog中的實驗,一開始client端調用socket的write操作將一個int型數據(稱為A塊)寫入到網路中,由於此時連接是空閑的(也就是說還沒有未被確認的小段),因此這個int型數據會被馬上發送到server端,接著,client端又調用write操作寫入『/r/n』(簡稱B塊),這個時候,A塊的ACK沒有返回,所以可以認為已經存在了一個未被確認的小段,所以B塊沒有立即被發送,一直等待A塊的ACK收到(大概40ms之後),B塊才被發送。整個過程如圖所示:

這裡還隱藏了一個問題,就是A塊數據的ACK為什麼40ms之後才收到?這是因為TCP/IP中不僅僅有nagle演算法,還有一個ACK延遲機制 。當Server端收到數據之後,它並不會馬上向client端發送ACK,而是會將ACK的發送延遲一段時間(假設為t),它希望在t時間內server端會向client端發送應答數據,這樣ACK就能夠和應答數據一起發送,就像是應答數據捎帶著ACK過去。在我之前的時間中,t大概就是40ms。這就解釋了為什麼"/r/n"(B塊)總是在A塊之後40ms才發出。

如果你覺著nagle演算法太搗亂了,那麼可以通過設置TCP_NODELAY將其禁用 。當然,更合理的方案還是應該使用一次大數據的寫操作,而不是多次小數據的寫操作。

下面也是介紹

網路編程中的Socket詳解---Delayed Ack(Ack確認延遲) Nagle Algorithm(納格演算法)


在TCP/IP詳解的第19章有明確描述


首先你要知道什麼是nagle演算法。
nagle演算法:如果有未確認的小包,那麼下一個包會緩存下來(在緩存過程中這個包的大小會累積變大),緩存的包被發送直到上一個包得到確認或者緩存的包的大小達到最大值。可見nagle是阻止網路中產生太多小包,它只允許網路中存在一個小包

nagle演算法默認是啟用的。

禁用nagle有兩種方式:
1--tcp_nodelay
啟用tcp_nodelay的作用,就是上一個小包沒有確認,當前包也不用緩存而是直接發送。

2--tcp_cork
啟用tcp_cork,那麼網路中一個小包也沒有。發送的都是最大包。所有包發送的時候,都會被累積,直到包的大小達到最大才會被發送。


可以研讀下這篇文章,裡面有你的答案 TCP-IP詳解:Nagle演算法


啟用每次的話,每次都會等到40ms才會一起發送數據


推薦閱讀:

網路爬蟲相關畢業設計,有什麼比較合適的書籍推薦?
TCP/IP 和 HTTP 的區別和聯繫是什麼?
為什麼tcp連接的傳輸速度慢,斷開重新連接後,傳輸速度就變快了呢?
如何用Python寫一個分散式爬蟲?
為什麼網關與主機可以不在同一個網段?

TAG:計算機網路 | 計算機科學 | Socket | 網路編程 | TCPIP |