前段時間,有幾個研究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原始碼及 說明文件
版權宣告:本文為博主原創文章,未經博主允許不得轉載。