我在前面一篇隨筆《Socket開發框架之框架設計及分析》中,介紹了整個Socket開發框架的總體思路,對各個層次的基類進行了一些總結和抽象,已達到重用、簡化代碼的目的。本篇繼續分析其中重要的協議設計部分,對其中消息協議的設計,以及數據的拆包和封包進行了相關的介紹,使得我們在更高級別上更好利用Sock ...
我在前面一篇隨筆《Socket開發框架之框架設計及分析》中,介紹了整個Socket開發框架的總體思路,對各個層次的基類進行了一些總結和抽象,已達到重用、簡化代碼的目的。本篇繼續分析其中重要的協議設計部分,對其中消息協議的設計,以及數據的拆包和封包進行了相關的介紹,使得我們在更高級別上更好利用Socket的特性。
1、協議設計思路
對Socket傳輸消息的封裝和拆包,一般的Socket應用,多數採用基於順序位置和位元組長度的方式來確定相關的內容,這樣的處理方式可以很好減少數據大小,但是這些處理對我們分析複雜的協議內容,簡直是一場災難。對跟蹤解決過這樣協議的開發人員來說會很好理解其中的難處,協議位置一旦變化或者需要特殊的處理,就是很容易出錯的,而且大多數代碼充斥著很多位置的數值變數,分析和理解都是非常不便的。隨著網路技術的發展,有時候傳輸的數據稍大一點,損失一些帶寬來傳輸數據,但是能成倍提高開發程式的效率,是我們值得追求的目標。例如,目前Web API在各種設備大行其道,相對Socket消息來說,它本身在數據大小上不占優勢,但是開發的便利性和高效性,是眾所周知的。
借鑒了Web API的特點來考慮Socket消息的傳輸,如果對於整體的內容,Socket應用也使用一種比較靈活的消息格式,如JSON格式來傳輸數據,那麼我們可以很好的把消息封裝和消息拆包解析兩個部分,交給第三方的JSON解析器來進行,我們只需要關註具體的消息處理邏輯就可以了,而且對於協議的擴展,就如JSON一樣,可以自由靈活,這樣瞬間,整個世界都會很清靜了。
對於Socket消息的安全性和完整性,加密處理方面我們可以採用 RSA公鑰密碼系統。平臺通過發送平臺RSA公鑰消息向終端告知自己的RSA公鑰,終端回覆終端RSA公鑰消息,這樣平臺和終端的消息,就可以通過自身的私鑰加密,讓對方根據接收到的公鑰解密就可以了,雖然加密的數據長度會增加不少,但是對於安全性要求高的,採用這種方式也是很有必要的。
對於數據的完整性,傳統意義的CRC校驗碼其實沒有太多的用處了,因為我們的數據不會發生部分的丟失,而我們更應該關註的是數據是否被篡改過,這點我想到了微信公眾號API介面的設計,它們帶有一個安全簽名的加密字元串,也就是對其中內容進行同樣規則的加密處理,然後對比兩個簽名內容是否一致即可。不過對於非對稱的加密傳輸,這種數據完整性的校驗也可以不必要。
前面介紹了,我們可以參照Web API的方式,以JSON格式作為我們傳輸的內容,方便序列號和反序列化,這樣我們可以大大降低Socket協議的分析難度和出錯幾率,降低Socket開發難度並提高開發應用的速度。那麼我們應該如何設計這個格式呢?
首先我們需要為Socket消息,定義好開始標識和結束標識,中間部分就是整個通用消息的JSON內容。這樣,一條完整的Socket消息內容,除了開始和結束標識位外,剩餘部分是一個JSON格式的字元串數據。
我們準備根據需要,設計好整個JSON字元串的內容,而且最好設計的較為通用一些,這樣便於我們承載更多的數據信息。
2、協議設計分析和演化
參考微信的API傳遞消息的定義,我設計了下麵的消息格式,包括了送達用戶ID,發送用戶ID、消息類型、創建時間,以及一個通用的內容欄位,這個通用的欄位應該是另外一個消息實體的JSON字元串,這樣我們整個消息格式不用變化,但是具體的內容不同,我們把這個對象類稱之BaseMessage,常用欄位如下所示。
上面的Content欄位就是用來承載具體的消息數據的,它會根據不同的消息類型,傳送不同的內容的,而這些內容也是具體的實體類序列化為JSON字元串的,我們為了方便,也設計了這些類的基類,也就是Socket傳遞數據的實體類基類BaseEntity。
我們在不同的請求和應答消息,都繼承於它即可。我們為了方便讓它轉換為我們所需要的BaseMessage消息,為它增加一個MsgType協議類型的標識,同時增加PackData的方法,讓它把實體類轉換為JSON字元串。
例如我們一般情況下的請求Request和應答Response的消息對象,都是繼承自BaseEntity的,我們可以把這兩類消息對象放在不同的目錄下方便管理。
繼承關係示例如下所示。
其中子類都可以使用基類的PackData方法,直接序列號為JSON字元串即可,那個PacketData的函數主要就是用來組裝好待發送的對象BaseMessage的,函數代碼如下所示:
/// <summary> /// 封裝數據進行發送 /// </summary> /// <returns></returns> public BaseMessage PackData() { BaseMessage info = new BaseMessage() { MsgType = this.MsgType, Content = this.SerializeObject() }; return info; }
有時候我們需要根據請求的信息,用來構造返回的應答消息,因為需要把發送者ID和送達者ID逆反過來。
/// <summary> /// 封裝數據進行發送(複製請求部分數據) /// </summary> /// <returns></returns> public BaseMessage PackData(BaseMessage request) { BaseMessage info = new BaseMessage() { MsgType = this.MsgType, Content = this.SerializeObject(), CallbackID = request.CallbackID }; if(!string.IsNullOrEmpty(request.ToUserId)) { info.ToUserId = request.FromUserId; info.FromUserId = request.ToUserId; } return info; }
以登陸請求的數據實體對象介紹,它繼承自BaseEntity,同時指定好對應的消息類型即可。
/// <summary> /// 登陸請求消息實體 /// </summary> public class AuthRequest : BaseEntity { #region 欄位信息 /// <summary> /// 用戶帳號 /// </summary> public string UserId { get; set; } /// <summary> /// 用戶密碼 /// </summary> public string Password { get; set; } #endregion /// <summary> /// 預設構造函數 /// </summary> public AuthRequest() { this.MsgType = DataTypeKey.AuthRequest; } /// <summary> /// 參數化構造函數 /// </summary> /// <param name="userid">用戶帳號</param> /// <param name="password">用戶密碼</param> public AuthRequest(string userid, string password) : this() { this.UserId = userid; this.Password = password; } }
這樣我們的消息內容就很簡單,方便我們傳遞及處理了。
3、消息的接收和發送
前面我們介紹過了一些基類,包括Socket客戶端基類,和數據接收的基類設計,這些封裝能夠給我提供很好的便利性。
在上面的BaseSocketClient裡面,我們為了能夠解析不同協議的Socket消息,把它轉換為我們所需要的基類對象,那麼我們這裡引入一個解析器MessageSplitter,這個類主要的職責就是用來分析位元組數據,併進行整條消息的提取的。
因此我們把BaseSocketClient的類定義的代碼設計如下所示。
/// <summary> /// 基礎的Socket操作類,提供連接、斷開、接收和發送等相關操作。 /// </summary> /// <typeparam name="TSplitter">對應的消息解析類,繼承自MessageSplitter</typeparam> public class BaseSocketClient<TSplitter> where TSplitter : MessageSplitter, new()
MessageSplitter對象,給我們處理低層次的協議解析,前面介紹了我們除了協議頭和協議尾標識外,其餘部分就是一個JSON的,那麼它就需要根據這個規則來實現位元組數據到對象級別的轉換。
首先需要把位元組數據進行拆分,把它完整的一條數據加到列表裡面後續進行處理。
其中結尾部分,我們就是需要提取緩存的直接數據到一個具體的對象上了。
RawMessage msg = this.ConvertMessage(MsgBufferCache, from);
這個轉換的大概規則如下所示。
這樣我們在收到消息後,利用TSplitter對象來進行解析就可以了,如下所示就是對Socket消息的處理。
TSplitter splitter = new TSplitter(); splitter.InitParam(this.Socket, this.StartByte, this.EndByte);//指定分隔符,用來拆包 splitter.DataReceived += splitter_DataReceived;//如果有完整的包處理,那麼通過事件通知
數據接收並獲取一條消息的直接數據對象後,我們就進一步把直接對象轉換為具體的消息對象了
/// <summary> /// 消息分拆類收到消息事件 /// </summary> /// <param name="data">原始消息對象</param> void splitter_DataReceived(RawMessage data) { ReceivePackCount += 1;//增加收到的包數量 OnReadRaw(data); } /// <summary> /// 接收數據後的處理,可供子類重載 /// </summary> /// <param name="data">原始消息對象(包含原始的位元組數據)</param> protected virtual void OnReadRaw(RawMessage data) { //提供預設的包體處理:假設整個內容為Json的方式; //如果需要處理自定義的消息體,那麼需要在子類重寫OnReadMessage方法。 if (data != null && data.Buffer != null) { var json = EncodingGB2312.GetString(data.Buffer); var msg = JsonTools.DeserializeObject<BaseMessage>(json); OnReadMessage(msg);//給子類重載 } }
在更高一層的數據解析上面,我們就可以對對象級別的消息進行處理了
例如我們收到消息後,它本身解析為一個實體類BaseMessage的,那麼我們就可以利用BaseMessage的消息內容,也可以把它的Content內容轉換為對應的實體類進行處理,如下代碼所示是接收對象後的處理。
void TextMsgAnswer(BaseMessage message) { var msg = string.Format("來自【{0}】的消息:", message.FromUserId); var request = JsonTools.DeserializeObject<TextMsgRequest>(message.Content); if (request != null) { msg += string.Format("{0} {1}", request.Message, message.CreateTime.IntToDateTime()); } //MessageUtil.ShowTips(msg); Portal.gc.MainDialog.AppendMessage(msg); }
對於消息的發送處理,我們可以舉一個例子,如果客戶端登陸後,需要獲取線上用戶列表,那麼可以發送一個請求命令,那麼伺服器需要根據這個命令返回列表信息給終端,如下代碼所示。
/// <summary> /// 處理客戶端請求用戶列表的應答 /// </summary> /// <param name="data">具體的消息對象</param> private void UserListProcess(BaseMessage data) { CommonRequest request = JsonTools.DeserializeObject<CommonRequest>(data.Content); if (request != null) { Log.WriteInfo(string.Format("############\r\n{0}", data.SerializeObject())); List<CListItem> list = new List<CListItem>(); foreach(ClientOfShop client in Singleton<ShopClientManager>.Instance.LoginClientList.Values) { list.Add(new CListItem(client.Id, client.Id)); } UserListResponse response = new UserListResponse(list); Singleton<ShopClientManager>.Instance.AddSend(data.FromUserId, response.PackData(data), true); } }