在网络上,交互的双方基于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源码及 说明文档