TCP是個“流”協議,所謂流,就是沒有界限的一串數據。可以想想河里的流水,是連成一片的,其間并沒有分界線。TCP底層并不了解上層業務數據的具體含義,它會根據TCP緩沖區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行發送,也有可能把多個小的包封裝成一個大的數據包發送,這就是所謂的TCP粘包和拆包問題。
有關TCP的詳細講解,可以點擊關于三次握手與四次揮手你要知道這些和快速了解TCP的流量控制與擁塞控制
TCP粘包或拆包的原因
- 應用程序寫入的數據大于套接字緩沖區大小,這將會發生拆包。
- 應用程序寫入數據小于套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包。
- 進行MSS(最大報文長度)大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發生拆包。
- 接收方法不及時讀取套接字緩沖區數據,這將發生粘包。
拆包和粘包的形式
第一種情況:接收端正常收到兩個數據包,即沒有發生拆包和粘包的現象,此種情況不在本文的討論范圍內。
發生拆包
第二種情況:接收端只收到一個數據包,由于TCP是不會出現丟包的,所以這一個數據包中包含了發送端發送的兩個數據包的信息,這種現象即為粘包。這種情況由于接收端不知道這兩個數據包的界限,所以對于接收端來說很難處理。
發生粘包
第三種情況:這種情況有兩種表現形式,如下圖。接收端收到了兩個數據包,但是這兩個數據包要么是不完整的,要么就是多出來一塊,這種情況即發生了拆包和粘包。這兩種情況如果不加特殊處理,對于接收端同樣是不好處理的。
發生拆包和粘包
發生拆包和粘包
粘包和拆包的解決辦法
- 發送端給每個數據包添加包首部,首部中應該至少包含數據包的長度,這樣接收端在接收到數據后,通過讀取包首部的長度字段,便知道每一個數據包的實際長度了。
- 發送端將每個數據包封裝為固定長度(不夠的可以通過補0填充),這樣接收端每次從接收緩沖區中讀取固定長度的數據就自然而然的把每個數據包拆分開來。
- 可以在數據包之間設置邊界,添加特殊符號(如:回車符),這樣,接收端通過這個邊界就可以將不同的數據包拆分開。
Netty中的代碼示例
Netty封裝了JDK的NIO,是一個異步事件驅動的網絡應用框架,用于快速開發可維護的高性能服務器和客戶端。一般開發中并不會用JDK原生NIO,原因如下:
- 使用JDK自帶的NIO需要了解太多的概念,編程復雜,一不小心bug橫飛
- Netty底層IO模型隨意切換,而這一切只需要做微小的改動,改改參數,Netty可以直接從NIO模型變身為IO模型
- Netty自帶的拆包解包,異常檢測等機制讓你從NIO的繁重細節中脫離出來,讓你只需要關心業務邏輯
- Netty解決了JDK的很多包括空輪詢在內的bug
- Netty底層對線程,selector做了很多細小的優化,精心設計的reactor線程模型做到非常高效的并發處理
- 自帶各種協議棧讓你處理任何一種通用協議都幾乎不用親自動手
- Netty社區活躍,遇到問題隨時郵件列表或者issue
- Netty已經歷各大rpc框架,消息中間件,分布式通信中間件線上的廣泛驗證,健壯性無比強大
所以,本文選擇演示Netty的編解碼代碼。
在Netty中,我們定義MessageToByteEncoder<T>的繼承類,重寫其encode函數,來自定義編碼器。
public class SocketEncoder extends MessageToByteEncoder<Packet> { @Override protected void encode(ChannelHandlerContext channelHandlerContext, NetPacket msg, ByteBuf byteBuf) throws Exception { byte body[] = msg.getBody(); int packetLen = body.length; // 先設置包長度,然后寫入二進制數據 byteBuf.writeInt(packetLen); byteBuf.writeBytes(body); } }
在Netty中,我們定義ByteToMessageDecoder的繼承類,重寫其decode函數,用來自定義解碼器。
public class SocketDecoder extends ByteToMessageDecoder { @Override void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { int bufLen = byteBuf.readableBytes(); // 解決粘包問題(不夠一個包頭的長度) // 4字節是報文中使用了一個int表示了報文長度 if (bufLen < 4) { return; } // 標記一下當前的readIndex的位置 byteBuf.markReaderIndex(); int packetLength = byteBuf.readInt(); // 讀到的消息體長度如果小于我們傳送過來的消息長度,則resetReaderIndex。重置讀索引,繼續接收 if (byteBuf.readableBytes() < packetLength) { // 配合markReaderIndex使用的。把readIndex重置到mark的地方 byteBuf.resetReaderIndex(); return; } NetPacket netPacket = new NetPacket(); netPacket.setPacketLen(packetLength); // 傳送過來數據的長度,滿足我們的要求了 byte body[] = new byte[packetLength]; byteBuf.readBytes(body); netPacket.setBody(body); list.add(netPacket); } }
更多內容,歡迎關注微信公眾號:全菜工程師小輝~