分享套接字数据包序列化与反序列化方法
简单说一下,本文不涉及Socket的连接、数据接收,只是对数据包(byte[])的序列化和反序列化方法的封装介绍。
本文目录
本文背景
一般操作
本文操作
总结
1.本文背景
经常做C/S,客户端与服务端通信基本是TCP/UDP通信,套接字用得飞起。
比如我们有一个系统,这个系统又分几个系统子模块进程:
C++服务端
Android 客户端
iOS 客户端
WPF桌面管理端
......
几个模块之间通过TCP或者UDP通信,数据包解析与组装是常规操作,我们定义数据包格式如下:
一个数据包包含包头和包体,定义如下:
包头
包体
包体直接定义字段信息,就像定义类属性一样。
另包头与包体中数据类型定义如下:
数据包字段类型定义
其他数据类型类似,复杂数据类型使用4个字节的值字节长度+实际值byte。
给一个测试数据包
大致理解下:
前三个字段是包体:用于标识整个数据包,便于包体解析;
后面的包体,简单说就是三国中的国家信息简介,前三个字段为三国中的一个国家基本信息:编号、国名、皇帝,后面是该国家大将信息列表,每个大将有编号、名称、备注等。
定义数据对象
根据数据包定义,我们可以很快定义类进行使用,不管你是C++还是Java。下面是我用C#写的对应类,用于序列化与反序列化使用:
///<summary>
///三国
///</summary>
publicclassThreeCountries
{
///<summary>
///获取或者设置 ID
///</summary>
publicintID {get;set; }
///<summary>
///获取或者设置 国名
///</summary>
publicstringName {get;set; }
///<summary>
///获取或者设置 皇帝
///</summary>
publicstringEmperor {get;set; }
///<summary>
///获取或者设置 所选课程列表
///</summary>
publicList Courses {get;set; }
publicoverridestringToString()
{
return$"三国之一{ID}:{Name}皇帝{Emperor},有{Courses.Count}名大将";
}
}
///<summary>
///三国名将
///</summary>
publicclassFamousGeneral
{
///<summary>
///获取或者设置 编号
///</summary>
publicintID {get;set; }
///<summary>
///获取或者设置 名字
///</summary>
publicstringName {get;set; }
///<summary>
///获取或者设置 描述
///</summary>
publicstringMemo {get;set; }
publicoverridestringToString()
{
return$"{ID}:{Name}=>{Memo}";
}
}
对于上面给的数据包你怎么序列化及反序列化?转换成数据如下,下节接着讨论
ThreeCountries shuKingdom =newThreeCountries
{
ID =1,
Name ="蜀国",
Emperor ="刘备",
Courses =newSystem.Collections.Generic.List
{
newFamousGeneral{ ID=1,Name="张飞",Memo="三板斧"},
newFamousGeneral{ ID=2,Name="关羽",Memo="青龙偃月刀"},
newFamousGeneral{ ID=3,Name="赵云",Memo="很猛的"},
newFamousGeneral{ ID=3,Name="马超",Memo="强"},
newFamousGeneral{ 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 Deserialize(byte[] arr);
}
public class ThreeCountries : ISerializeInterface
{
public byte[] Serialize(T t)
{
// 将上面的序列化代码写在这
}
T Deserialize(byte[] arr)
{
// 将上面的反序列化代码写在这,不好意思我没写
}
}
3. 本文操作
写了半天的Demo,文章可能就写的有点水了,我估计读者也不会仔细看代码,直接去Github check项目去了,哈哈。
我还是简单说说吧,实现很简单,定义一些特性,下面红框里的代码文件:
序列化特性及帮助类
使用很简单,在上面的数据类上加上特性,改动不多,看下面代码:
///<summary>
///三国
///</summary>
[NetObject(Name ="ThreeCountries", Version = 1)]
publicclassThreeCountries
{
///<summary>
///获取或者设置 ID
///</summary>
[NetObjectProperty(ID = 1)]
publicintID {get;set; }
///<summary>
///获取或者设置 国名
///</summary>
[NetObjectProperty(ID = 2)]
publicstringName {get;set; }
///<summary>
///获取或者设置 皇帝
///</summary>
[NetObjectProperty(ID = 3)]
publicstringEmperor {get;set; }
///<summary>
///获取或者设置 所选课程列表
///</summary>
[NetObjectProperty(ID = 4)]
publicList Courses {get;set; }
publicstaticNetObjectAttribute CurrentObject =null;
staticThreeCountries()
{
CurrentObject = NetObjectSerializeHelper.GetAttribute(default(ThreeCountries));
}
publicoverridestringToString()
{
return$"三国之一{ID}:{Name}皇帝{Emperor},有{Courses.Count}名大将";
}
}
///<summary>
///三国名将
///</summary>
publicclassFamousGeneral
{
///<summary>
///获取或者设置 编号
///</summary>
[NetObjectProperty(ID = 1)]
publicintID {get;set; }
///<summary>
///获取或者设置 名字
///</summary>
[NetObjectProperty(ID = 2)]
publicstringName {get;set; }
///<summary>
///获取或者设置 描述
///</summary>
[NetObjectProperty(ID = 3)]
publicstringMemo {get;set; }
publicoverridestringToString()
{
return$"{ID}:{Name}=>{Memo}";
}
}
仔细看的话,只在外层类(ThreeCountries)上加了NetObject特性,和属性上加了NetObjectProperty特性,分别标识消息名称、版本号及每个属性的序列化与反序列化顺序即可,类中使用的子对象Courses属性,也只需要加属性特性即可,如上。
下面添加单元测试,并且测试通过:
单元测试通过
4. 总结
用这套代码(demo,有所改变,但也差不多),完成了几个类似的项目,每次数据通信联调、测试问题,C++和java的同事找我时,我就说:
"你先看你自己数据包的序列化和反序列化代码有没有问题,我这不会出问题的,完全按数据包格式转的。"
刚开始还在那闹,后面定位几次问题后,类似的问题他们就没再找我了,偷笑中。
源码:见开源项目TerminalMACS。