【正文】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相同,使得發送端可以進行事務匹配處理
參考文章:
瘋狂創客圈 實戰計划
Netty 億級流量 高並發 IM后台 開源項目實戰
Netty 源碼、原理、JAVA NIO 原理
Java 面試題 一網打盡
瘋狂創客圈 【 博客園 總入口 】
