前段時間,有幾個研究ESFramework通信框架的朋友對我說,ESFramework有點龐大,對於他們目前的項目來說有點“殺雞用牛刀”的意思,因為他們的項目不需要文件傳送、不需要P2P、不存在好友關系、也不存在組廣播、不需要服務器均衡、不需要跨服務器通信、甚至都不需要使用UserID,只要客戶端能與服務端進行簡單的穩定高效的通信就可以了。於是,他們建議我,整一個輕量級的C#通訊組件來滿足類似他們這種項目的需求。我覺得這個建議是有道理的,於是,花了幾天時間,我將ESFramework的內核抽離出來,經過修改封裝后,形成了StriveEngine通訊組件,其最大的特點就是穩定高效、易於使用。
在網絡上,交互的雙方基於TCP或UDP進行通信,通信協議的格式通常分為兩類:文本消息、二進制消息。
文本協議相對簡單,通常使用一個特殊的標記符作為一個消息的結束。
二進制協議,通常是由消息頭(Header)和消息體(Body)構成的,消息頭的長度固定,而且,通過解析消息頭,可以知道消息體的長度。如此,我們便可以從網絡流中解析出一個個完整的二進制消息。
兩種類型的協議格式各有優劣:文本協議直觀、容易理解,但是在文本消息中很難嵌入二進制數據,比如嵌入一張圖片;而二進制協議的優缺點剛剛相反。
在 輕量級通信引擎StriveEngine —— C/S通信demo(附源碼)一文中,我們演示了如何使用了相對簡單的文本協議,這篇文章我們將構建一個使用二進制消息進行通信的Demo。本Demo所做的事情是:客戶端提交運算請求給服務端,服務端處理后,將結果返回給客戶端。demo中定義消息頭固定為8個字節:前四個字節為一個int,其值表示消息體的長度;后四個字節也是一個int,其值表示消息的類型。
1.StriveEngine通訊組件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通訊組件Demo服務端
關於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.StriveEngine通訊組件Demo客戶端
(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.StriveEngine通訊組件Demo源碼下載
附相關系列:文本協議通信demo源碼及 說明文檔
版權聲明:本文為博主原創文章,未經博主允許不得轉載。