回答网友的一个C#通讯的问题

事情是这样的

在QQ群中有位网友在解析通讯包时,用了很多字符串处理,出了一些问题。俺的建议是:通讯处理可以用byte[]、可以用stream、可以用结构体,尽量少用字符串。

有网友问完为啥用结构体?俺的回答时,实际上很多网络通讯的程序都在使用结构体来生成或者解析或解析通讯包,特别是c++ 和 Delphi ,实际上C#也有很多是这么用的。下面就是一个例子。

一个简单的通讯定义例子

通讯包定义:【Header】 + 【数据】 + 【Foot】

头和尾的定义如下:

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct Header  
    {
        public byte Verson; //版本号
        public UInt32 SequenceNo;//流水号
        public UInt32 Cmd;//命令
        public Int32 ParamInt1;//参数1
        public Int32 ParamInt2;//参数2
        public Int32 ParamInt3;//参数3
        public Int32 ParamInt4;//参数4
        public Int32 ParamInt5;//参数5 
        public int DataLen;//数据长度
    }
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct Foot
    {
        public UInt32 Timestamp;//时间戳
        public UInt16 ChkSum;//16位校验和 
    }

   //【Header】 + 【数据】 + 【Foot】
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct Header  
    {
        public byte Verson; //版本号
        public UInt32 SequenceNo;//流水号
        public UInt32 Cmd;//命令
        public Int32 ParamInt1;//参数1
        public Int32 ParamInt2;//参数2
        public Int32 ParamInt3;//参数3
        public Int32 ParamInt4;//参数4
        public Int32 ParamInt5;//参数5 
        public int DataLen;//数据长度
    }
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct Foot
    {
        public UInt32 Timestamp;//时间戳
        public UInt16 ChkSum;//16位校验和 
    }

数据部分由业务需求决定,可以是文本、json、xml、音视频等等。

数据生成和解析测试(方法一)

 200万次 数据生成和解析  大约0.1秒左右 (本文的所有测试为 64位 Release版本)。

数据的生成

下面的方法中最大的特点就是先分配内存 byte[] buff = new byte[Len]; 

然后用fixed 对头、尾进行赋值。

        private unsafe byte[]   MakeData(UInt32 SequenceNo, UInt32 Cmd, Int32 ParamInt1, Int32 ParamInt2, byte[] Data)
        {            
            int HeaderLen = sizeof(Header);
            int DataLen = Data.Length;
            int FootLen = sizeof(Foot);
            int Len = HeaderLen + DataLen + FootLen;
            byte[] buff = new byte[Len];
            fixed (byte* p = &buff[0])
            {
                Header* head = (Header*)p; 
                (*head).Verson = 1;
                (*head).SequenceNo = SequenceNo;
                (*head).Cmd = Cmd;
                (*head).ParamInt1 = ParamInt1;
                (*head).ParamInt2 = ParamInt2;
                (*head).DataLen = DataLen;
            }
            Array.Copy(Data, 0,buff, HeaderLen, DataLen);      
            fixed (byte* p = &buff[HeaderLen + DataLen])
            {
                Foot* foot = (Foot*)p;
                (*foot).Timestamp = 1;
                (*foot).ChkSum = 0xffff;
            }
            return buff;
        }

数据的解析

这里使用了一个文本做为数据进行测试。注释掉了  gb2312.GetString ,只测试数据的解析。

通过 Cmd = (*head).Cmd; 可以看出 ,数据定义中的成员可以直接访问,不需要一个一个的去解析数值。

服务端和客户端的结构体的定义一致,有非常多非常多的成员都没关系,直接访问就ok。早年间很多通讯文档就是这样的。通讯文档里有 C++或 Delphi的结构体的定义,开发时直接引入工程就可以了。有点像Protocol Buffers ,但比Protocol Buffers 更快。

        private unsafe void button1_Click(object sender, EventArgs e)
        {
            Encoding gb2312 = Encoding.GetEncoding("GB2312");
            byte[] Data = gb2312.GetBytes( "63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本");
            Stopwatch timer = new Stopwatch();
            timer.Start();
            for (int m = 0; m < 10000 * 200; m++)
            {
                //生成一个测试数据 
                byte[] buff = MakeData(1, 63, 1, 100, Data); 

                //从数据中读取数据
                int HeaderLen = sizeof(Header);
                int DataLen = 0;
                int FootLen = sizeof(Foot);
                UInt32 Cmd = 0;
                fixed (byte* p = &buff[0])
                {
                    Header* head = (Header*)p;
                    DataLen = (*head).DataLen;
                    Cmd = (*head).Cmd;
                }
                // string Text = gb2312.GetString(buff, HeaderLen, DataLen);// 检查文本
            }
            timer.Stop();
            MessageBox.Show("方法1: 200万次 数据生成和解析 耗时"+ timer.Elapsed.Duration().TotalSeconds.ToString());
        }

数据生成和解析测试(方法二)

可能有细心的同学看到了 方法一执行速度快 的原因了。无它,方法一用了 unsafe 。下面再讲一个无需 unsafe的方法。

好像速度慢了很多,那是俺还没有优化的原因。 不过,优化了应该也比方法一慢不少,毕竟方法一 unsafe了(这等于开挂了)。

数据的生成

一段平平淡淡的代码,没啥特点。 

        private   byte[] MakeData2(UInt32 SequenceNo, UInt32 Cmd, Int32 ParamInt1, Int32 ParamInt2, byte[] Data)
        {
            int HeaderLen = Marshal.SizeOf(typeof(Header));
            int DataLen = Data.Length;
            int FootLen = Marshal.SizeOf(typeof(Foot));
            int Len = HeaderLen + DataLen + FootLen;
            byte[] buff = new byte[Len];

            Header head;
            head.Verson = 1;
            head.SequenceNo = SequenceNo;
            head.Cmd = Cmd;
            head.ParamInt1 = ParamInt1;
            head.ParamInt2 = ParamInt2;
            head.ParamInt3 = 0;
            head.ParamInt4 = 0;
            head.ParamInt5 = 0;
            head.DataLen = DataLen; 

            IntPtr head_buffer = Marshal.AllocHGlobal(HeaderLen);
            Marshal.StructureToPtr(head, head_buffer, false);
            Marshal.Copy(head_buffer, buff, 0, HeaderLen);
            Marshal.FreeHGlobal(head_buffer);

            Array.Copy(Data, 0, buff, HeaderLen, DataLen);

            Foot foot;
            foot.Timestamp = 1;
            foot.ChkSum = 0xffff;
            IntPtr foot_buffer = Marshal.AllocHGlobal(FootLen);
            Marshal.StructureToPtr(foot, foot_buffer, false);
            Marshal.Copy(foot_buffer, buff, HeaderLen + DataLen, FootLen);
            Marshal.FreeHGlobal(foot_buffer);

            return buff;
        }

数据的解析

同样是一段平平淡淡的代码,没啥特别的。只是用了结构体进行数据的生成和解析。这种写法唯一的好处就是,结构体定义好了,一切就很简单,不管成员有多少。其实这个方法也不是特别慢,一般的场景  1.6秒处理 200万条数据 也可以了。因为真正的瓶颈不在这里,瓶颈在网络、业务、存储等等。

        private void button2_Click(object sender, EventArgs e)
        {
            Encoding gb2312 = Encoding.GetEncoding("GB2312");
            byte[] Data = gb2312.GetBytes("63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本");
             
            int HeaderLen = Marshal.SizeOf(typeof(Header));
            int FootLen = Marshal.SizeOf(typeof(Foot));
            
            Stopwatch timer = new Stopwatch();
            timer.Start();
            for (int m = 0; m < 10000 * 200; m++)
            {
                //生成一个测试数据 
                byte[] buff = MakeData2(1, 63, 1, 100, Data);

                IntPtr structPtr = Marshal.AllocHGlobal(HeaderLen);
                Marshal.Copy(buff, 0, structPtr, HeaderLen);
                Header head= Marshal.PtrToStructure<Header>(structPtr);
                Marshal.FreeHGlobal(structPtr);

                int DataLen = head.DataLen;
                UInt32 Cmd = head.Cmd;

                //string Text = gb2312.GetString(buff, HeaderLen, DataLen);// 检查文本
            }
            timer.Stop();
           
            MessageBox.Show("方法2: 200万次 数据生成和解析 耗时" + timer.Elapsed.Duration().TotalSeconds.ToString());
        }

另外

为啥大家都喜欢在定义中加个   public byte Verson; //版本号 ?

因为可以上下兼容。例如多版本的协议并行,客户端的版本可以是 1.3.23 、也可能是 2.2.4、也可能是 2.5.1。 服务端根据Verson 走不同结构体就ok 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月巴月巴白勺合鸟月半

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值