Netty 學習(三):通訊協定和編解碼

2022-09-15 18:01:02

Netty 學習(三):通訊協定和編解碼

作者: Grey

原文地址:

部落格園:Netty 學習(三):通訊協定和編解碼

CSDN:Netty 學習(三):通訊協定和編解碼

無論使用 Netty 還是原生 Socket 程式設計,都可以實現自定義的通訊協定。

所謂協定就是:使用者端和伺服器端商量好,每一個二進位制封包中的每一段位元組分別代表什麼含義的規則。

有了規則,在伺服器端和使用者端就可以通過這個設定好的規則進行二進位制和物件的轉換。

通訊協定格式可以參考如下格式

每個部分的說明如下

魔數:用來標識這個封包是否遵循我們設計的通訊協定,類似 Java 位元組碼開頭的4位元組:0xcafebabe

版本標識:用來標識這個協定是什麼版本,用於後續協定的升級

序列化演演算法:用於標識這個協定的封包使用什麼序列化演演算法,比如:JSON,XML等

指令:用於標識這個資料在收到後應該使用什麼處理邏輯。

資料長度&資料內容:不贅述

定好格式以後,

接下來我們可以約定雙方的序列化方法,這裡我們可以用 JSON 序列化/反序列化 為例,其他格式的類似。

使用 Gson 可以很方便將 JSON 字串和物件進行互轉:

    private static final Gson gson = new Gson();
 // 序列化
    public byte[] serialize(Object object) {
        return gson.toJson(object).getBytes(UTF_8);
    }
 // 反序列化
    public <T> T deserialize(Class<T> clazz, byte[] bytes) {
        return gson.fromJson(new String(bytes, UTF_8), clazz);
    }

實現了物件和位元組陣列的互轉以後,我們需要實現位元組陣列和 Netty 通訊載體 ByteBuf 的互轉,包括如下兩個方法

ByteBuf 編碼(封包) 

上述編碼方法需要做如下幾個事情

  1. 分配 ByteBuf (分配一塊記憶體區域,Netty 會直接建立一個堆外記憶體)

  2. 按照協定獲取封包對應的內容

  3. 嚴格按照協定規定的位元組數填充到 ByteBuf 中

封包 解碼(ByteBuf byteBuf)

上述解碼方法主要做如下幾件事情

  1. 校驗魔數

  2. 校驗版本號

  3. 如果嚴格按照規範傳輸的 ByteBuf,上述兩步校驗一定是通過的,可以直接跳過。

  4. 獲取序列化演演算法,指令和封包長度,並將資料內容轉換成位元組陣列

  5. 將位元組陣列轉換成對應的封包物件。

因為不同的封包內容有所不一樣,所以應該設定一個抽象類,由各個子類實現具體封包的內容。

package protocol;

import lombok.Data;

/**
 * 封包抽象類
 *
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2022/9/15
 * @since
 */
@Data
public abstract class Packet {
    /**
     * 協定版本
     */
    private Byte version = 1;

    /**
     * 指令,由子類實現
     *
     * @return
     */
    public abstract Byte getCommand();
}

對於一個具體的操作,比如登入操作,它需要的封包需要繼承並實現這個抽象類的抽象方法。

package protocol;

import lombok.Data;

import static protocol.Command.LOGIN_REQUEST;

/**
 * @author <a href="mailto:[email protected]">Grey</a>
 * @date 2022/9/15
 * @since
 */
@Data
public class LoginRequestPacket extends Packet {
    // 登入操作需要的資料內容包括如下三個
    private Integer userId;
    private String username;
    private String password;

    @Override
    public Byte getCommand() {
        return LOGIN_REQUEST;
    }
}

對於呼叫者來說,只需要使用LoginRequestPacket即可,無須關注其底層的編碼和解碼工作。虛擬碼如下

func() {
        LoginRequestPacket loginRequestPacket = new LoginRequestPacket();
        loginRequestPacket.setVersion(((byte) 1));
        loginRequestPacket.setUserId(123);
        loginRequestPacket.setUsername("zhangsan");
        loginRequestPacket.setPassword("password");
        // 編碼
        ByteBuf byteBuf = 封裝好的編解碼工具類.編碼(loginRequestPacket);
        // 解碼
        Packet decodedPacket = 封裝好的編解碼工具類.解碼(byteBuf); 
        // 序列化成我們需要的物件
        序列化和反序列化工具類.序列化(decodedPacket);
}

完整程式碼見:hello-netty

本文所有圖例見:processon: Netty學習筆記

更多內容見:Netty專欄

參考資料

跟閃電俠學 Netty:Netty 即時聊天實戰與底層原理

深度解析Netty原始碼