轻量级通信引擎StriveEngine —— C/S通信demo(2) —— 使用二进制协议 (附源码)

在网络上,交互的双方基于TCP或UDP进行通信,通信协议的格式通常分为两类:文本消息、二进制消息。

文本协议相对简单,通常使用一个特殊的标记符作为一个消息的结束。

二进制协议,通常是由消息头(Header)和消息体(Body)构成的,消息头的长度固定,而且,通过解析消息头,可以知道消息体的长度。如此,我们便可以从网络流中解析出一个个完整的二进制消息。

两种类型的协议格式各有优劣:文本协议直观、容易理解,但是在文本消息中很难嵌入二进制数据,比如嵌入一张图片;而二进制协议的优缺点刚刚相反。

在 轻量级通信引擎StriveEngine —— C/S通信demo(附源码)一文中,我们演示了如何使用了相对简单的文本协议,这篇文章我们将构建一个使用二进制消息进行通信的Demo。本Demo所做的事情是:客户端提交运算请求给服务端,服务端处理后,将结果返回给客户端。demo中定义消息头固定为8个字节:前四个字节为一个int,其值表示消息体的长度;后四个字节也是一个int,其值表示消息的类型。

1.Demo简介

该Demo总共包括三个项目:

(1)StriveEngine.BinaryDemoServer:基于StriveEngine开发的二进制通信服务端,处理来自客户端的请求并返回结果。

(2)StriveEngine.BinaryDemo:基于StriveEngine开发的二进制通信客户端,提交用户请求,并显示处理结果。

(3)StriveEngine.BinaryDemoCore:用于定义客户端和服务端都要用到的公共的消息类型和消息协议的基础程序集。

Demo运行起来后的截图如下所示:

2.消息头

首先,我们按照前面的约定,定义消息头MessageHead。

    public class MessageHead
    {
        public const int HeadLength = 8;

        public MessageHead() { }
        public MessageHead(int bodyLen, int msgType)
        {
            this.bodyLength = bodyLen;
            this.messageType = msgType;
        }

        private int bodyLength;
        /// <summary>
              /// 消息体长度
        /// </summary>
              public int BodyLength
        {
            get { return bodyLength; }
            set { bodyLength = value; }
        }

        private int messageType;
        /// <summary>
              /// 消息类型
        /// </summary>
              public int MessageType
        {
            get { return messageType; }
            set { messageType = value; }
        }

        public byte[] ToStream()
        {
            byte[] buff = new byte[MessageHead.HeadLength];
            byte[] bodyLenBuff = BitConverter.GetBytes(this.bodyLength) ;
            byte[] msgTypeBuff = BitConverter.GetBytes(this.messageType) ;
            Buffer.BlockCopy(bodyLenBuff,0,buff,0,bodyLenBuff.Length) ;
            Buffer.BlockCopy(msgTypeBuff,0,buff,4,msgTypeBuff.Length) ;
            return buff;
        }
    }

消息头由两个int构成,正好是8个字节。而且在消息头的定义中增加了ToStream方法,用于将消息头序列化为字节数组。

通过ToStream方法,我们已经可以对消息转化为流(即所谓的序列化)的过程窥见一斑了,基本就是操作分配空间、设置偏移、拷贝字节等。

3.消息类型

根据业务需求,需要定义客户端与服务器之间通信消息的类型MessageType。

    public static class MessageType
    {
        /// <summary>
        /// 加法请求
        /// </summary>
        public const int Add = 0;

        /// <summary>
        /// 乘法请求
        /// </summary
        public const int Multiple = 1;

        /// <summary>
        /// 运算结果回复
        /// </summary
        public const int Result = 2;
    }

消息类型有两个请求类型,一个回复类型。请注意消息的方向,Add和Multiple类型的消息是由客户端发给服务器的,而Result类型的消息则是服务器发给客户端的。

4.消息体

一般的消息都由消息体(MessageBody),用于封装具体的业务数据。当然,也有些消息只有消息头,没有消息体的。比如,心跳消息,设计时,我们只需要使用一个消息类型来表示它是一个心跳就可以了,不需要使用消息体。

本demo中,三种类型的消息都需要消息体来封装业务数据,所以,demo中本应该定义了3个消息体,但demo中实际上只定义了两个:RequestContract、ResponseContract。这是因为Add和Multiple类型的消息公用的是同一个消息体RequestContract。

    [Serializable]
    public class RequestContract
    {
        public RequestContract() { }
        public RequestContract(int num1, int num2)
        {
            this.number1 = num1;
            this.number2 = num2;
        }

        private int number1;
        /// <summary>
        /// 运算的第一个数。
        /// </summary>
        public int Number1
        {
            get { return number1; }
            set { number1 = value; }
        }

        private int number2;
        /// <summary>
        /// 运算的第二个数。
        /// </summary>
        public int Number2
        {
            get { return number2; }
            set { number2 = value; }
        }
    }

    [Serializable]
    public class ResponseContract
    {
        public ResponseContract() { }
        public ResponseContract(int num1, int num2 ,string opType,int res)
        {
            this.number1 = num1;
            this.number2 = num2;
            this.operationType = opType;
            this.result = res;
        }

        private int number1;
        /// <summary>
        /// 运算的第一个数。
        /// </summary>
        public int Number1
        {
            get { return number1; }
            set { number1 = value; }
        }

        private int number2;
        /// <summary>
        /// 运算的第二个数。
        /// </summary>
        public int Number2
        {
            get { return number2; }
            set { number2 = value; }
        }

        private string operationType;
        /// <summary>
        /// 运算类型。
        /// </summary>
        public string OperationType
        {
            get { return operationType; }
            set { operationType = value; }
        }

        private int result;
        /// <summary>
        /// 运算结果。
        /// </summary>
        public int Result
        {
            get { return result; }
            set { result = value; }
        }
    }

关于消息体的序列化,demo采用了.NET自带的序列化器的简单封装(即SerializeHelper类)。当然,如果客户端不是.NET平台,序列化器不一样,那就必须像消息头那样一个字段一个字段就构造消息体了。

5.服务端

关于StriveEngine使用的部分,在 轻量级通信引擎StriveEngine —— C/S通信demo(附源码)一文中已有说明,我们这里就不重复了。我们直接关注业务处理部分:

void tcpServerEngine_MessageReceived(IPEndPoint client, byte[] bMsg)
{
    //获取消息类型
    int msgType = BitConverter.ToInt32(bMsg, 4);//消息类型是 从offset=4处开始 的一个整数
    //解析消息体
    RequestContract request = (RequestContract)SerializeHelper.DeserializeBytes(bMsg, MessageHead.HeadLength, bMsg.Length - MessageHead.HeadLength);
    int result = 0;
    string operationType = "";
    if (msgType == MessageType.Add)
    {
        result = request.Number1 + request.Number2;
        operationType = "加法";
    }
    else if (msgType == MessageType.Multiple)
    {
        result = request.Number1 * request.Number2;
        operationType = "乘法";
    }
    else
    {
        operationType = "错误的操作类型";
    }

    //显示请求
    string record = string.Format("请求类型:{0},操作数1:{1},操作数2:{2}", operationType, request.Number1 , request.Number2);
    this.ShowClientMsg(client, record);

    //回复消息体
    ResponseContract response = new ResponseContract(request.Number1, request.Number2, operationType, result);
    byte[] bReponse = SerializeHelper.SerializeObject(response);
    //回复消息头
    MessageHead head = new MessageHead(bReponse.Length, MessageType.Result);
    byte[] bHead = head.ToStream();

    //构建回复消息
    byte[] resMessage = new byte[bHead.Length + bReponse.Length];
    Buffer.BlockCopy(bHead, 0, resMessage, 0, bHead.Length);
    Buffer.BlockCopy(bReponse, 0, resMessage, bHead.Length, bReponse.Length);

    //发送回复消息
    this.tcpServerEngine.PostMessageToClient(client, resMessage);
}

其主要流程为:

(1)解析消息头,获取消息类型和消息体的长度。

(2)根据消息类型,解析消息体,并构造协议对象。

(3)业务处理运算。(如 加法或乘法)

(4)根据业务处理结果,构造回复消息。

(5)发送回复消息给客户端。

6.客户端

(1)提交请求

    private void button1_Click(object sender, EventArgs e)
    {
        this.label_result.Text = "-";
        int msgType = this.comboBox1.SelectedIndex == 0 ? MessageType.Add : MessageType.Multiple;

        //请求消息体
        RequestContract contract = new RequestContract(int.Parse(this.textBox1.Text), int.Parse(this.textBox2.Text));
        byte[] bBody = SerializeHelper.SerializeObject(contract);

        //消息头
        MessageHead head = new MessageHead(bBody.Length,msgType) ;
        byte[] bHead = head.ToStream();

            //构建请求消息
        byte[] reqMessage = new byte[bHead.Length + bBody.Length];
        Buffer.BlockCopy(bHead, 0, reqMessage, 0, bHead.Length);
        Buffer.BlockCopy(bBody, 0, reqMessage, bHead.Length, bBody.Length);

        //发送请求消息
        this.tcpPassiveEngine.PostMessageToServer(reqMessage);
    }

其流程为:构造消息体、构造消息头、拼接为一个完整的消息、发送消息给服务器。

注意:必须将消息头和消息体拼接为一个完整的byte[],然后通过一次PostMessageToServer调用发送出去,而不能连续两次调用PostMessageToServer来分别发送消息头、再发送消息体,这在多线程的情况下,是非常有可能在消息头和消息体之间插入其它的消息的,如果这样的情况发生,那么,接收方就无法正确地解析消息了。

(2)显示处理结果

    void tcpPassiveEngine_MessageReceived(System.Net.IPEndPoint serverIPE, byte[] bMsg)
    {
        //获取消息类型
        int msgType = BitConverter.ToInt32(bMsg, 4);//消息类型是 从offset=4处开始 的一个整数
        if (msgType != MessageType.Result)
        {
            return;
        }

        //解析消息体
        ResponseContract response = (ResponseContract)SerializeHelper.DeserializeBytes(bMsg, MessageHead.HeadLength, bMsg.Length - MessageHead.HeadLength);
        string result = string.Format("{0}与{1}{2}的答案是 {3}" ,response.Number1,response.Number2,response.OperationType,response.Result);
        this.ShowResult(result);
    }

过程与服务端处理接收到的消息是类似的:从接收到的消息中解析出消息头、再根据消息类型解析出消息体,然后,将运算结果从消息体中取出并显示在UI上。

7.源码下载

二进制通信demo源码

 附相关系列:文本协议通信demo源码 说明文档

             打通B/S与C/S通信demo源码与说明文档

  另附:简单即时通讯Demo源码及说明

时间: 2024-08-12 02:28:14

轻量级通信引擎StriveEngine —— C/S通信demo(2) —— 使用二进制协议 (附源码)的相关文章

轻量级通信引擎StriveEngine —— C/S通信demo(附源码)

前段时间,有几个研究ESFramework的朋友对我说,ESFramework有点庞大,对于他们目前的项目来说有点"杀鸡用牛刀"的意思,因为他们的项目不需要文件传送.不需要P2P.不存在好友关系.也不存在组广播.不需要服务器均衡.不需要跨服务器通信.甚至都不需要使用UserID,只要客户端能与服务端进行简单的稳定高效的通信就可以了.于是,他们建议我,整一个轻量级的通信组件来满足类似他们这种项目的需求.我觉得这个建议是有道理的,于是,花了几天时间,我将ESFramework的内核抽离出来

【网站国际化必备】Asp.Net MVC 集成Paypal(贝宝)快速结账 支付接口 ,附源码demo

开篇先给大家讲段历史故事,博主是湖北襄阳人.襄阳物华天宝,人杰地灵,曾用名襄樊.在2800多年的历史文化中出现了一代名相诸葛亮(卧龙),三国名士庞统(凤雏),魏晋隐士司马徽(水镜先生),唐代大诗人孟浩然(孟襄阳),张继.杜审言,文学家皮日休,北宋著名书画家米芾(米襄阳),“允冠百王”的光武帝刘秀,东方圣人释道安等一大批历史文化名人.小说<三国演义>120回故事中有30多回提到襄阳. 相传诸葛亮的老婆黄月英黄头发黑皮肤,但知识广博.诸葛亮发明木牛流马,就是从黄月英的传授的技巧上发展出来.不仅如此

Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO

距离上次发布(android高仿系列)今日头条 --新闻阅读器 (二) 相关的内容已经半个月了,最近利用空闲时间,把今日头条客户端完善了下.完善的功能一个一个全部实现后,就放整个源码.开发的进度就是按照一个一个功能的思路走的,所以开发一个小的功能,如果有用,就写一个专门的博客以便有人用到独立的功能可以方便使用. 这次实现的功能是很多新闻阅读器(网易,今日头条,360新闻等)以及腾讯视频等里面都会出现的频道管理功能. 下面先上这次实现功能的效果图:(注:这个效果图没有拖拽的时候移动动画,DEMO里

Android中AIDL实现进程通信(附源码下载)

AIDL概述 之前的博客<Android中通过Messenger与Service实现进程间双向通信>演示了如何通过Messenger实现与Service进行跨进程通信,即IPC.但用Messenger实现的IPC存在一点不足:Service内部维护着一个Messenger,Messenger内部又维护着一个Hanlder,当多个client向该Service发送Message时,这些Message需要依次进入Hanlder的消息队列中,Hanlder只能处理完一个Message之后,再从消息队

开源数据引擎-介绍(附源码)

NetUML.DataEngine 数据引擎支持多数据库,数据访问引擎采用配置方式,类似ibatis.net底层原理,支持多数据库连接方式.将来可支持数据库读写分离,读写分离配置采用MVC路由机制. 源码结构 一.配置介绍 providers.config 配置文件名称,只需把它放在程序的根目录下即可. 1 <provider name="oracleManagedDataAccess" 2 description="Oracle, Microsoft provider

使用“Cocos引擎”创建的cpp工程如何在VS中调试Cocos2d-x源码

前段时间Cocos2d-x更新了一个Cocos引擎,这是一个集合源码,IDE,Studio这一家老小的整合包,我们可以使用这个Cocos引擎来创建我们的项目. 在Cocos2d-x被整合到Cocos引擎之前,我们可以不那么方便地在我们创建的工程里调试Cocos2d-x的代码,当我们使用了整合后的Cocos引擎,调试Cocos2d-x的代码就变得更加,非常不方便了! 使用Cocos2d-x创建的项目,在最先的版本必须是在Cocos2d-x引擎的目录下,放到其他的位置需要进行各种麻烦的设置,诸如头文

使用duilib开发半透明异形窗体程序(附源码和demo)

转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/43532791 半透明异形窗体的功能在之前维护的老版本的duilib里面已经有了基本的功能,但是因为一直存在较多的缺陷,所以我一直建议少用,就连我自己写仿酷狗项目也只是在几个小地方用了半透明异形窗体.不过今天在群里和其他几位朋友讨论后,发现了之前的许多问题以及解决方法.所以我立马修复了当前的库,并且写了一个半透明异形窗体的demo来测试效果.这里的半透明窗体是用Updat

单独编译和使用webrtc音频降噪模块(附源码+测试demo)

webrtc的音频处理模块分为降噪ns,回音消除aec,回声控制acem,音频增益agc,静音检测部分.另外webrtc已经封装好了一套音频处理模块APM,如果不是有特殊必要,使用者如果要用到回声消除,音频增益等较为复杂的模块时,最好使用全部的音频处理模块二不要单独编译其中一部分以免浪费宝贵的时间. 但是音频降噪部分较为简单,用起来也就几个函数,除了需要传入的音频数据以外,需要调整的参数也就是音频采样率和降噪等级.另外这部分代码采用纯C语言语法编写,可以跨平台编译.整个算法也不算特别复杂,运行起

Spring Web Flow 入门demo(一)附源码

Spring Web Flow (SWF)是Spring Framework的一个脱离模块.这个模块是Spring Web应用开发模块栈的一部分,Spring Web包含Spring MVC. Spring Web Flow的目标是成为管理Web应用页面流程的最佳方案.当你的应用需要复杂的导航控制,例如向导,在一个比较大的事务过程中去指导用户经过一连串的步骤的时候,SWF将会是一个功能强大的控制器. 下面我们还是从一个简单的demo开始了解它: 这个例子是结合Springmvc来实现,项目结构: