Netty中粘包/拆包處理
Python實(shí)戰(zhàn)社群
Java實(shí)戰(zhàn)社群
長按識別下方二維碼,按需求添加
掃碼關(guān)注添加客服
進(jìn)Python社群▲
掃碼關(guān)注添加客服
進(jìn)Java社群▲
作者丨ytao
來源丨ytao
TCP 是基于流傳輸?shù)膮f(xié)議,請求數(shù)據(jù)在其傳輸?shù)倪^程中是沒有界限區(qū)分,所以我們在讀取請求的時(shí)候,不一定能獲取到一個完整的數(shù)據(jù)包。如果一個包較大時(shí),可能會切分成多個包進(jìn)行多次傳輸。同時(shí),如果存在多個小包時(shí),可能會將其整合成一個大包進(jìn)行傳輸。這就是 TCP 協(xié)議的粘包/拆包概念。
本文基于 Netty5 進(jìn)行分析
粘包/拆包描述
假設(shè)當(dāng)前有 123和 abc兩個數(shù)據(jù)包,那么他們傳輸情況示意圖如下:

I 為正常情況,兩次傳輸兩個獨(dú)立完整的包。
II 為粘包情況,?
123和?abc封裝成了一個包。III 為拆包情況,圖中的描述是將?
123拆分成了?1和?23,并且?1和?abc一起傳輸。?123和?abc也可能是?abc進(jìn)行拆包。甚至?123和?abc進(jìn)行多次拆分也有可能。
Netty 粘包/拆包問題
為突出 Netty 的粘包/拆包問題,這里通過例子進(jìn)行重現(xiàn)問題,以下為突出問題的主要代碼:
服務(wù)端:
/**
* 服務(wù)端網(wǎng)絡(luò)事件的讀寫操作類
*
* Created by YangTao.
*/
publicclassServerHandlerextendsChannelHandlerAdapter{
// 接收消息計(jì)數(shù)器
privateint i = 0;
// client端消息
@Override
publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throwsException{
i++;
System.out.print(msg);
// 對每條讀取到的消息進(jìn)行打數(shù)標(biāo)記
System.out.println("================== ["+ i +"]");
// 發(fā)送應(yīng)答消息給客戶端
ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes());
ctx.write(rmsg);
}
// 其他操作 .......
}
客戶端:
/**
* 客戶端發(fā)送數(shù)據(jù)
*
* Created by YangTao.
*/
publicclassNettyClient{
publicvoid send() {
Bootstrap bootstrap = newBootstrap();
NioEventLoopGroup group = newNioEventLoopGroup();
try{
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(newChannelInitializer<NioSocketChannel>() {
@Override
protectedvoid initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(newStringDecoder());
pipeline.addLast("logger", newLoggingHandler(LogLevel.INFO));
pipeline.addLast(newClientHandler());
}
});
Channel channel = bootstrap.connect(HOST, PORT).channel();
int i = 1;
while(i <= 300){
channel.writeAndFlush(String.format("【時(shí)間 %s: \t%s】", newDate(), i));
// 打印發(fā)送請求的次數(shù)
System.out.println(i);
i++;
}
}catch(Exception e){
e.printStackTrace();
}finally{
if(group != null)
group.shutdownGracefully();
}
}
}
以上代碼中,我們第一反應(yīng)理解的是,如果非異常情況下客戶端所有數(shù)據(jù)發(fā)送成功,并且服務(wù)端全部接收到。那么從打印信息中可以看到客戶端的發(fā)送次數(shù) i和服務(wù)端的接收消息計(jì)數(shù) i應(yīng)該是相同的數(shù)。那么下面通過運(yùn)行程序,查看打印結(jié)果。

如上圖所示, 【】中的最后一個數(shù)字與 []中數(shù)字對上的是已獨(dú)立完整的包接收到(粘包/拆包示意圖中的情況 I)。但是 【】中為 37和 38的出現(xiàn)了粘包情況(粘包/拆包示意圖中的情況 II),兩條數(shù)據(jù)粘合在一起。

上圖中可以看到 【】中 167的數(shù)據(jù)被拆分為了兩部分(圖中畫綠線數(shù)據(jù)),該情況為拆包(粘包/拆包示意圖中的情況 III)。
上面程序沒有考慮到 TCP 的粘包/拆包問題,所以如果是我們實(shí)際應(yīng)用的程序的話,不能保證數(shù)據(jù)的正常情況,就會導(dǎo)致程序異常。
Netty 解決粘包/拆包問題
LineBasedFrameDecoder 換行符處理
Netty 的強(qiáng)大,方便,簡單使用的優(yōu)勢,在粘包/拆包問題上也提供了多種編解碼解決方案,并且很容易理解和掌握。這里使用 LineBasedFrameDecoder 和 StringDecoder(將接收到的對象轉(zhuǎn)換成字符串) 來解決粘包/拆包問題。只需在服務(wù)端和客戶端分別添加 LineBasedFrameDecoder 和 StringDecoder解碼器,因?yàn)槭请p向會話,所以兩端都要添加,由于我一開始就添加 StringDecoder 編碼器,所以只需添加 LineBasedFrameDecoder 就夠了。服務(wù)端:

客戶端:

服務(wù)端網(wǎng)絡(luò)事件操作:
/**
* 服務(wù)端網(wǎng)絡(luò)事件的讀寫操作類
*
* Created by YangTao.
*/
publicclassServerHandlerextendsChannelHandlerAdapter{
// 接收消息計(jì)數(shù)器
privateint i = 0;
// client端消息
@Override
publicvoid channelRead(ChannelHandlerContext ctx, Object msg) throwsException{
i++;
System.out.print(msg);
// 對每條讀取到的消息進(jìn)行打數(shù)標(biāo)記
System.out.println("================== ["+ i +"]");
// 發(fā)送應(yīng)答消息給客戶端
ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes());
ctx.write(rmsg);
}
// 其他操作 .......
}
客戶端發(fā)送數(shù)據(jù):
/**
* 客戶端發(fā)送數(shù)據(jù)
*
* Created by YangTao.
*/
publicclassNettyClient{
publicvoid send() {
// 連接操作 .......
try{
// 獲取 channel
Channel channel = channel();
int i = 1;
ByteBuf buf = null;
while(i <= 300){
String str = String.format("【時(shí)間 %s: \t%s】", newDate(), i) + System.getProperty("line.separator");
byte[] bytes = str.getBytes();
// 寫入緩沖區(qū)
buf = Unpooled.buffer(bytes.length);
buf.writeBytes(bytes);
channel.writeAndFlush(buf);
// 打印發(fā)送請求的次數(shù)
System.out.println(i);
i++;
}
}catch(Exception e){
e.printStackTrace();
}
// 退出操作 .......
}
}
細(xì)心觀察代碼的變化,應(yīng)該會發(fā)現(xiàn)現(xiàn)在的代碼每次在發(fā)送消息的時(shí)候,在消息末尾后加了換行分隔符。注意,使用 LineBasedFrameDecoder 時(shí),換行分隔符必須加,否則接收消息端收不到消息,如果手寫換行分割,要記得區(qū)分不同系統(tǒng)的適配。
經(jīng)過多次測試 3W 條請求,沒有再出現(xiàn)過粘包/拆包情況,看最后一條數(shù)據(jù)數(shù)字是否相同便知。

DelimiterBasedFrameDecoder 自定義分隔符
自定義分隔符和換行分隔符差不多,只需將發(fā)送的數(shù)據(jù)后換行符換成你自己設(shè)定的分割符即可。
服務(wù)端和客戶端均在 pipeline 添加 DelimiterBasedFrameDecoder:
// 指定的分隔符
publicstaticfinalString DELIMITER = "$@$";
// 如果當(dāng)前數(shù)據(jù)2048個字節(jié)中沒有分隔符,就會拋出異常,避免內(nèi)存溢出。也可以自定義預(yù)檢查當(dāng)前讀取的數(shù)據(jù),自定義這里超過的規(guī)則
pipeline.addLast(newDelimiterBasedFrameDecoder(
2048,
Unpooled.wrappedBuffer(DELIMITER.getBytes())) // 分割符緩沖對象
);
FixedLengthFrameDecoder 根據(jù)固定長度
設(shè)定固定長度,進(jìn)行數(shù)據(jù)傳輸,如果不達(dá)固定長度,使用空格補(bǔ)全。
服務(wù)端和客戶端均在 pipeline 添加 FixedLengthFrameDecoder:
// 100為指定的固定長度
ch.pipeline().addLast(newFixedLengthFrameDecoder(100));
每次讀取數(shù)據(jù)時(shí)都會按照 FixedLengthFrameDecoder 中設(shè)置的固定長度進(jìn)行解碼,如果出現(xiàn)粘包,那么會進(jìn)行多次解碼,如果出現(xiàn)拆包的情況,那么 FixedLengthFrameDecoder 會先緩存當(dāng)前部分包的信息,當(dāng)接收下一個包時(shí),會與緩存的部分包進(jìn)行拼接,知道滿足規(guī)定的長度。
動態(tài)指定長度
動態(tài)指定長度就是說,每條消息的長度都是隨著消息頭進(jìn)行指定,這里使用的編碼器為 LengthFieldBasedFrameDecoder。
pipeline().addLast(
newLengthFieldBasedFrameDecoder(
2048, // 幀的最大長度,即每個數(shù)據(jù)包最大限度
0, // 長度字段偏移量
4, // 長度字段所占的字節(jié)數(shù)
0, // 消息頭的長度,可以為負(fù)數(shù)
4) // 需要忽略的字節(jié)數(shù),從消息頭開始,這里是指整個包
);
發(fā)送消息時(shí),創(chuàng)建自己的消息對象編碼器
// 創(chuàng)建 byteBuf
ByteBuf buf = getBuf();
// .....
// 設(shè)置該條消息內(nèi)容長度
buf.writeInt(msg.length());
// 設(shè)置消息內(nèi)容
buf.writeBytes(msg.getBytes("UTF-8"));
服務(wù)端讀取的時(shí)候就直接讀取即可,沒其他特殊操作。
除了以上 Netty 提供的現(xiàn)成方案,還可以通過重寫 MessageToByteEncoder 編碼實(shí)現(xiàn)自定義協(xié)議。
總結(jié)
Netty 極大的為使用者提供了多種解決粘包/拆包方案,并且可以很愉快的對多種消息進(jìn)行自動解碼,在使用過程中也極容易掌握和理解,很大程度上提升開發(fā)效率和穩(wěn)定性。


近期精彩內(nèi)容推薦:??


