• 分享套接字数据包序列化与反序列化方法


    分享套接字数据包序列化与反序列化方法

    简单说一下,本文不涉及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 版本号 int 当前消息版本号,允许程序中消息存在多个版本,用于版本迭代

    包含这三个字段:消息标识、名称、版本号,唯一确定消息对象。

    包体

    序号 字段名 数据类型 备注
    1 字段1 数据类型 字段1
    2 字段2 数据类型 字段2

    包体直接定义字段信息,就像定义类属性一样。

    另包头与包体中数据类型定义如下:

    数据包字段类型定义

    序号 数据类型 备注
    1 int 4个字节的整型值
    2 string 组成格式:字符串实际值字节长度(2个字节)+字符串实际值byte
    3 char 单字节值
    4 列表 组成格式:4个字节列表长度+列表实际数据值byte
    5 字典 同上,具体看源码

    其他数据类型类似,复杂数据类型使用4个字节的值字节长度+实际值byte。

    给一个测试数据包

    序号 字段名 数据类型 备注
    1 消息标识 int 取值:0x4A534604
    2 消息名称 string 三国信息,取值:"ThreeCountries"
    3 版本号 int 取值:1
    4 编号 int 给三国一个编号吧,取值:1
    5 国名 string 取值:"蜀国"
    6 皇帝 string 取值:"刘备"
    7 大将个数 int 5
    8 大将1编号 int 取值:1
    9 大将1名字 string 取值:"张飞"
    10 大将1备注 string 取值:"三板斧"
    11 大将2编号 int 取值:2
    12 大将2名字 string 取值:"关羽"
    13 大将2备注 string 取值:"青龙偃月刀"
    14 大将3编号 int 取值:3
    15 大将3名字 string 取值:"赵云"
    16 大将3备注 string 取值:"很猛的"
    17 大将4编号 int 取值:4
    18 大将4名字 string 取值:"马超"
    19 大将4备注 string 取值:"强"
    20 大将5编号 int 取值:5
    21 大将5名字 string 取值:"黄忠"
    22 大将5备注 string 取值:"老当益壮"

    大致理解下:

    • 前三个字段是包体:用于标识整个数据包,便于包体解析;
    • 后面的包体,简单说就是三国中的国家信息简介,前三个字段为三国中的一个国家基本信息:编号、国名、皇帝,后面是该国家大将信息列表,每个大将有编号、名称、备注等。

    定义数据对象

    根据数据包定义,我们可以很快定义类进行使用,不管你是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

    Dotnet9微信公众号

  • 相关阅读:
    TF-IDF
    3.路径模板两张表设计
    6.订单支付回调接口
    5.创建订单并生成支付链接接口
    5.使用ES代替whoosh全文检索
    4.docker基本使用
    3.ubuntu安装docker
    2.课程全文检索接口
    1.搜索引擎工作原理
    7.视频播放页面接口开发
  • 原文地址:https://www.cnblogs.com/Dotnet9-com/p/13851895.html
Copyright © 2020-2023  润新知