最近使用 NODE-RED 跟 TCP 打交道。NODE-RED 裡內建了一個節點叫「tcp-out」,看檔案呢使用這個節點可以很方便的把 payload 用 TCP 協定傳送出去,但是事實上事情沒有這麼簡單。其實當我第一次看到這個節點用法的時候我就覺得會有問題,果不其然。既然節點有問題,那麼就乾脆寫程式碼吧,反正 NODE-RED 支援自定義 javascript function 。於是就花了點時間研究了下用 Nodejs 來傳送 TCP 訊息。
上面說了使用內建的節點「tcp-out」傳送 TCP 訊息會有問題。那麼到底是什麼問題呢?
「tcp-out」 節點只是簡單的把 payload 字串轉成了 buffer 然後傳送了出去。其實如果自己做測試,傳送一個訊息然後伺服器端接受一個訊息一點問題都沒有的。但是稍微有一些 socket 程式設計經驗的人都知道,這麼做在生產環境是有問題的。因為在真實的生產環境下,伺服器端都是會定義訊息的結構的。比如我們這次對接的伺服器端就要求每個訊息頭部都需要帶4位元組的包頭,來標識整個訊息的長度。所以我們直接傳送的訊息伺服器端校驗包頭不通過會直接丟棄。
那麼為什麼要這麼做呢?
伺服器端這麼做的原因是 TCP 伺服器端接收訊息有可能出現「粘包」的問題。這時候肯定有同學會出來說了:TCP 是流式協定,根本沒有包的概念怎麼可能粘包呢?是的 ,這說的沒錯。本質上 TCP 作為流式協定根本不可能出現粘包的問題。但是如果從應用層開發者的角度來看,TCP 伺服器端在接受訊息的時候確確實實會出現多個訊息同時收到,或者收到1.x個訊息的問題。站在應用層開發者的角度看,就是幾個包(訊息)黏在了一起。所以也沒必要去咬文嚼字,畢竟大家多數都是應用層開發玩家。
那麼為什麼會有以上問題?讓我們先回顧一下 OSI 網路模型:
TCP位於傳輸層(第四層),傳輸的單位叫 Segment(段);
下面是 IP 協定位於網路層,傳輸的單位叫 Packet (包);
下面是 Datalink 資料鏈路層,單位是 frame (幀);
好了知道了以上知識,我們可以知道 TCP 是已 segment 單位來傳輸的。但是 segment 是有最大值限制的。在 TCP 協定中有個叫 MSS(Max Segment Size) 的東西。一般來說 MSS = MTU - 40 = 1460 位元組。為什麼是一般來說,因為 TCP 協定太複雜了。看上面又引入了一個 MTU 的概念,這裡就不展開來說了,有興趣大家可以自己研究一下 TCP,會大開眼界的。
好了,既然 segment 有最大值限制,那麼很顯然當我們一次傳送的訊息長度超過 MSS ,那麼訊息就會被拆分成多個 segment 來傳送。既然有拆分那麼顯然就有合併。TCP 協定有個 TCP_NODELAY 演演算法,當傳輸大量長度短的資料的時候有可能會觸發 TCP_NODELAY 演演算法。TCP_NODELAY 演演算法就會嘗試把多個短訊息合併成一個 segment 來傳送。
那麼如何解決上述問題呢?方法就是上面說的 ,在每個訊息的開始的地方放一個固定長度的頭部用來表示整個訊息的長度。
伺服器端收到訊息後,先擷取4個位元組的長度,讀取裡面的值獲得整個訊息的長度。然後 payload 長度 = 整個長度-4。然後使用這個長度擷取對應的長度的資料。這樣就得到了一個完整的訊息。如果後面的長度不夠了就等下一個訊息到達後補齊對應長度的資料。如此迴圈以上操作,伺服器端就能解決這個問題了。
好了上面鋪墊了這麼多 ,總算要開始寫程式碼了。
如果你開啟 Google 搜尋 "nodejs 傳送 tcp" 你會得到很多程式碼範例。但是大多數程式碼都是 demo 級別的。也就是都是簡單的把所有的訊息當做 payload 傳送到伺服器端,然後伺服器端列印一下而已。這也是我寫這篇文章的初衷,科普一下一個真正的 TCP 報文(訊息)該怎麼傳送。
就以上面的結構為例:頭部固定4位元組表示整個訊息的長度(4 + length(payload))。
const payloadString = 'hello , world .';
const headerLength = 4;
let socket = net.createConnection({ port: 8888, host: '127.0.0.1' });
socket.on('connect', () => {
console.log('start send data .');
let messageBuff = Buffer.from(payloadString);
let messageLength = messageBuff.length;
let contentLength = Buffer.allocUnsafe(4);
contentLength.writeUInt32BE(headerLength + messageLength);
socket.write(contentLength);
console.log('send header done');
socket.write(messageBuff);
console.log('send payload done');
console.log('send data done .');
});
其實程式碼也沒幾行。簡單說一下就是,在傳送 payload 之前,需要先分配一個 4 位元組長度的 buffer,然後寫入整個訊息的長度,傳送出去,緊接著傳送真正的 payload 。這樣就完成了一次 TCP 報文訊息的傳送。
雖然題目叫 Nodejs 傳送訊息,但是程式碼卻是寥寥幾行。本文多數文字都是在描述 TCP 協定相關的東西。TCP是個偉大(複雜)的協定,要理解它不是件容易的事情,光是連結建立,連結關閉的過程都非常複雜。更別說它那些演演算法了(NODELAY,視窗演演算法,擁堵避免演演算法等等)。但是有時間的話還是可以花點時間研究下,這對於我們這些應用層開發者來說也是一件非常有意義的事。當你瞭解了 TCP 協定後,很多以前似懂非懂的問題都豁然開朗了。比如到底有沒有粘包問題,應用層為什麼要定義資料結構,同一個連線伺服器端會有並行問題嗎?
QQ群:1022985150 VX:kklldog 一起探討學習.NET技術
作者:Agile.Zhou(kklldog)
出處:http://www.cnblogs.com/kklldog/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。