Netty 編碼和解碼
1. 前言
本節內容,主要是講解 Netty 的編碼和解碼,前面我們講解了 ByteBuf,Netty 是面向 ByteBuf 來編程的,發送的內容會被編碼成 ByteBuf,從 Channel 接受的數據流則被封裝成了 ByteBuf,需要把它解碼成我們所熟悉的格式。
2. 編碼和解碼的作用
首先,我們先通過一個實例來進行說明。
實例:
ch.pipeline().addLast(new CodecHandler());
public class CodecHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接受:"+msg.toString());
}
}
客戶端發送數據:
執行結果:
接受:PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 1024)
通過以上測試,發現客戶端往服務端發送普通的字符串,服務端接受的時候并不是正常字符串,而是把 ByteBuf 類型打印出來。
主要原因是,Netty 的數據類型是 ByteBuf,無法直接強轉,需要通過解碼的方式去轉換才能得到正常的數據,編碼也是同樣道理。
因此,本節學編碼和解碼的知識可以了解 Netty 如何去接受和發送參數。
3. 解碼示例
實例:
public class ServerLoginHandler extends ChannelInboundHandlerAdapter {
//1.讀取客戶端發送過來的數據
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//1.轉換ByteBuf
ByteBuf buffer=(ByteBuf)msg;
//2.定義一個byte數組,長度是ByteBuf的可讀字節數
byte[] bytes=new byte[buffer.readableBytes()];
//3.往自定義的byte[]讀取數據
buffer.readBytes(bytes);
//4.字節流->字符串
String str=new String(bytes);
}
}
通過以上代碼,我們發現能正常接收并且打印客戶端發送過來的字符串數據。但是如果是其它的類型數據(比如:Map,實體,List 等)那么還得手工寫另外的轉換方法,相對比較麻煩。
4. 編碼解碼流程
4.1 整體流程
無論是使用 Netty 還是原始的 Socket 編程,基于 TCP 通信的數據包格式均為二進制,但是我們平時開發不可能基于二進制去開發,而是封裝一個一個的實體。這樣的話,我們就需要實現實體和二進制之間的編碼和解碼了。
- 客戶端往服務端發送消息,手寫需要把實體轉換成 byte [],并且把 byte [] 寫入到 ByteBuf 容器里面,最終轉換二進制。其實,整個過程就是一個編碼的過程;
- 服務端接受到消息,二進制是給機器去識別的,人眼無法快速去識別它,然而實體是我們所熟悉并且一看就能看出有哪些屬性,因此需要把二進制轉換我們所熟悉的實體,整個過程就是一個解碼的過程。
4.2 編碼流程
實例:
//封裝編碼方法
public ByteBuf encode(Object obj) {
// 1. 創建 ByteBuf 對象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 2. 序列化 Java 對象
byte[] bytes = SerializeUtils.serialize(obj);
// 3. 實際編碼過程
byteBuf.writeBytes(bytes);
return byteBuf;
}
//序列化工具類
public class SerializeUtils{
//序列化方法
public static byte[] serialize(Object obj){
//省略序列化過程
return null;
}
}
代碼說明:
- 創建一個 ByteBuf(前面章節詳細講解過);
- 把內容序列化成字節數組;
- 把字節數組寫入到 ByteBuf。
4.3 解碼流程
實例:
//解碼
public <T> T decode(ByteBuf byteBuf,Class clazz) {
// 數據包長度
int length = byteBuf.readableBytes();
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);
return SerializeUtils.desrialize(bytes,clazz);
}
//序列化工具類
public class SerializeUtils{
//序列化方法
public static <T> T desrialize(byte[] bytes,Class clazz){
//省略反序列化過程
return null;
}
}
代碼說明:
- 根據 ByteBuf 獲取可讀的數據長度;
- 根據數據長度創建相應的字節數組;
- 把 ByteBuf 里面的內容讀取到自定義的字節數組里面;
- 通過反序列化的手段,把字節數組反序列化成對象。
5. 序列化和反序列化
上面講編碼和解碼的時候,涉及兩個空方法沒有實現,分別是 serialize()
序列化和 desrialize()
反序列化,其實序列化和反序列化技術選擇很多,常見的解決方案大概如下:
- 通過對象流來手工實現序列化,但是實體必須實現
Serializeable
序列化接口,否則無法被正常序列化和反序列化; - 對象 -> 轉換 json 格式的字符串,Java 里面 String 類型字符串可以自動轉換字節數組,常見的開源框架分別有 Fastjson、Jackjson 等;
- 對象 - 轉存 xml 格式的字符串,常見框架有 XStream 等;
- 其他技術,如:Hessian 序列化、Kryo 序列化等。
這里就不詳細展開展示序列化和反序列化的說明,如果有興趣,可以參考我寫的另外一篇文章:
接下來,主要說明的是,為了靈活擴展,我們最好不要寫死某種序列化技術,為了方便后期更改技術框架,因為每種序列化技術的差距比較大,主要體現兩點:
- 消耗時間: 序列化和反序列化的消耗時間長度;
- 數據長度: 序列化過后的字節數組長度,這個是會影響網絡傳輸性能的。
一般情況下,通過面向接口 + 策略模式的方式去解耦,底層可以靈活的切換序列化技術。
實例:
//定義一個序列化接口
public interface SerializeService<T>{
//序列化方法
public byte[] serialize(T t);
//反序列化方法
public T deserialize(byte[] bytes,Class<T> clazz);
}
//具體序列化實現列
public class JsonSerializeService<T> implements SerializeService<T>{
//序列化方法
public byte[] serialize(T t){
return null;
}
//反序列化方法
public T deserialize(byte[] bytes,Class<T> clazz){
return null;
}
}
//序列化使用
@Component
public class Test{
@Autowired
private SerializeService serializeService;
public ByteBuf encode(Object obj) {
// 1. 創建 ByteBuf 對象
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 2. 序列化 Java 對象
byte[] bytes = serializeService.serialize(obj);
// 3. 實際編碼過程
byteBuf.writeBytes(bytes);
return byteBuf;
}
}
6. 小結
本節內容大家掌握好以下內容:
- 編碼和解碼的概念是什么?為什么需要編碼和解碼?
- Netty 如何去進行編碼和解碼,以及大體流程是什么?
- 編碼和解碼需要依賴序列化和反序列化技術,要了解序列化方面的技術有哪些。
思考題:能否把我們的編碼和解碼封裝成獨立的 Handler 呢?那么應該如何去封裝呢?