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

来源:http://www.cnblogs.com/wuhuacong/archive/2016/03/26/5321467.html
-Advertisement-
Play Games

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在Nginx的插件模塊中有一個模塊stub_status可以監控Nginx的一些狀態信息,預設安裝可能沒有這個模塊,手動編譯的時候加一下即可。 1. 模塊安裝 先使用命令查看是否已經安裝這個模塊: 如果已經安裝,會在顯示的信息中包含 --with-http_stub_status_module信息。 ...
  • 51 Windows 無法找到網路路徑。請確認網路路徑正確並且目標電腦不忙或已關閉。如果 Windows 仍然無法找到網路路徑,請與網路管理員聯繫。 52 由於網路上有重名,沒有連接。請到“控制面板”中的“系統”更改電腦名,然後重試。 53 找不到網路路徑。 54 網路很忙。 55 指定的網路資 ...
  • ID 類型 來 源 代 表 的 意 義 舉 例 解 釋 ...
  • 一、進程/線程 進程:系統進行資源分配和調度的一個獨立單位。(存資源) 線程:CPU調度和分派的基本單位。(執行) 一個進程可以有多個線程,一個線程可與同屬一個進程的其他線程共用進程所擁有的全部資源。 場景:超市。 /// <summary> /// 單線程 /// </summary> /// < ...
  • 大家在用三層架構做開發的時候,是否有使用介面,使用介面的時候是否有類似這樣的代碼: 然後每個每個表都有一個這樣的介面代碼,對比之後發現,這樣的代碼是不是很多重覆呢?那麼有什麼好的辦法可以減少這樣的重覆代碼??? 我想到的是泛型,介面同樣可以泛型,看下麵這張圖,IT_admin,IT_advs,IT_ ...
  • 1.//彈出對話框.點擊轉向指定頁面 2.//彈出對話框 3.//刪除文件 4.//綁定下拉列表框datalist 5.//時間去秒顯示 6.//標題帶鏈接 7.//修改轉向 8.//彈出確定按鈕 9.//輸出數據格式化 "{0:F2}" 是格式 F2表示小數點後剩兩位 10.//提取動態網頁內容 ...
  • 請求如何進入ASP.NET MVC框架 thx@4littleProgrammer url:http://www.cnblogs.com/4littleProgrammer/p/5317058.html ...
  • 環境:VS2013+MVC5+IIS EXPRESS 問題:如果從Asp.net Web遷移到MVC,可能會遇到需要使原來的鏈接(如http://localhost:12345/old/library.html)可以繼續訪問的情況,而預設情況下,MVC對於html尾碼是不經過路由的,直接給你一個40 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...