1. 程式人生 > >netty+Protobuf (整合二)

netty+Protobuf (整合二)

Protobuf 訊息設計方法

瘋狂創客圈 死磕Netty 系列之12 【部落格園 總入口

本文說明

本篇是 netty+Protobuf 實戰的第二篇,完成一個 基於Netty + Protobuf 實戰案例。

本篇簡單說明一下,例項中,設計Protobuf 訊息的大致原則和思路。

訊息的大致型別

網路通訊涉及到訊息的定義,不管是直接使用二進位制格式,還是 xml、json等字串格式。訊息都可以大體的分為3大訊息型別:

  • 請求訊息

  • 應答訊息

  • 命令訊息

    一般情況下,每個訊息還會包含一個序列號、和一個能夠唯一區分訊息型別的型別定義。

原則一:使用 enum定義訊息型別。

為每個系統都定義一個 HeadType 列舉。包含系統用到的所有訊息的列舉型別

enum HeadType
{
  Login_Request = 1;//登陸請求
  Login_Response = 2;//登入響應
  Logout_Request = 3;//退出請求
  Logout_Response = 4;
  Keepalive_Request = 5;//心跳請求ping;
  Keepalive_Response = 6;
  Message_Request = 7;//訊息請求;
  Message_Response = 8;//訊息回執;
  Message_Notification = 9;//通知訊息
}

原則二: 一個 protobuf message 對應一類訊息

會為每個具有訊息體的訊息定義一個對應的protobuf message。

例如Login_Request會有一個對應LoginRequest訊息。

下面是一個登陸請求的訊息例項。

/*登入資訊*/
// LoginRequest對應的HeadType為Login_Request
// 訊息名稱去掉下劃線,更加符合Java 的類名規範
message LoginRequest{
	required string uid = 1;        // 使用者唯一id
	required string deviceId = 2;     // 裝置ID
	required string token = 3;       // 使用者token
	optional uint32 platform = 4;      //客戶端平臺 windows、mac、android、ios、web
	optional string app_version = 5;    // APP版本號
}

原則三:應答訊息需要成功標記和應答序號

對於應答訊息,並非總是成功的,因此在應答訊息中還會包含另外2個欄位。

  • 一個用於描述應答是否成功,一個用於描述失敗時的字串資訊。

  • 對於有多個應答的訊息來說,可能會包含是否為最後一個應答訊息的標識——應答的序號(類似與網路資料包被分包以後,協議要合併時,需要知道分片在包中的具體位置)。

因此Response看起來是這樣:

/*聊天響應*/
message MessageResponse
{
    required bool result = 1; //true表示傳送成功,false表示傳送失敗
    required uint32 code = 2;	//錯誤碼
    required string info = 3;	//錯誤描述
    required uint32 expose = 4; //錯誤描述是否提示給使用者:1 提示;0 不提示
    required bool last_block = 5;
    required fixed32 block_index = 6;
}

原則四:編解碼從頂層訊息開始

最後我會定義一個大訊息,把所有的訊息型別,全部封裝在一起,讓後在通訊的時候都從頂層訊息開始編解碼。大訊息看起來想下面這樣。。

/*頂層訊息*/
//頂層訊息是一種巢狀訊息,嵌套了各種型別訊息
//內部的訊息型別,全部使用optional欄位
//根據訊息型別 type的值,最多隻有一個有效
message Message
{
 required HeadType type = 1; //訊息型別
 required fixed32 sequence = 2;//訊息系列號
 fixed32  session_id = 3;
 optional LoginRequest loginRequest = 4;
 optional LoginResponse loginResponse = 5;
 optional MessageRequest messageRequest = 6;
 optional MessageResponse messageResponse = 7;
 optional MessageNotification notification = 8;
}

原則五:TCP 訊息需要進行二進位制包裝

用於UDP的時候比較簡單,因為每個資料包就是一個獨立的Message訊息,可以直接解碼,或者編碼後直接傳送。

但是如果是使用於TCP的時候,由於涉及到粘包、拆包等處理,而且Message訊息裡面也沒有包含長度相關的欄位(不好處理),因此把Message編碼後的訊息嵌入另外一個二進位制訊息中。

使用4位元組訊息長度+Message(二進位制資料)+(2位元組CRC校驗(可選))

其中4位元組的內容,只包含Message的長度,不包含自身和CRC的長度。如果需要也可以包含,當要記得通訊雙方必須一致。

協議介面檔案完整 例項

下面是一個 為瘋狂創客圈 100W*100級 分散式 IM專案定義 google protobuf 的協議介面檔案

//定義protobuf的包名稱空間

option java_package = "com.crazymakercircle.chat.common.bean.msg";

// 訊息體名稱
option java_outer_classname = "ProtoMsg";


enum HeadType
{
  LOGIN_REQUEST = 1;//登陸請求
  LOGIN_RESPONSE = 2;//登入響應
  LOGOUT_REQUEST = 3;//退出請求
  LOGOUT_RESPONSE = 4;
  KEEPALIVE_REQUEST = 5;//心跳請求PING;
  KEEPALIVE_RESPONSE = 6;
  MESSAGE_REQUEST = 7;//訊息請求;
  MESSAGE_RESPONSE = 8;//訊息回執;
  MESSAGE_NOTIFICATION = 9;//通知訊息
}

/*登入資訊*/
// LoginRequest對應的HeadType為Login_Request
// 訊息名稱去掉下劃線,更加符合Java 的類名規範
message LoginRequest{
	required string uid = 1;        // 使用者唯一id
	required string deviceId = 2;     // 裝置ID
	required string token = 3;       // 使用者token
	optional uint32 platform = 4;      //客戶端平臺 windows、mac、android、ios、web
	optional string app_version = 5;    // APP版本號
}

//token說明: 賬號伺服器登入時生成的Token

/*登入響應*/
message LoginResponse{
    required bool  result = 1; //true 表示成功,false表示失敗
    required uint32 code = 2;	//錯誤碼
    required string info = 3;	//錯誤描述
    required uint32 expose = 4;	//錯誤描述是否提示給使用者:1 提示;0 不提示
    required string session_id = 5;		//sessionId
}



/*聊天訊息*/
message MessageRequest{
	 uint64 msg_id = 1;		//訊息id
	 string from = 2;		//傳送方uId
	 string to = 3;			//接收方uId
	 uint64 time = 4;		//時間戳(單位:毫秒)
	 required uint32 msg_type = 5;	//訊息型別  1:純文字  2:音訊 3:視訊 4:地理位置 5:其他
   required string session_id = 6;		//sessionId
   string content = 7;	//訊息內容
	 string url = 8;		//多媒體地址
	 string property = 9;	//附加屬性
	 string from_nick = 10;	//傳送者暱稱
	 optional string json = 11;		//附加的json串
}

/*聊天響應*/
message MessageResponse
{
    required bool result = 1; //true表示傳送成功,false表示傳送失敗
    required uint32 code = 2;	//錯誤碼
    required string info = 3;	//錯誤描述
    required uint32 expose = 4; //錯誤描述是否提示給使用者:1 提示;0 不提示
    required bool last_block = 5;
    required fixed32 block_index = 6;
}

/*通知訊息*/
message MessageNotification
{
 required uint32 msg_type = 1;	//通知型別 1 上線 2 下線 ...
 required bytes sender = 2;
 required string json = 3;
 required string timestamp = 4;
}

/*頂層訊息*/
//頂層訊息是一種巢狀訊息,嵌套了各種型別訊息
//內部的訊息型別,全部使用optional欄位
//根據訊息型別 type的值,最多隻有一個有效
message Message
{
 required HeadType type = 1; //訊息型別
 required uint64   sequence = 2;//訊息系列號
 required fixed32  session_id = 3;
 optional LoginRequest loginRequest = 4;
 optional LoginResponse loginResponse = 5;
 optional MessageRequest messageRequest = 6;
 optional MessageResponse messageResponse = 7;
 optional MessageNotification notification = 8;
}


// sequence 訊息系列號
// 主要用於Request和Response,Response的值必須和Request相同,使得傳送端可以進行事務匹配處理

參考文章:

我的Protobuf訊息設計原則


瘋狂創客圈 實戰計劃
  • Netty 億級流量 高併發 IM後臺 開源專案實戰
  • Netty 原始碼、原理、JAVA NIO 原理
  • Java 面試題 一網打盡
  • 瘋狂創客圈 【 部落格園 總入口 】