分享套接字資料包序列化與反序列化方法
簡單說一下,本文不涉及Socket的連接配接、資料接收,隻是對資料包(byte[])的序列化和反序列化方法的封裝介紹。
本文目錄
- 本文背景
- 一般操作
- 本文操作
- 總結
1.本文背景
經常做C/S,用戶端與服務端通信基本是TCP/UDP通信,套接字用得飛起。
比如我們有一個系統,這個系統又分幾個系統子子產品程序:
- C++服務端
- Android 用戶端
- iOS 用戶端
-
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
時間如流水,隻能流去不流回。