含義:
TCP 傳輸協(xié)議是面向流的,沒有數(shù)據(jù)包界限,也就是說消息無邊界。客戶端向服務端發(fā)送數(shù)據(jù)時,可能將一個完整的報文拆分成多個小報文進行發(fā)送,也可能將多個報文合并成一個大的報文進行發(fā)送。(TCP協(xié)議的底層,并不了解上層業(yè)務的具體定義,它會根據(jù)TCP緩沖區(qū)的實際情況進行包的劃分。在業(yè)務層面認為一個完整的包,可能會被TCP拆分成多個小包進行發(fā)送,也可能把多個小的包封裝成一個大的數(shù)據(jù)包進行發(fā)送,這就是所謂的TCP粘包拆包問題。)。
因此就有了拆包和粘包。 在網(wǎng)絡通信的過程中,每次可以發(fā)送的數(shù)據(jù)包大小是受多種因素限制的,如 MTU 傳輸單元大小、滑動窗口等。
所以如果一次傳輸?shù)木W(wǎng)絡包數(shù)據(jù)大小超過傳輸單元大小,那么我們的數(shù)據(jù)可能會拆分為多個數(shù)據(jù)包發(fā)送出去。 如果每次請求的網(wǎng)絡包數(shù)據(jù)都很小,比如一共請求了 10000 次,TCP 并不會分別發(fā)送 10000 次。 TCP采用的 Nagle(批量發(fā)送,主要用于解決頻繁發(fā)送小數(shù)據(jù)包而帶來的網(wǎng)絡擁塞問題) 算法對此作出了優(yōu)化。
客戶端發(fā)送了兩個數(shù)據(jù)包P1和P2給服務端,服務端一次讀取到的字節(jié)數(shù)是不確定的,可能存在以下4種情況:
(1)服務端分兩次讀取到了兩個獨立的數(shù)據(jù)包P1和P2,沒有發(fā)送粘包和拆包;
(2)服務端一次讀到
了兩個數(shù)據(jù)包,P1和P2粘在一起,這就是TCP粘包情況;
(3)服務端分兩次讀取到了兩個數(shù)據(jù)包,第一次讀取了完整的P1包和P2包的一部分,第二次讀取到了P2包的剩余部分,這被稱為TCP拆包;
(4)服務端分兩次讀取了兩個數(shù)據(jù)包,第一次讀取了P1包的一部分,第二次讀取到了P1包的剩余部分,這也是TCP拆包;
解決方法:
由于TCP協(xié)議底層無法理解上層的業(yè)務數(shù)據(jù),所以在底層是無法保證數(shù)據(jù)包不被拆分和重組的,所以,這個問題只能通過上層的應用層協(xié)議設計來解決,常見方案如下:
(1)消息定長,發(fā)送方和接收方規(guī)定固定大小的消息長度,例如每個報文大小固定為200字節(jié),如果不夠,空位補空格;(消息定長法使用非常簡單,但是缺點也非常明顯,無法很好設定固定長度的值,如果長度太大會造成字節(jié)浪費,長度太小又會影響消息傳輸,所以在一般情況下消息定長法不會被采用。)
(2)在包圍增加特殊字符進行分割,例如FTP協(xié)議;分隔符的選擇一定要避免和消息體中字符相同,以免沖突。否則可能出現(xiàn)錯誤的消息拆分。比較推薦的做法是將消息進行編碼,例如 base64 編碼,然后可以選擇 64 個編碼字符之外的字符作為特定分隔符。
(3)自定義協(xié)議,將消息分為消息頭和消息體,消息頭中包含消息總長度,這樣服務端就可以知道每個數(shù)據(jù)包的具體長度了,知道了發(fā)送數(shù)據(jù)包的具體邊界后,就可以解決粘包和拆包問題了;
netty解決粘包拆包問題:
DelimiterBasedFrameDecoder:每個應用層數(shù)據(jù)包,都通過自定義分隔符,進行分割拆分
LineBasedFrameDecoder:每個應用層數(shù)據(jù)包,都以換行符作為分隔符,進行分割拆分。
FixedLengthFrameDecoder:每個應用層數(shù)據(jù)包的拆分都是固定長度大小
LengthFieldBasedFrameDecoder+LengthFieldPrepender:自定義消息長度。?將應用層數(shù)據(jù)包的長度,作為接收端應用層數(shù)據(jù)包的拆分依據(jù)。按照應用層數(shù)據(jù)包的大小,進行拆包。這個拆包器有個要求,應用層協(xié)議包含數(shù)據(jù)包長度。(LengthFieldPrepender:待發(fā)送消息長度寫入到前幾個字節(jié))。
筆者本人自研rpc框架的編解碼自定義協(xié)議:
先讀取消息類型(Requst, Response), 序列化方式(原生, json?加上消息長度:防止粘包, 再根據(jù)長度讀取data.文章來源:http://www.zghlxwxcb.cn/news/detail-420710.html
消息類型(2Byte) | 序列化方式 2Byte | 消息長度 4Byte |
---|---|---|
序列化后的Data…. | 序列化后的Data… | 序列化后的Data…. |
編碼
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// 寫入消息類型
if(msg instanceof RPCRequest){
out.writeShort(MessageType.REQUEST.getCode());
}
else if(msg instanceof RPCResponse){
out.writeShort(MessageType.RESPONSE.getCode());
}
// 寫入序列化方式
out.writeShort(serializer.getType());
// 得到序列化數(shù)組
byte[] serialize = serializer.serialize(msg);
// 寫入長度
out.writeInt(serialize.length);
// 寫入序列化字節(jié)數(shù)組
out.writeBytes(serialize);
}
解碼
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 1. 讀取消息類型
short messageType = in.readShort();
// 現(xiàn)在還只支持request與response請求
if(messageType != MessageType.REQUEST.getCode() &&
messageType != MessageType.RESPONSE.getCode()){
System.out.println("暫不支持此種數(shù)據(jù)");
return;
}
// 2. 讀取序列化的類型
short serializerType = in.readShort();
// 根據(jù)類型得到相應的序列化器
Serializer serializer = Serializer.getSerializerByCode(serializerType);
if(serializer == null)throw new RuntimeException("不存在對應的序列化器");
// 3. 讀取數(shù)據(jù)序列化后的字節(jié)長度
int length = in.readInt();
// 4. 讀取序列化數(shù)組
byte[] bytes = new byte[length];
in.readBytes(bytes);
// 用對應的序列化器解碼字節(jié)數(shù)組
Object deserialize = serializer.deserialize(bytes, messageType);
out.add(deserialize);
}
?文章來源地址http://www.zghlxwxcb.cn/news/detail-420710.html
到了這里,關于TCP粘包和拆包問題及其解決方法的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!