一.引子與協議說明
之前開發了一個項目——車載導航系統。遇到的第一個問題就是硬件設備如何與服務器通信。
關鍵在於通信協議!
眾所周知:要想實現通信,首先通信雙方就要達成通信協議。
話不多說,且看協議:
————————————————華麗的分割線—————————————————
以上的這些協議說明是不是看得很頭大呢?
遵循如此這般的通信協議的硬件設備又如何才能與服務器以及PC順利通信呢?
還請各位看官稍安勿躁!且聽我娓娓道來!
二.基礎知識-TCP與粘包
我們都知道,互聯網的核心是TCP/IP協議簇。其中TCP(傳輸控制協議)是一種面向連接的、可靠的、基於字節流的傳輸層協議。另外我們大概都聽過一個詞,叫做“粘包”,然而很多人對其內涵不甚了了。其實,粘包問題和TCP密切相關。因為TCP是面向連接的而且基於字節流,我們可以用一根水管來比喻TCP的工作方式。
字節流就跟水流一樣,當兩個消息一起讀取時,你無法分別出二者的邊界。
三.粘包的解決-消息定界
為了解決粘包問題,就需要對消息定界。
方法1:文本協議模式
方法2:二進制協議模式
文本協議模式通過在消息尾部加上特殊的標志來作為划分消息的依據;二進制協議則將消息封裝成消息頭+消息體,通過解析定長消息頭,然后從消息頭中取得消息體長度,進一步解析出消息體——從而粘包的問題得到了解決。
四.回到問題-協議選擇
以上是硬件設備的消息結構,從中我們既能夠看到文本協議的影子也能夠看到二進制協議的影子。
因為含有標志位,所以可以采用文本協議。
然后我們將開頭的標志位作為消息頭的一部分,剩下的部分都當成消息體,那么就是一個二進制協議的形式。二進制協議的兩個要求是:1.消息頭定長 2.消息頭中能解析出消息體長度。而這個消息結構是滿足這個要求的。
五.文本協議實現示例
// 摘要: // 文本協議助手接口。 public interface ITextContractHelper { // 摘要: // 消息結束標識符(經過編碼后得到的字節數組)的集合。 // 比如一般應用使、用"\0"作為消息結束標志,那么,集合中只有一個元素("\0"的二進制)。 // 有的應用可能有多個標識符(如"\0"、"\n"及其它)都可以作為消息的結束標志,則集合中就有多個元素。 // 如果設置為null,引擎則不進行消息完整性識別及構造,每次接收到數據,就直接觸發MessageReceived事件。 List<byte[]> EndTokens { get; } }
文本協議助手接口定義了采用文本協議的最基本的規范——具備消息結束符。接下來我們來看該接口的一個簡單的實現。
public class DefaultTextContractHelper : ITextContractHelper { public DefaultTextContractHelper(params string[] endTokenStrs) { this.endTokens = new List<byte[]>(); ; if (endTokenStrs == null || endTokenStrs.Length == 0) { return; } foreach (string str in endTokenStrs) { this.endTokens.Add(System.Text.Encoding.UTF8.GetBytes(str)); } } private List<byte[]> endTokens; public List<byte[]> EndTokens { get { return this.endTokens; } } }
其實文本協議的本質就是:消息的結束符經過編碼(比如UTF-8)后得到的字節數組作為字節流中識別消息邊界的標志。
我們將其注入通信引擎,引擎即可根據我們設置的標志來分割出一個個消息。
//初始化並啟動客戶端引擎(TCP、文本協議) this.tcpPassiveEngine = NetworkEngineFactory.CreateTextTcpPassiveEngine(this.textBox_IP.Text, int.Parse(this.textBox_port.Text), new DefaultTextContractHelper("\0"));
Demo中用“\0”來作為消息結束標志。我們知道,消息結束符是我們人為的加到消息尾部,而真正的消息是不具備這樣的內容的。所以在收到消息時我們需要剔除這個結束符,也就是解封。
void tcpPassiveEngine_MessageReceived(System.Net.IPEndPoint serverIPE, byte[] bMsg) { string msg = System.Text.Encoding.UTF8.GetString(bMsg); //消息使用UTF-8編碼 msg = msg.Substring(0, msg.Length - 1); //將結束標記"\0"剔除 this.ShowMessage(msg);
}
六.二進制協議Demo實現示例
// 摘要: // 二進制協議助手接口。 public interface IStreamContractHelper { // 摘要: // 消息頭的長度。 int MessageHeaderLength { get; } // 摘要: // 從消息頭中解析出消息體的長度(注意,不是整個消息的長度,而是不包含消息頭的Body的長度)。 // // 參數: // head: // 完整的消息頭,長度固定為MessageHeaderLength int ParseMessageBodyLength(byte[] head); }
文本協議助手接口定義了采用文本協議的最基本的規范——消息頭定長,從消息頭中能夠解析出消息體長度。接下來我們看一個該接口的簡單實現:消息頭規定為8個字節,其中頭4個字節存放一個int類型的消息體長度。
public class StreamContractHelper : IStreamContractHelper { public int MessageHeaderLength { get { return 8; } } public int ParseMessageBodyLength(byte[] head) { return BitConverter.ToInt32(head, 0); } }
我們將其注入通信引擎,引擎即可識別消息頭、取出消息體長度來分割出一個個消息。
//初始化並啟動客戶端引擎(TCP、文本協議)
this.tcpPassiveEngine = NetworkEngineFactory.CreateStreamTcpPassivEngine(this.textBox_IP.Text, int.Parse(this.textBox_port.Text), new StreamContractHelper());
七.總結
本文旨在介紹文本協議設計的一般方法,通過對於文本協議與二進制協議的本質的掌握,大家就能根據實際的需要來針對性的實現其通信協議了。大家可以根據這個一般的方法自己來實現一開始我給出來的衛星定位系統的通信協議。
有許多需要與硬件設備通信的應用,諸如與GPS設備通信、與工廠車間的一些單片機設備通信、與物聯網設備通信等等,大家只要掌握了這些設備的通信協議,或采用文本協議或采用二進制協議,將其實現,那么通信的問題就迎刃而解了!
最后,隨着HTML5 WebSocket技術的日益成熟與普及,B/S架構的應用於C/S架構的應用的通信也逐漸走向大一統。有興趣的朋友可以參考:打通B/S與C/S!讓HTML5 WebSocket與.NET Socket共用一個服務端!