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

語言: CN / TW / HK

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原始碼