Unity3D C# 從零自定義通訊協議

1 引言

博主大學學的電氣工程,畢業後做了一年多地鐵環控和低壓配電自控系統的工作,期間經常要調試各種各樣的設備,比如說電錶、PLC、電機。和這些設備打交道的一個核心就是要明白設備支持的通訊協議,比如工業上常用的Modbus Rtu、Modbus Tcp。那時只簡單知道這些通訊協議該怎麼用,該怎麼發數據去設定參數或讀取數據,但是不清楚爲什麼這些協議會這麼規定。直到後來從事開發工作,在一個項目中自定義了通訊協議,纔算理解了那些通訊協議如此定義的原因。
在這期間,我走了很多彎路,希望這篇文章能夠幫助到有需要的同學。
接下來,咱們會根據一個簡化後的通訊協議模型,然後刨析這個簡化後的模型,並據此一步步實現一個咱們自己的通訊協議,包括

  1. 幀頭幀尾的作用,幀頭幀尾該怎麼選
  2. 校驗碼的作用,校驗碼該如何計算
  3. 內容長度的作用,如何解決粘包、分包問題
  4. 版本號的作用
  5. 指令號該取多少個字節
  6. 大端模式、小端模式
  7. 什麼是心跳包
  8. 協議加密
  9. 協議接收與解析框架。

2 簡化的通訊協議模型

如圖,這是簡化後的通訊模型。有些通訊協議可能會沒有版本號,但那些都是非常成熟的協議了,如果我們自定義協議的話,最好還是加上,方便以後的版本升級。
無論我們通過串口還是socket還是其它什麼方式與設備或服務器通訊,發送的數據本質上都是Byte數組。這個Byte數組裏面存放的數據是按照如下圖的順序排列的,比如說前幾個Byte代表幀頭,幀頭後幾個Byte代表版本號,依次類推(這個“幾個Byte”中的幾是我們人爲約定好的)。理解了這一點,下面就來刨析一下,並從零開始設計一個屬於咱們自己的通訊協議。
通訊協議的簡化模型

2.1 幀頭、幀尾

幀頭、幀尾的作用是什麼?答案是用來標識,標識幀頭、幀尾中間這一段數據是我們自定義的協議數據。試想,許多種設備連接着我們的上位機,每種設備的通訊協議各不相同,只有其中一個是按照我們定義的協議進行收發數據的,那麼我們上位機如何去把這個設備識別出來呢?答案就是通過我們獨特的幀頭和幀尾。
那麼怎麼設計才能做到獨特呢,答案是隨機選2 ~ 4個數然後組合起來,這個數的範圍是(0x00, 0xff),不要選用0x00和0xff,因爲許多設備的雜散信號也是0x00和0xff。那爲什麼選2 ~ 4個呢?答案是使用1個Byte當作幀頭幀尾的話,我們發送或接收到的數據中可能會含有大量相同的數據,其實起不到標識的作用,會增大咱們的計算量。而超過4個之後,就完全沒必要了,因爲2 ~ 4個已經完全能夠標識出咱們的協議了,多了反而會浪費內存。(ps: 0x00是16進制的0,0xff是16進制的255,文章後面都是使用16進製表示數字,以後不再贅述)
我們一般用兩個Byte來表示幀頭和幀尾,幀頭和幀尾一般是互相顛倒的。什麼意思?比如說咱們決定使用0xdd和0x99來表示幀頭,那幀尾就是0x99和0xdd。所以,添加幀頭、幀尾後的協議如下。
添加幀頭和幀尾後協議

2.2 校驗碼

校驗碼有什麼用?答案是用來驗證這一段數據是否是有效的數據。
上一節咱們根據幀頭、幀尾找到了咱們收發的數據,試想一種情況,有一段雜散數據剛好也滿足咱們的幀頭、幀尾,那我們如何把這段數據給辨別出來呢?答案就是通過校驗碼。那麼校驗碼是如何辨別的呢?步驟如下:
1.根據幀頭、幀尾和後面要說的長度把內容以及校驗碼給全部提取出來
2.將內容按照咱們約定好的計算方法去計算校驗碼
3.然後比對咱們計算的校驗碼與提取的校驗碼,如果它們相等,就認爲這段數據是有效的(然後進行解析再處理),不等則人爲無效(判定無效之後有些人會直接捨棄掉這段數據,但是不建議直接捨棄,而是隻把幀頭給捨棄掉,然後再繼續查找,爲什麼要這樣做見2.3節中的協議處理流程)。
現在有兩個問題,校驗碼的計算方法該如何定義?校驗碼一般佔幾個Byte?
校驗碼的計算方法有別人已經定義好的,如CRC16、CRC32等等。當然咱們也可以自己定義計算方法,有一個基本要求是內容中的每一個Byte都要參與計算。我們這裏就選用CRC16來計算校驗碼,校驗碼佔2個Byte就行了,原理類似幀頭幀尾,少了不夠多了浪費。
添加校驗碼後的指令

2.3 長度

長度表示內容的長度。爲什麼要加一個長度,通過找到幀頭、幀尾不就已經可以把數據提取出來了嗎?答案是不能,光靠幀頭幀尾並不能將數據提取出來,爲什麼?先考慮一種情況,假設我們發送的內容中包含和幀尾一樣的數據,比如這裏的0x99和0xdd,且我們沒有將內容的長度發送過去,這時我們取出來的數據肯定是不能通過校驗的,那麼問題來了,本來這條數據應該是有效的,但是隻是因爲我們沒有取對!除此之外,還有粘包分包的問題。解決這些問題的一個方法就是加上一個內容的長度。
那爲什麼加上長度之後就能取出正確的數據了呢?這裏直接給出處理數據的流程(假設通過Tcp協議接收數據),相信大家看了就會明白了。

  1. 首先我們定義一個List<Byte>集合
  2. 然後開啓Tcp監聽端口
  3. 將每一次收到的數據(Byte數組)都添加到1的集合中
  4. 另外單獨開一個線程去處理1中集合中的數據
  5. 首先從集合的第一個元素開始找幀頭,如果前兩個元素不是幀頭就直接拋棄掉(從集合中移除)
  6. 找到幀頭後,再根據長度去把內容提取出來,如果發現這一次處理的時候數據還未收完(Tcp分包了,發送的內容較長時會出現這種情況),就等待,直到收到的長度足夠了,再進行步驟7
  7. 然後檢測幀尾,並計算校驗碼。如果校驗碼通過,就從集合中將這段數據拷貝出來進行解析,同時集合中移除這一段數據,並回到步驟5繼續重複執行5、6、7;如果校驗不通過,則把幀頭幀尾移除掉,再回到步驟5繼續重複執行5、6、7

至於長度需要佔幾個Byte,一般選2個Byte及以上(原因還是1個Byte不夠用),2個Byte能表示的值的範圍爲0~65535,如果項目發送給的內容長度不會超過這個範圍,就選2個Byte就行。我們這裏就選擇2Byte。
加上長度的指令

2.4 版本號

爲什麼設計的協議要帶一個版本號?爲了方便升級維護!試想一下,我們做爲客戶端,有一天突然改動了某一條指令的定義,這時同一條指令就會有兩個版本,但是指令中沒有版本號,這時服務器那邊該怎麼辦?服務器只能一臉懵逼不知道該如何解析,不知道該用舊的指令定義去解析還是改用新的指令定義去解析。
那版本號該怎麼定義?我一般用兩個Byte來表示,如第一個Byte表示大版本號,第二個Byte表示小版本號,如0x01 0x00就表示版本號爲1.0,以此類推。當然這個可以約定好規則定義的。
加上版本號的指令

2.5 內容

內容中包含什麼完全是由我們協議制定者規定的。一般前兩個Byte表示指令號,爲什麼用2Byte來表示指令號?還是那個原因,2個Byte能夠表示65536個指令,而如果用1個Byte的話只能表示256個指令,如果項目大了,可能完全不夠用的!我們做項目什麼地方都得留一手,剛好夠用的話後期擴展會很蛋疼的。
指令號

2.6 大端模式、小端模式

我們知道一個int值佔4個Byte,比如123456用16進製表示爲0x0001e240,這時我們要把這個數據發送給服務器,那是按照0x00、0x01、0xe2、0x40(大端模式,先發送高位再發送低位)的順序發送給服務器還是按照0x40、0xe2、0x01、0x00(小端模式,先發送低位再發送高位)發送給服務器呢?這個在設計協議時就得約定好,然後全部統一,不要一會兒用大端模式,一會兒用小端模式。

2.7 心跳包

心跳包地作用就是告訴對方(服務器)我一直還“活”着在,服務器端可以用來判斷客戶端是否還在正常工作。
心跳包的原理就是客戶端定時發送(5秒,或1分鐘,這個時間可以按照項目需要更改)一條指令給服務器,然後服務器回覆。有些協議心跳包做得比較簡單,就是一條簡單的指令,什麼都不帶。如下是最簡單的心跳包(不帶任何附加內容),假設爲心跳包的指令號爲0x00 0x01。
最簡心跳包
複雜一點的心跳包可以如下設計:

  1. 客戶端定時發送心跳包,心跳包的內容包含有時間戳,累加數C1
  2. 服務器收到客戶端的心跳包後,將收到的累加數加1,即C2=C1+1,並回復給客戶端;同時服務器開始計時,如果規定時間(按項目需要設定,如3分鐘)內沒有收到客戶端包含C2的心跳包,就認爲客戶端斷線,然後服務器強制斷開這個客戶端的連接
  3. 客戶端收到服務器回覆的心跳包後,將服務器發過來的累加數C2提取出來,然後下次發送心跳時就將C2發送過去

複雜一點的心跳包

2.7 指令加密

指令加密,其實就是對指令中的內容部分進行加密,加密後的內容長度會改變,這時就需要重新拼成新的一條指令再發送,然後服務器端收到之後,先把內容提取出來,然後進行解密之後再解析內容。
爲什麼要加密?很簡單,爲了避免別人模擬設備來做一些非法的事情。
簡單的加密使用對稱加密算法就是可以的,如des、aes加密,這裏就不展開說了,就是調用api而已。
目前常用的加密方式是服務器發token的方式:每個客戶端連接的時候服務器都發送一條指令給這個客戶端,這條指令中包含了一個永遠唯一的token,以後客戶端每次發送指令的時候,內容中都必須帶有這個token,服務器端通過比對客戶端發送過來的token和服務器端存儲的token是否一致來決定客戶端的指令是否有效。另外,服務器端會定時更改這個客戶端的token,同時token中包含有時間戳,服務器可以用來檢測超時。
這個有點類似於jsonwebtoken,大家可以去了解一下。

2.8 協議接收與解析框架

暫時未完成,todo.

ps:所有協議萬變不離其宗,相信把以上部分弄懂,面對新的協議咱們也能以不變應萬變。

博主個人博客本文鏈接。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章