程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> Socket開發框架之數據傳輸協議,socket數據傳輸

Socket開發框架之數據傳輸協議,socket數據傳輸

編輯:C#入門知識

Socket開發框架之數據傳輸協議,socket數據傳輸


我在前面一篇隨筆《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);
            }
        }

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved