天天看點

分享套接字資料包序列化與反序列化方法

分享套接字資料包序列化與反序列化方法

簡單說一下,本文不涉及Socket的連接配接、資料接收,隻是對資料包(byte[])的序列化和反序列化方法的封裝介紹。

本文目錄

  1. 本文背景
  2. 一般操作
  3. 本文操作
  4. 總結

1.本文背景

經常做C/S,用戶端與服務端通信基本是TCP/UDP通信,套接字用得飛起。

比如我們有一個系統,這個系統又分幾個系統子子產品程序:

  1. C++服務端
  2. Android 用戶端
  3. iOS 用戶端
  4. WPF桌面管理端

    ......

幾個子產品之間通過TCP或者UDP通信,資料包解析與組裝是正常操作,我們定義資料包格式如下:

一個資料包包含標頭和包體,定義如下:

標頭

序号 字段名 資料類型 備注
1 消息辨別 int 用于辨別資料包是否合法
2 名稱 string 目前消息名稱,用于辨別資料包類型
3 版本号 目前消息版本号,允許程式中消息存在多個版本,用于版本疊代

包含這三個字段:消息辨別、名稱、版本号,唯一确定消息對象。

包體

字段1
字段2

包體直接定義字段資訊,就像定義類屬性一樣。

另標頭與包體中資料類型定義如下:

資料包字段類型定義

4個位元組的整型值
組成格式:字元串實際值位元組長度(2個位元組)+字元串實際值byte
char 單位元組值
4 清單 組成格式:4個位元組清單長度+清單實際資料值byte
5 字典 同上,具體看源碼

其他資料類型類似,複雜資料類型使用4個位元組的值位元組長度+實際值byte。

給一個測試資料包

取值:0x4A534604
消息名稱 三國資訊,取值:"ThreeCountries"
取值:1
編号 給三國一個編号吧,取值:1
國名 取值:"蜀國"
6 皇帝 取值:"劉備"
7 大将個數
8 大将1編号
9 大将1名字 取值:"張飛"
10 大将1備注 取值:"三闆斧"
11 大将2編号 取值:2
12 大将2名字 取值:"關羽"
13 大将2備注 取值:"青龍偃月刀"
14 大将3編号 取值:3
15 大将3名字 取值:"趙雲"
16 大将3備注 取值:"很猛的"
17 大将4編号 取值:4
18 大将4名字 取值:"馬超"
19 大将4備注 取值:"強"
20 大将5編号 取值:5
21 大将5名字 取值:"黃忠"
22 大将5備注 取值:"老當益壯"

大緻了解下:

  • 前三個字段是包體:用于辨別整個資料包,便于包體解析;
  • 後面的包體,簡單說就是三國中的國家資訊簡介,前三個字段為三國中的一個國家基本資訊:編号、國名、皇帝,後面是該國家大将資訊清單,每個大将有編号、名稱、備注等。

定義資料對象

根據資料包定義,我們可以很快定義類進行使用,不管你是C++還是Java。下面是我用C#寫的對應類,用于序列化與反序列化使用:

/// <summary>
/// 三國
/// </summary>
public class ThreeCountries
{
    /// <summary>
    /// 擷取或者設定 ID
    /// </summary>
    public int ID { get; set; }

    /// <summary>
    /// 擷取或者設定 國名
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 擷取或者設定 皇帝
    /// </summary>
    public string Emperor { get; set; }

    /// <summary>
    /// 擷取或者設定 所選課程清單
    /// </summary>
    public List<FamousGeneral> Courses { get; set; }

    public override string ToString()
    {
        return $"三國之一{ID}:{Name}皇帝{Emperor},有 {Courses.Count}名大将";
    }
}

/// <summary>
/// 三國名将
/// </summary>
public class FamousGeneral
{
    /// <summary>
    /// 擷取或者設定 編号
    /// </summary>
    public int ID { get; set; }

    /// <summary>
    /// 擷取或者設定 名字
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 擷取或者設定 描述
    /// </summary>
    public string Memo { get; set; }

    public override string ToString()
    {
        return $"{ID}:{Name}=>{Memo}";
    }
}
           

對于上面給的資料包你怎麼序列化及反序列化?轉換成資料如下,下節接着讨論

ThreeCountries shuKingdom = new ThreeCountries
{
    ID = 1,
    Name = "蜀國",
    Emperor = "劉備",
    Courses = new System.Collections.Generic.List<FamousGeneral>
    {
        new FamousGeneral{ ID=1,Name="張飛",Memo="三闆斧"},
        new FamousGeneral{ ID=2,Name="關羽",Memo="青龍偃月刀"},
        new FamousGeneral{ ID=3,Name="趙雲",Memo="很猛的"},
        new FamousGeneral{ ID=3,Name="馬超",Memo="強"},
        new FamousGeneral{ ID=3,Name="黃忠",Memo="老當益壯"},
    }
};
           

2. 正常操作

序列化

代碼太繁瑣,我就寫個不正規的僞代碼吧

定義一個byte數組;
一、寫標頭
1、寫入4位元組的消息辨別:0x4A534604
計算消息對象名稱字元串“ThreeCountries”長度,及轉換字元串為byte數組
2、寫入2位元組的bytes數組長度,寫入實際的byte數組值
3、寫入4位元組的消息版本号
二、寫包體
4、寫入4位元組的大将個數
循環每個大将資訊,依次寫入
5、寫入大将1編号
6、寫入大将1名稱
7、寫入大獎1備注
8、寫入大将2編号
9、寫入大将3名稱
10、寫入大獎4備注
...寫吐了,省略号
           

反序列化

不想寫了,累
           

正常操作

定義一個序列化接口,每個網絡對象實作其中的序列化與反序列化接口

public interface ISerializeInterface
{
  byte[] Serialize<T>(T t);
  T Deserialize<T>(byte[] arr);
}

public class ThreeCountries : ISerializeInterface
{
  public byte[] Serialize<T>(T t)
  {
    // 将上面的序列化代碼寫在這
  }
  
  T Deserialize<T>(byte[] arr)
  {
    // 将上面的反序列化代碼寫在這,不好意思我沒寫
  }
}
           

3. 本文操作

寫了半天的Demo,文章可能就寫的有點水了,我估計讀者也不會仔細看代碼,直接去Github check項目去了,哈哈。

我還是簡單說說吧,實作很簡單,定義一些特性,下面紅框裡的代碼檔案:

分享套接字資料包序列化與反序列化方法

使用很簡單,在上面的資料類上加上特性,改動不多,看下面代碼:

/// <summary>
/// 三國
/// </summary>
[NetObject(Name = "ThreeCountries", Version = 1)]
public class ThreeCountries
{
    /// <summary>
    /// 擷取或者設定 ID
    /// </summary>
    [NetObjectProperty(ID = 1)]
    public int ID { get; set; }

    /// <summary>
    /// 擷取或者設定 國名
    /// </summary>
    [NetObjectProperty(ID = 2)]
    public string Name { get; set; }

    /// <summary>
    /// 擷取或者設定 皇帝
    /// </summary>
    [NetObjectProperty(ID = 3)]
    public string Emperor { get; set; }

    /// <summary>
    /// 擷取或者設定 所選課程清單
    /// </summary>
    [NetObjectProperty(ID = 4)]
    public List<FamousGeneral> Courses { get; set; }

    public static NetObjectAttribute CurrentObject = null;

    static ThreeCountries()
    {
        CurrentObject = NetObjectSerializeHelper.GetAttribute<ThreeCountries, NetObjectAttribute>(default(ThreeCountries));
    }

    public override string ToString()
    {
        return $"三國之一{ID}:{Name}皇帝{Emperor},有 {Courses.Count}名大将";
    }
}

/// <summary>
/// 三國名将
/// </summary>
public class FamousGeneral
{
    /// <summary>
    /// 擷取或者設定 編号
    /// </summary>
    [NetObjectProperty(ID = 1)]
    public int ID { get; set; }

    /// <summary>
    /// 擷取或者設定 名字
    /// </summary>
    [NetObjectProperty(ID = 2)]
    public string Name { get; set; }

    /// <summary>
    /// 擷取或者設定 描述
    /// </summary>
    [NetObjectProperty(ID = 3)]
    public string Memo { get; set; }

    public override string ToString()
    {
        return $"{ID}:{Name}=>{Memo}";
    }
}
           

仔細看的話,隻在外層類(ThreeCountries)上加了NetObject特性,和屬性上加了NetObjectProperty特性,分别辨別消息名稱、版本号及每個屬性的序列化與反序列化順序即可,類中使用的子對象Courses屬性,也隻需要加屬性特性即可,如上。

下面添加單元測試,并且測試通過:

分享套接字資料包序列化與反序列化方法

4. 總結

用這套代碼(demo,有所改變,但也差不多),完成了幾個類似的項目,每次資料通信聯調、測試問題,C++和java的同僚找我時,我就說:

"你先看你自己資料包的序列化和反序列化代碼有沒有問題,我這不會出問題的,完全按資料包格式轉的。"

剛開始還在那鬧,後面定位幾次問題後,類似的問題他們就沒再找我了,偷笑中。

源碼:見開源項目TerminalMACS。

原文連結:https://dotnet9.com/16583.html

歡迎關注我的微信公衆号:Dotnet9

時間如流水,隻能流去不流回。

c#