Netty中序列化的作用及自定義協(xié)議詳解
前言
上一章已經(jīng)說了怎么解決沾包和拆包的問題,但是這樣離一個成熟的通信還是有一點(diǎn)距離,我們還需要讓服務(wù)端和客戶端使用同一個"語言"來溝通,要不然一個講英文一個講中文,兩個都聽不懂豈不是很尷尬?這種語言就叫協(xié)議。
Netty自身就支持很多種協(xié)議比如Http、Websocket等等,但如果用來作為自己的RPC框架通常會自定義協(xié)議,所以這也是本文的重點(diǎn)!
序列化的重要性
在說協(xié)議之前,我們需要先知道什么是序列化,序列化是干嘛的?
我們要知道數(shù)據(jù)在傳輸?shù)倪^程中是以0和1的形式傳輸?shù)?,而把對象轉(zhuǎn)化成二進(jìn)制的過程就叫序列化,將二進(jìn)制轉(zhuǎn)化為對象的過程就叫反序列化。
為什么要說這個很重要呢?因?yàn)樾蛄谢头葱蛄谢切枰臅r的,而序列化后的字節(jié)大小也會影響到傳輸?shù)男?,所以選對一種高效的序列化方式是非常之重要的,下面我們以JDK自帶的序列化和我們常用的JSON序列化來做一個對比,序列化后大小的對比、序列化效率的對比
大小對比
我們先準(zhǔn)備一個實(shí)體類SerializeTestVO實(shí)現(xiàn)Serializable 接口
public class SerializeTestVO implements Serializable { private Integer id; private String name; private Integer age; private Integer sex; private Integer bodyWeight; private Integer height; private String school; //Set、get方法省略 }
測試方法:
public static void main(String[] args) throws IOException { // 普普通通的實(shí)體類 SerializeTestVO serializeTestVO = new SerializeTestVO(); serializeTestVO.setAge(18); serializeTestVO.setBodyWeight(120); serializeTestVO.setHeight(180); serializeTestVO.setId(10000); serializeTestVO.setName("張三"); serializeTestVO.setSchool("XXXXXXXXXXXX"); // JDK序列化 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(serializeTestVO); objectOutputStream.flush(); objectOutputStream.close(); System.out.println("JDK 序列化大小: "+(byteArrayOutputStream.toByteArray().length)); byteArrayOutputStream.close(); //JSON序列化 System.out.println("JSON 序列化大小: " + JSON.toJSONString(serializeTestVO).getBytes().length); }
結(jié)果:
可以看到序列化后大小相差了好幾倍,這也意味著傳輸效率的幾倍
效率對比
實(shí)體類保持不變,我們序列化300W次,看看結(jié)果
public static void main(String[] args) throws IOException { SerializeTestVO serializeTestVO = new SerializeTestVO(); serializeTestVO.setAge(18); serializeTestVO.setBodyWeight(120); serializeTestVO.setHeight(180); serializeTestVO.setId(10000); serializeTestVO.setName("張三"); serializeTestVO.setSchool("XXXXXXXXXXXX"); long start = System.currentTimeMillis(); for (int i = 0; i < 3000000; i++) { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(serializeTestVO); objectOutputStream.flush(); objectOutputStream.close(); byte[] bytes = byteArrayOutputStream.toByteArray(); byteArrayOutputStream.close(); } System.out.println("JDK 序列化耗時: " + (System.currentTimeMillis() - start)); long start1 = System.currentTimeMillis(); for (int i = 0; i < 3000000; i++) { byte[] bytes = JSON.toJSONString(serializeTestVO).getBytes(); } System.out.println("JSON 序列化耗時: " + (System.currentTimeMillis() - start1)); }
結(jié)果:
幾乎6倍的差距,結(jié)合序列化后的大小綜合來看,選擇一種好的序列化方式是多么的重要
自定義協(xié)議
其實(shí)到現(xiàn)在我們已經(jīng)掌握了自定義協(xié)議里面最關(guān)鍵的幾個點(diǎn)了,序列化、數(shù)據(jù)結(jié)構(gòu)、編解碼器,我們一個一個來
序列化
直接采用我們常用且熟悉的JSON序列化
數(shù)據(jù)結(jié)構(gòu)
我們設(shè)置為消息頭和消息體,結(jié)構(gòu)如下:
消息頭包含:開始標(biāo)志、時間戳、消息體長度
消息體包含:通信憑證、消息ID、消息類型、消息
實(shí)體類如下:
@Data public class NettyMsg { private NettyMsgHead msgHead=new NettyMsgHead(); private NettyBody nettyBody; public NettyMsg(ServiceCodeEnum codeEnum, Object msg){ this.nettyBody=new NettyBody(codeEnum, msg); } } @Data public class NettyMsgHead { // 開始標(biāo)識 private short startSign = (short) 0xFFFF; // 時間戳 private final int timeStamp; public NettyMsgHead(){ this.timeStamp=(int)(DateUtil.current() / 1000); } } @Data public class NettyBody { // 通信憑證 private String token; // 消息ID private String msgId; // 消息類型 private short msgType; // 消息 這里序列化采用JSON序列化 // 所以這個msg可以是實(shí)體類的msg 兩端通過消息類型來判斷實(shí)體類類型 private String msg; public NettyBody(){ } public NettyBody(ServiceCodeEnum codeEnum,Object msg){ this.token=""; // 鑒權(quán)使用 this.msgId=""; // 拓展使用 this.msgType=codeEnum.getCode(); this.msg= JSON.toJSONString(msg); } }
消息類型枚舉
@JsonFormat(shape = JsonFormat.Shape.OBJECT) public enum ServiceCodeEnum { TEST_TYPE((short) 0xFFF1, "測試"); private final short code; private final String desc; ServiceCodeEnum(short code, String desc) { this.code = code; this.desc = desc; } public short getCode() { return code; } }
自定義編碼器
編碼器的作用就是固定好我們的數(shù)據(jù)格式,無需在每次發(fā)送數(shù)據(jù)的時候還需要去對數(shù)據(jù)進(jìn)行格式編碼
public class MyNettyEncoder extends MessageToByteEncoder<NettyMsg> { @Override protected void encode(ChannelHandlerContext channelHandlerContext, NettyMsg msg, ByteBuf out) throws Exception { // 寫入開頭的標(biāo)志 out.writeShort(msg.getMsgHead().getStartSign()); // 寫入秒時間戳 out.writeInt(msg.getMsgHead().getTimeStamp()); byte[] bytes = JSON.toJSON(msg.getNettyBody()).toString().getBytes(); // 寫入消息長度 out.writeInt(bytes.length); // 寫入消息主體 out.writeBytes(bytes); } }
自定義解碼器
解碼器的第一個作用就是解決沾包和拆包的問題,第二個作用就是對數(shù)據(jù)有效性的校驗(yàn),比如數(shù)據(jù)協(xié)議是否匹配、數(shù)據(jù)是否被篡改、數(shù)據(jù)加解密等等
所以我們直接繼承LengthFieldBasedFrameDecoder類,重寫decode方法,利用父類來解決沾包和拆包問題,自定義來解決數(shù)據(jù)有效性問題
public class MyNettyDecoder extends LengthFieldBasedFrameDecoder { // 開始標(biāo)記 private final short HEAD_START = (short) 0xFFFF; public MyNettyDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength) { super(maxFrameLength, lengthFieldOffset, lengthFieldLength); } public MyNettyDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) { super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip); } public MyNettyDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) { super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast); } public MyNettyDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) { super(byteOrder, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast); } @Override protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { // 經(jīng)過父解碼器的處理 我們就不需要在考慮沾包和半包了 // 當(dāng)然,想要自己處理沾包和半包問題也不是不可以 ByteBuf decode = (ByteBuf) super.decode(ctx, in); if (decode == null) { return null; } // 開始標(biāo)志校驗(yàn) 開始標(biāo)志不匹配直接 過濾此條消息 short startIndex = decode.readShort(); if (startIndex != HEAD_START) { return null; } // 時間戳 int timeIndex = decode.readInt(); // 消息體長度 int lenOfBody = decode.readInt(); // 讀取消息 byte[] msgByte = new byte[lenOfBody]; decode.readBytes(msgByte); String msgContent = new String(msgByte); // 將消息轉(zhuǎn)成實(shí)體類 傳遞給下面的數(shù)據(jù)處理器 return JSON.parseObject(msgContent, NettyBody.class); } }
安全性
上述的協(xié)議里面,我只預(yù)留了三種簡單的校驗(yàn),一個是開始標(biāo)識,二是消息憑證,三是時間戳,實(shí)時上這太簡單了,下面我說幾種可以加上去拓展的:
消息整體加密:消息頭添加一個加密類型,客戶端和服務(wù)端都內(nèi)置幾種加解密手段,在發(fā)送消息的時候隨機(jī)一種加密方式對加密類型、消息長度以外的其他內(nèi)容加密,接收的時候再解密,但是要注意加密后不能影響沾包和拆包的處理
消息體加密:添加結(jié)束標(biāo)識放入消息體,和上述方式類似,但是是對消息體中的內(nèi)容再次加密,可和上述方式結(jié)合,形成二次加密
時間戳:可以對長時間才接收到的消息拒收,或者要求重發(fā)根據(jù)消息ID
加簽和驗(yàn)簽:對具體的消息加簽和驗(yàn)簽,防止篡改
憑證:這個很熟悉了,就比如登錄憑證
復(fù)雜格式:上述的數(shù)據(jù)格式還是過于簡單,實(shí)際可以整了更加復(fù)雜
驗(yàn)證
主體代碼呢還是之前的,我們改動幾個地方
NettyClient
解碼器是繼承的LengthFieldBasedFrameDecoder,所以參數(shù)也一樣,不懂的看一下上一篇
NettyServer
NettyClientTestHandler
發(fā)送100次是為了驗(yàn)證沾包和拆包,發(fā)送不同的開始標(biāo)志,是為了驗(yàn)證接收的時候是否有過濾無效數(shù)據(jù)
NettyServerTestHandler
有了編碼器,發(fā)送可以直接發(fā)送實(shí)體類,有了解碼器我們可以直接用實(shí)體類接收數(shù)據(jù),因?yàn)榻獯a器里面往下傳遞的是過濾了消息頭的實(shí)體類
結(jié)果
一共接收到了50條消息,而且都是偶數(shù)消息,說明無效消息被過濾了,也沒有沾包和拆包
到此這篇關(guān)于Netty中序列化的作用及自定義協(xié)議詳解的文章就介紹到這了,更多相關(guān)Netty序列化及自定義協(xié)議內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot3+Vue3實(shí)現(xiàn)JWT登錄鑒權(quán)功能
JWT用于在網(wǎng)絡(luò)應(yīng)用間安全的傳遞消息,它以緊湊且自包含的方式,通過JSON對象在各方之間傳遞經(jīng)過驗(yàn)證的信息,這篇文章主要介紹了Springboot3+Vue3實(shí)現(xiàn)JWT登錄鑒權(quán)功能,需要的朋友可以參考下2025-03-03IDEA2023創(chuàng)建MavenWeb項(xiàng)目并搭建Servlet工程的全過程
Maven提供了大量不同類型的Archetype模板,通過它們可以幫助用戶快速的創(chuàng)建Java項(xiàng)目,這篇文章主要給大家介紹了關(guān)于IDEA2023創(chuàng)建MavenWeb項(xiàng)目并搭建Servlet工程的相關(guān)資料,需要的朋友可以參考下2023-10-10SpringBoot之使用Feign實(shí)現(xiàn)微服務(wù)間的交互
這篇文章主要介紹了SpringBoot中使用Feign實(shí)現(xiàn)微服務(wù)間的交互,對微服務(wù)這方面感興趣的小伙伴可以參考閱讀本文2023-03-03mybatis-plus主鍵id生成、字段自動填充的實(shí)現(xiàn)代碼
這篇文章主要介紹了mybatis-plus主鍵id生成、字段自動填充的實(shí)現(xiàn)代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12基于Spring的Maven項(xiàng)目實(shí)現(xiàn)發(fā)送郵件功能的示例
這篇文章主要介紹了基于Spring的Maven項(xiàng)目實(shí)現(xiàn)發(fā)送郵件功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03SpringBoot中@RestControllerAdvice注解實(shí)現(xiàn)全局異常處理類
這篇文章主要介紹了SpringBoot中@RestControllerAdvice注解全局異常處理類,springboot中使用@RestControllerAdvice注解,完成優(yōu)雅的全局異常處理類,可以針對所有異常類型先進(jìn)行通用處理后再對特定異常類型進(jìn)行不同的處理操作,需要的朋友可以參考下2024-01-01Spring線程池ThreadPoolExecutor配置并且得到任務(wù)執(zhí)行的結(jié)果
今天小編就為大家分享一篇關(guān)于Spring線程池ThreadPoolExecutor配置并且得到任務(wù)執(zhí)行的結(jié)果,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03