netty系列之:自定义编码解码器
文章目錄
- 簡介
- 自定義編碼器
- 自定義解碼器
- 添加編碼解碼器到pipeline
- 計算2的N次方
- 總結
簡介
在之前的netty系列文章中,我們講到了如何將對象或者String轉換成為ByteBuf,通過使用netty自帶的encoder和decoder可以實現非常方便的對象和ByteBuf之間的轉換,然后就可以向channel中隨意寫入對象和字符串了。
使用netty自帶的編碼器當然很好,但是如果你有些特殊的需求,比如希望在編碼的過程中對數據進行變換,或者對對象的字段進行選擇,那么可能就需要自定義編碼解碼器了。
自定義編碼器
自定義編碼器需要繼承MessageToByteEncoder 類,并實現encode方法,在該方法中寫入具體的編碼邏輯。
本例我們希望計算2的N次方,據說將一張紙折疊100次可以達到地球到月亮的高度,這么大的數據普通的number肯定是裝不下的,我們將會使用BigInteger來對這個巨大的數字進行保存。
那么對于被編碼器來說,則需要將這個BigInteger轉換成為byte數組。同時在byte數組讀取的過程中,我們需要界定到底哪些byte數據是屬于同一個BigInteger的,這就需要對寫入的數據格式做一個約定。
這里我們使用三部分的數據結構來表示一個BigInteger。第一部分是一個magic word也就是魔法詞,這里我們使用魔法詞“N”,當讀取到這個魔法詞就表示接下來的數字是BigInteger。第二部分是表示bigInteger數字的byte數組的長度,獲取到這個長度值,就可以讀取到所有的byte數組值,最后將其轉換成為BigInteger。
因為BigInteger是Number的子類,為了更加泛化編碼器,我們使用Number作為MessageToByteEncoder的泛型,核心編碼代碼如下:
protected void encode(ChannelHandlerContext ctx, Number msg, ByteBuf out) {// 將number編碼成為ByteBufBigInteger v;if (msg instanceof BigInteger) {v = (BigInteger) msg;} else {v = new BigInteger(String.valueOf(msg));}// 將BigInteger轉換成為byte[]數組byte[] data = v.toByteArray();int dataLength = data.length;// 將Number進行編碼out.writeByte((byte) 'N'); // 魔法詞out.writeInt(dataLength); // 數組長度out.writeBytes(data); // 最終的數據}自定義解碼器
有了編碼之后的byte數組,就可以在解碼器中對其解碼了。
上一節介紹了,編碼過后的數據格式是魔法詞N+數組長度+真正的數據。
其中魔法詞長度是一個字節,數組長度是四個字節,前面部分總共是5個字節。所以在解碼的時候,首先判斷ByteBuf中可讀字節的長度是否小于5,如果小于5說明數據是無效的,可以直接return。
如果可讀字節的長度大于5,則表示數據是有效的,可以進行數據的解碼了。
解碼過程中需要注意的是,并不是所有的數據都是我們所希望的格式,如果在讀取的過程中讀到了我們不認識的格式,那么說明這個數據并不是我們想要的,則可以交由其他的handler進行處理。
但是對于ByteBuf來說,一旦調用read方法,就會導致reader index移動位置,所以在真正的讀取數據之前需要調用ByteBuf的markReaderIndex方法,對readerIndex進行記錄。然后分別讀取魔法詞、數組長度和剩余的數據,最后將數據轉換成為BigInteger,如下所示:
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {// 保證魔法詞和數組長度有效if (in.readableBytes() < 5) {return;}in.markReaderIndex();// 檢查魔法詞int magicNumber = in.readUnsignedByte();if (magicNumber != 'N') {in.resetReaderIndex();throw new CorruptedFrameException("無效的魔法詞: " + magicNumber);}// 讀取所有的數據int dataLength = in.readInt();if (in.readableBytes() < dataLength) {in.resetReaderIndex();return;}// 將剩下的數據轉換成為BigIntegerbyte[] decoded = new byte[dataLength];in.readBytes(decoded);out.add(new BigInteger(decoded));}添加編碼解碼器到pipeline
有了兩個編碼解碼器,還需要將其添加到pipeline中進行調用。
在實現ChannelInitializer中的initChannel中,可以對ChannelPipeline進行初始化,本例中的初始化代碼如下:
// 對流進行壓縮pipeline.addLast(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP));pipeline.addLast(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));// 添加number編碼解碼器pipeline.addLast(new NumberDecoder());pipeline.addLast(new NumberEncoder());// 添加業務處理邏輯pipeline.addLast(new CustomProtocolServerHandler());其中最后一行是真正的業務處理邏輯,NumberDecoder和NumberEncoder是編碼和解碼器。這里我們還使用了一個ZlibEncoder用于對流數據進行壓縮,這里使用的壓縮方式是GZIP。
壓縮的好處就是可以減少數據傳輸的數量,提升傳輸效率。其本質也是一個編碼解碼器。
計算2的N次方
計算2的N次方的邏輯是這樣的,首先客戶端發送2給服務器端,服務器端接收到該消息和結果1相乘,并將結果寫回給客戶端,客戶端收到消息之后再發送2給服務器端,服務器端將上次的計算結果乘以2,再發送給客戶端,以此類推直到執行N次。
首先看下客戶端的發送邏輯:
// 最大計算2的1000次方ChannelFuture future = null;for (int i = 0; i < 1000 && next <= CustomProtocolClient.COUNT; i++) {future = ctx.write(2);next++;}當next小于等于要計算的COUNT時,就將2寫入到channel中。
對于服務器來說,在channelRead0方法中,讀取消息,并將其和結果相乘,再把結果寫回給客戶端。
public void channelRead0(ChannelHandlerContext ctx, BigInteger msg) throws Exception {// 將接收到的msg乘以2,然后返回給客戶端count++;result = result.multiply(msg);ctx.writeAndFlush(result);}客戶端統計讀取到的消息個數,如果消息個數=COUNT,說明計算完畢,就可以將結果保存起來供后續使用,其核心代碼如下:
public void channelRead0(ChannelHandlerContext ctx, final BigInteger msg) {receivedMessages ++;if (receivedMessages == CustomProtocolClient.COUNT) {// 計算完畢,將結果放入answer中ctx.channel().close().addListener(future -> {boolean offered = answer.offer(msg);assert offered;});}}總結
本文實現了一個Number的編碼解碼器,事實上你可以自定義實現任何對象的編碼解碼器。
本文的例子可以參考:learn-netty4
本文已收錄于 http://www.flydean.com/13-netty-customprotocol/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!
總結
以上是生活随笔為你收集整理的netty系列之:自定义编码解码器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: netty系列之:对聊天进行加密
- 下一篇: netty系列之:自定义编码和解码器要注