Netty中解碼器的作用及實現詳解
拆包和沾包
是典型的拆包和沾包問題,俗話說就是兩端通信,一端發(fā)送一端接收,接收的那一端怎么知道是否已經完整的接收了數據?
假設服務端連續(xù)發(fā)送了兩條消息:hello world! / hello client!
由于客戶端不知道怎么才算一條消息,怎么才算兩條消息,所以讀取會有以下幾種情況:
1.分兩次讀取消息,第一次是hello world!,第二次是hello client! 這是正常情況
2.一次就讀取完成,hello world!hello client! 這種情況就叫沾包
3.分兩次讀取消息,第一次是hello ,第二次是world!hello client! 這第一次讀取就是拆包,第二次就是沾包
總之就是讀取到的信息不完整就是拆包,讀取到的信息有額外多的信息就是沾包
演示
我們接著上一章的代碼來演示,我們只需要讓客戶端發(fā)送消息的時候循環(huán)發(fā)送100次,服務端不變,看看服務端是不是接收到了100條消息
NettyClientTestHandler

結果如下:

這明顯就不對吧
解決方案
要怎么解決這種問題呢?
- 消息定長,每條消息都固定長度,不夠則補空格
- 添加分隔符,等于是為每條消息都增加一個結束標識
- 將消息分為消息頭和消息體,消息頭固定長度,里面包含整條消息的長度或者消息體的長度
- 更復雜的協議約定
下面我們通過Netty中幾種內置的解碼器來解決這種問題:
- LineBasedFrameDecoder:行分隔符解碼器(結尾根據 “\n” 作為結束標識)
- DelimiterBasedFrameDecoder:自定義分割器解碼器,結尾根據什么作為結束標識可以自定義
- FixedLengthFrameDecoder:固定長度解碼器,發(fā)送的消息需要定長
- LengthFieldBasedFrameDecoder:基于長度的自定義解碼器,比較靈活
注意:所有編解碼在Netty中都是數據處理管道當中的一個數據處理器而已
LineBasedFrameDecoder
這個是采用行分隔符來解決,所以我們需要改兩個地方
1.發(fā)送消息的時候,消息結尾要加上行分隔符(接著上面的例子來)

2.服務端接收消息,需要在管道內加入解碼器
LineBasedFrameDecoder:傳入的參數是消息最大長度,發(fā)送消息的大小必須小于設置值

結果如下:

DelimiterBasedFrameDecoder
這個跟上面一樣,只不過分隔符我們可以自定義
1.發(fā)送消息的時候,消息結尾要加上分隔符(這里我們定義分隔符是 “$$”)

2.服務端接收消息,需要在管道內加入解碼器

結果如下:

FixedLengthFrameDecoder
會按照設置的固定字節(jié)大小來切割消息
1.這里我們正常的發(fā)送消息就好了

2.服務端接收消息,需要在管道內加入解碼器

結果如下:

LengthFieldBasedFrameDecoder
這個是需要重點介紹一下的,上面三個解碼器明顯的看到不夠靈活,太過于死板,我們看看這個怎么用
源代碼構造如下
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {
this(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true);
}參數含義:
- maxFrameLength:最大幀長度。也就是可以接收的數據的最大長度。如果超過,此次數據會被丟棄
- lengthFieldOffset:長度域偏移量。存儲數據長度的一個偏移量
- lengthFieldLength:長度域字節(jié)數。存儲數據長度的一個大小
- lengthAdjustment:數據長度修正。因為長度既可以代表data的長度,也可以是整個消息的長度
- initialBytesToStrip:跳過的字節(jié)數??梢赃x擇舍棄一部分數據
這參數前三個可以比較好理解,后兩個是干嘛的?沒關系我們一步一步來,后兩個先不用
假設我們要發(fā)送一個消息,結構為:消息頭+數據長度+數據(Hello Server)

看看我們要怎么設置:
整體消息長度:20個字節(jié)、數據data長度:12字節(jié)
首先我們要找到長度域,所以要往右讀取4個字節(jié): lengthFieldOffset設置為4
然后需要讀取數據的長度,所以需要再往右讀4個字節(jié): lengthFieldLength設置為4
之后會根據上面讀取到的數據長度再往后讀取數據:
假設這里數據長度是12,則會繼續(xù)往后讀取12個字節(jié)
至此數據就全讀取完了(lengthAdjustment和initialBytesToStrip都設置成0的情況下)
下面我們演示一下:
NettyClientTestHandler
客戶端發(fā)送消息,結構為:消息頭+數據長度+數據
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String data= "Hello Server";
ByteBuf buffer = Unpooled.buffer();
// 請求頭 4字節(jié)
buffer.writeInt(666);
// 數據長度 4字節(jié)
buffer.writeInt(data.getBytes().length);
// 然后寫入數據
buffer.writeBytes(Unpooled.copiedBuffer(data, CharsetUtil.UTF_8));
// 寫入并發(fā)送 完整的數據為 666+12+Hello Server
ctx.writeAndFlush(buffer);
SocketAddress socketAddress = ctx.channel().remoteAddress();
log.info(socketAddress + " 已連接");
}NettyServerTestHandler
服務端按照結構解析消息
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 和NIO一樣有緩沖區(qū) ByteBuf就是對ByteBuffer做了一層封裝
ByteBuf msg1 = (ByteBuf) msg;
// 讀取請求頭
log.info("請求頭:" + msg1.readInt());
// 讀取長度
int i = msg1.readInt();
log.info("數據長度:" + i);
// 讀取數據
log.info("客戶端信息:" + msg1.readBytes(i).toString(CharsetUtil.UTF_8));
}服務端編碼設置

結果如下:

initialBytesToStrip參數的作用
像上面這樣沾包拆包的問題解決了,但是有一個點很麻煩,就是每次讀取消息都需要一步一步的拆解消息,能不能把消息體前面無用的數據直接舍棄掉,只保留有用的數據部分呢?

可以!initialBytesToStrip參數就是可以在數據讀取完后,可以選擇跳過多少字節(jié)(你可以理解為舍棄,這樣簡單點),就像上面這個例子,我不需要請求頭和數據長度的8個字節(jié),我只需要后面的數據體,所以我可以將initialBytesToStrip設置成8
改動兩個地方

服務端就不需要再拆解了,直接讀取

lengthAdjustment參數的作用
上面我們知道了最后會根據長度域里面的數據來決定再往后讀取多少個字節(jié),這里我們設置的數據長度是12,所以剛剛好往后讀取了12個字節(jié),讀取完成了,要是我設置的數據長度不是12呢?那往后讀取多少個字節(jié),這個是不是需要修正?
所以lengthAdjustment的作用就是來修正最終往后讀取多少個字節(jié)
假設我設置的數據長度是20,代表了整個消息體的長度,但是我數據卻只有12個字節(jié),這往后讀20個字節(jié)無疑是錯的,所以我們需要修正,怎么修正? 減8唄,對吧
所以最終往后讀取多少個字節(jié)=數據長度+lengthAdjustment像上面為例,我們就需要設置成-8
總結
這章主要介紹了解碼器的作用,沾包拆包的問題,但是還是存在一些問題:
每次發(fā)送消息都要寫特定格式,是不是太麻煩了?(自定義編碼器)現在傳輸都是簡單的字符串,實際都是實體類對象,這咋搞?(序列化和反序列化)
之后再介紹怎么自定義協議解決這些問題
到此這篇關于Netty中解碼器的作用及實現詳解的文章就介紹到這了,更多相關Netty解碼器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
idea 創(chuàng)建properties配置文件的步驟
這篇文章主要介紹了idea 創(chuàng)建properties配置文件的步驟,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01
SpringBoot如何使用@Cacheable進行緩存與取值
這篇文章主要介紹了SpringBoot如何使用@Cacheable進行緩存與取值,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08

