-
設計通信協定

- 首先,第一個字段是魔數,通常情況下為固定的幾個位元組(我們這邊規定為4個位元組)。 為什麼需要這個字段,而且還是一個固定的數?假設我們在伺服器上開了一個端口,比如 80 端口,如果沒有這個魔數,任何資料包傳遞到伺服器,伺服器都會根據自定義協定來進行處理,包括不符合自定義協定規範的資料包。例如,我們直接通過
來通路伺服器(預設為 80 端口), 服務端收到的是一個标準的 HTTP 協定資料包,但是它仍然會按照事先約定好的協定來處理 HTTP 協定,顯然,這是會解析出錯的。而有了這個魔數之後,服務端首先取出前面四個位元組進行比對,能夠在第一時間識别出這個資料包并非是遵循自定義協定的,也就是無效資料包,為了安全考慮可以直接關閉連接配接以節省資源。在 Java 的位元組碼的二進制檔案中,開頭的 4 個位元組為
http://伺服器ip
用來辨別這是個位元組碼檔案,亦是異曲同工之妙。
0xcafebabe
- 接下來一個位元組為版本号,通常情況下是預留字段,用于協定更新的時候用到,有點類似 TCP 協定中的一個字段辨別是 IPV4 協定還是 IPV6 協定,大多數情況下,這個字段是用不到的,不過為了協定能夠支援更新,我們還是先留着。
- 第三部分,序列化算法表示如何把 Java 對象轉換二進制資料以及二進制資料如何轉換回 Java 對象,比如 Java 自帶的序列化,json,hessian 等序列化方式。
- 第四部分的字段表示指令,關于指令相關的介紹,我們在前面已經讨論過,服務端或者用戶端每收到一種指令都會有相應的處理邏輯,這裡,我們用一個位元組來表示,最高支援256種指令,對于我們這個 IM 系統來說已經完全足夠了。
- 接下來的字段為資料部分的長度,占四個位元組。
- 最後一個部分為資料内容,每一種指令對應的資料是不一樣的,比如登入的時候需要使用者名密碼,收消息的時候需要使用者辨別和具體消息内容等等。
傳輸的資料示例:
-
編碼:
public ByteBuf encode() {
// 1. 建立 ByteBuf 對象
//傳回适配 io 讀寫相關的記憶體,它會盡可能建立一個直接記憶體,直接記憶體可以了解為不受 jvm 堆管理的記憶體空間,寫到 IO 緩沖區的效果更高。
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.ioBuffer();
// 2. 序列化 Java 對象
byte[] bytes = "傳輸的資料".getBytes();
// 3. 實際編碼過程
byteBuf.writeInt(1);//魔數,4個位元組
byteBuf.writeByte(1);//版本号 1個位元組
byteBuf.writeByte(1);//序列化算法 1個位元組
byteBuf.writeByte(1);//指令 1個位元組
byteBuf.writeInt(bytes.length);//資料部分的長度 4個位元組
byteBuf.writeBytes(bytes);//資料内容
return byteBuf;
}
-
解碼:
public void decode(ByteBuf byteBuf) {
// 假定 decode 方法傳遞進來的 ByteBuf 已經是合法的,跳過 magic number
byteBuf.skipBytes(4);
// 跳過版本号
byteBuf.skipBytes(1);
// 序列化算法辨別
byte serializeAlgorithm = byteBuf.readByte();
// 指令
byte command = byteBuf.readByte();
// 資料包長度
int length = byteBuf.readInt();
System.out.println("資料包長度="+length);
byte[] bytes = new byte[length];
byteBuf.readBytes(bytes);
System.out.println("資料:"+new String(bytes));
}