Socket通用TCP通信协议设计及实现(防止粘包,可移植,可靠)
Socket通用TCP通信協議設計及實現(防止粘包,可移植,可靠)
?
引文
我們接收Socket字節流數據一般都會定義一個數據包協議。我們每次開發一個軟件的通信模塊時,盡管具體的數據內容是不盡相同的,但是大體上的框架,以及常用的一些函數比如轉碼,校驗等等都是相似甚至一樣的。所以我感覺設計一個通用的通信協議,可以在之后的開發中進行移植實現高效率的開發是很有必要的。另外,本協議結合我自己所了解的通信知識盡可能的提升了可靠性和移植性,可處理類似粘包這樣的問題。對于本文中可能存在的問題,歡迎各位大神多多指點。
?
報文設計
???????? 本報文的字段結構分為Hex編碼和BCD(8421)編碼兩種。由于BCD編碼的取值范圍其實是Hex編碼的真子集,也就是所謂的16進制編碼中的“ABCDEF”這六個字母對應的數值是在BCD編碼中無法取值的。所以我利用這個特點,將報文中的用于標識的不含實際數據的抽象字段用Hex編碼,且取值范圍在A~F之間。將反應實際數據的字段用BCD編碼。這樣,具有標識作用的字段與實際數據字段的取值是互不交叉的。這無形中就避免了很多出現的問題,增強了報文的可靠性。例如:我使用”0xFFFF”代表報文起始符,這個取值是不會在任何一個數據字段中出現的,應為它們是BCD編碼。也就是是說,字節流緩沖區中只要出現”0xFFFF”我們就可以判斷這個是一個數據包的開頭(我在實現在緩沖區中找尋數據包算法時還做了另外的控制,進行雙重保障)。
???????? 對于正文部分,我設計成了“標識符|數據”成對出現的形式。每個標識符用來指示后面出現的數據的含義,數據字段用于傳輸真實的數據。這種對的形式,增強了報文的移植性,在新的一次開發到來時,我們只要按需求定義好正文部分的“標識符|數據”對即可。另外,這種設計還增強了發送報文方的靈活性。標識符的存在使得各項數據可以按照任意的順序發送,沒有的數據也可以不發。
???????? 基于以上的這些考慮,我把報文設計成了如下形式:
?
通用報文協議
| 序號 | 名稱 | 編碼說明 |
| ?1 | 報文起始符 | 2字節Hex編碼? ??0xFFFF |
| ?2 | 功能碼(報文類型) | 2字節Hex編碼??? 0xD1D1 |
| ?3 | 密碼 | 4字節BCD編碼??? 00 00 00 01 |
| ?4 | 長度 | 2字節BCD編碼??? 正文實際長度 |
| ?5 | 標識符1 | 2字節Hex編碼?? 自定義數據標識符? 0xA001 |
| ?6 | 數據1 | N字節BCD編碼? N根據實際情況自定義 |
| ?7 | 標識符2 | 2字節Hex編碼?? 自定義數據標識符? 0xA002 |
| ?8 | 數據2 | N字節BCD編碼? N根據實際情況自定義 |
| ?... | … | ? |
| ? | 報文終止符 | 2字節Hex編碼?? 0xEEEE |
| ? | 校驗碼 | 校驗碼前所有字節的CRC校驗,生成多項式:X16+X15+X2+1,高位字節在前,低位字節在后。 |
?
報文示例:
示例背景:發送報文通知遠程服務器第1號設備開關的當前狀態為開啟
需自定義正文部分,含兩個字段,設備編號和開關狀態
發送的字節數組:255 255 | 209209 | 0 0 0 1 | 0 6 | 160 1 | 1 | 160 2| 0 | 238 238 | 245 40 |
對應含義解釋:?? 起始符FFFF | 功能碼D1D1 | 密碼00 00 00 01 | 長度(正文)00 06|??? 標識符A001 | 數據 1 | 標識符A002 | 數據 0 | 報文終止符 EEEE | 校驗結果 |
?
粘包問題的解決
針對我的協議,我設計了一個緩沖區中找尋數據包算法,這兩者的配合完美的實現了防止粘包,過濾噪聲數據等類似的各種令人頭疼的問題。此算法思路來自博文點擊打開鏈接?
算法流程圖如下:
算法C#代碼具體實現:
/// <summary>/// 數據緩沖區/// </summary>public class DataBuffer{//字節緩沖區private List<byte> m_buffer = new List<byte>();#region 私有方法/// <summary>/// 尋找第一個報頭 (0xFFFF)/// </summary>/// <returns>返回報文起始符索引,沒找到返回-1</returns>private int findFirstDataHead(){int tempIndex=m_buffer.FindIndex(o => o == 0xFF);if (tempIndex == -1)return -1;if ((tempIndex + 1) < m_buffer.Count) //防止越界if (m_buffer[tempIndex + 1] != 0xFF)return -1;return tempIndex;}/// <summary>/// 尋找第一個報尾 (0xEEEE)/// </summary>/// <returns></returns>private int findFirstDataEnd(){int tempIndex = m_buffer.FindIndex(o => o == 0xEE);if (tempIndex == -1)return -1;if((tempIndex+1)<m_buffer.Count) //防止越界if (m_buffer[tempIndex + 1] != 0xEE)return -1;return tempIndex;}#endregion/// <summary>/// 在緩沖區中尋找完整合法的數據包/// </summary>/// <returns>找到返回數據包長度len,數據包范圍即為0~(len-1);未找到返回0</returns>public int Find(){if (m_buffer.Count == 0)return 0;int HeadIndex = findFirstDataHead();//查找報頭的位置if (HeadIndex == -1){//沒找到報頭m_buffer.Clear();return 0; }if (HeadIndex >= 1)//不為開頭移掉之前的字節m_buffer.RemoveRange(0, HeadIndex);int length = GetLength();if (length==0){//報文還未全部接收return 0;}int TailIndex = findFirstDataEnd(); //查找報尾的位置if (TailIndex == -1){return 0;}else if (TailIndex + 4 != length) //包尾與包長度不匹配{//退出前移除當前報頭m_buffer.RemoveRange(0, 2);return 0;}return length;}/// <summary>/// 包長度/// </summary>/// <returns></returns>public int GetLength(){//報文起始符 功能碼 密碼 正文長度 報文終止符 CRC校驗碼 這六個基礎結構占14字節//因此報文長度至少為14if (m_buffer.Count >= 14){int length = m_buffer[8] * 256 + m_buffer[9];//正文長度return length + 14;}return 0;}/// <summary>/// 提取數據/// </summary>public void Dequeue(byte[] buffer, int offset, int size){m_buffer.CopyTo(0, buffer, offset, size);m_buffer.RemoveRange(offset, size);}/// <summary>/// 隊列數據/// </summary>/// <param name="buffer"></param>public void Enqueue(byte[] buffer){m_buffer.AddRange(buffer);}}
調用示例:
private void receive(){while (true)//循環直至用戶主動終止線程{int len = Server.Available;if (len > 0){byte[] temp = new byte[len];Server.Receive(temp,len,SocketFlags.None);buffer.Enqueue(temp);while (buffer.Find()!=0) //while可處理同時接收到多個包的情況 {int length = buffer.GetLength();byte[] readBuffer = new byte[len];buffer.Dequeue(readBuffer, 0, length);//OnReceiveDataEx(readBuffer); //這里自己寫一個委托或方法就OK了,封裝收到一個完整數據包后的工作 //示例,這里簡單實用靜態屬性處理:DataPacketEx da = Statute.UnPackMessage(readBuffer);ComFun.receiveList.Add(da);}}Thread.Sleep(100);//這里需要根據實際的數據吞吐量合理選定線程掛起時間}} 其中DataPacketEx是封裝數據包正文部分的類,其中的屬性記錄了要發送的數據使用時只需開啟一個線程,不斷的將收到的字節流數據加入緩沖區中。調用Find()方法找尋下一個數據包,如果該方法返回0,說明當前緩沖區中不存在數據包(數據尚未完整接收/存在錯誤數據,該方法可自行進行處理),如果返回一個正數n,則當前緩沖區中索引0-(n-1)的數據即為一個收到的完整的數據包。對其進行處理即可。
協議的實現
在實現協議前,首先我在自定義的TransCoding類中實現了幾個靜態方法用于Hex、BCD、string等之間的轉換。
/// <summary>/// 將十進制形式字符串轉換為BCD碼的形式/// </summary>/// <param name="str">十進制形式的待轉碼字符串,每個字符需為0~9的十進制數字</param>/// <returns></returns>public static byte[] BCDStrToByte(string str){#region 原方法//長度為奇數,隊首補0if (str.Length % 2 != 0){str = '0' + str;}byte[] bcd = new byte[str.Length / 2];for (int i = 0; i < str.Length / 2; i++){int index = i * 2;//計算BCD[index]處的字節byte high = (byte)(str[index] - 48); //高四位high = (byte)(high << 4);byte low = (byte)(str[index + 1] - 48); //低四位bcd[i] = (byte)(high | low);}return bcd;#endregion}/// <summary>/// 將字節數據轉化為16進制的字符串(注意:同樣適用與轉8421格式的BCD碼!!!!)/// </summary>/// <param name="hex"></param>/// <param name="index"></param>/// <returns></returns>public static string ByteToHexStr(byte[] hex, int index){string hexStr = "";if (index >= hex.Length || index < 0)throw new Exception("索引超出界限");for (int i = index; i < hex.Length; i++){if (Convert.ToInt16(hex[i]) >= 16){hexStr += Convert.ToString(hex[i], 16).ToUpper();}else{hexStr += "0" + Convert.ToString(hex[i], 16).ToUpper();}}return hexStr;}/// <summary>/// 將16進制字符串轉化為字節數據/// </summary>/// <param name="hexStr"></param>/// <returns></returns>public static byte[] HexStrToByte(string hexStr){if (hexStr.Trim().Length % 2 != 0){hexStr = "0" + hexStr;}byte[] hexByte = new byte[hexStr.Length / 2];for (int i = 0; i < hexByte.Length; i++){string hex = hexStr[i * 2].ToString(CultureInfo.InvariantCulture) + hexStr[i * 2 + 1].ToString(CultureInfo.InvariantCulture);hexByte[i] = byte.Parse(hex, NumberStyles.AllowHexSpecifier);}return hexByte;#region 使用Convert.ToByte轉換//長度為奇數,隊首補0,確保整數//if (str.Length % 2 != 0)//{// str = '0' + str;//}//string temp = "";//byte[] BCD = new byte[str.Length / 2];//for (int index = 0; index < str.Length; index += 2)//{// temp = str.Substring(index, 2);// BCD[index / 2] = Convert.ToByte(temp, 16);//}//return BCD;#endregion}
以下是協議的實現的兩個核心方法,裝包和解包
裝包方法將已有的具體的不同數據類型的數據轉換成byte字節流,以便進行socket通信
解包方法將socket接收到的完整數據包字節流解析成封裝數據包的類DataPacketEx
/// <summary>/// 構造向終端發送的消息(示例)/// </summary>/// <param name="data">記錄發送消息內容的數據包</param>/// <returns>發送的消息</returns>public byte[] BuildMessage(DataPacketEx data){List<byte> msg = new List<byte>(); //先用消息鏈表,提高效率//幀起始符byte[] tempS = TransCoding.HexStrToByte("FFFF");ComFun.bytePaste(msg, tempS);//功能碼tempS = TransCoding.HexStrToByte("D1D1");ComFun.bytePaste(msg, tempS);//密碼tempS = TransCoding.BCDStrToByte("00000001");ComFun.bytePaste(msg, tempS);//長度tempS = TransCoding.BCDStrToByte("0006");ComFun.bytePaste(msg, tempS);//開關設備編號標識符tempS = TransCoding.HexStrToByte("A001");ComFun.bytePaste(msg, tempS);//開關設備編號tempS = TransCoding.BCDStrToByte(data.ObjectID);ComFun.bytePaste(msg, tempS);//開/關標識符tempS = TransCoding.HexStrToByte("A002");ComFun.bytePaste(msg, tempS);//開/關tempS = TransCoding.BCDStrToByte(data.IsOpen);ComFun.bytePaste(msg, tempS);//報文終止符tempS = TransCoding.HexStrToByte("EEEE");ComFun.bytePaste(msg, tempS);//CRC校驗byte[] message = new byte[msg.Count];for (int i = 0; i < msg.Count; i++){message[i] = msg[i];}byte[] crc = new byte[2];Checksum.CalculateCrc16(message, out crc[0], out crc[1]);message = new byte[msg.Count + 2];for (int i = 0; i < msg.Count; i++){message[i] = msg[i];}message[message.Length - 2] = crc[0];message[message.Length - 1] = crc[1];return message;}/// <summary>/// 解包數據/// </summary>/// <param name="message">需要解包的數據</param>/// <returns>成功解析返回true,否則返回false </returns>public DataPacketEx UnPackMessage(byte[] message){//先校驗信息是否傳輸正確if (!CheckRespose(message))return null;//檢查密碼是否正確.(假設當前密碼為00 00 00 01,需在應用時根據實際情況解決)byte[] temp = new byte[4];temp[0] = message[4];temp[1] = message[5];temp[2] = message[6];temp[3] = message[7];if (TransCoding.ByteToHexStr(temp, 0) != "00000001")return null;DataPacketEx DataPacket = new DataPacketEx("", "", "");//獲取功能碼byte[] funType = new byte[2] { message[2], message[3] };string functionStr = TransCoding.ByteToHexStr(funType, 0);#region 具體解包過程,需根據實際情況修改int index = 10; //(當前索引指向第一個標識符)string tempStr="";switch (functionStr){case "D1D1":temp = new byte[2] { message[index], message[index + 1] };index = index + 2;tempStr = TransCoding.ByteToHexStr(temp, 0);while (tempStr != "EEEE"){switch (tempStr){//注意:每種標識符對應的數據長度是協議中自定義的case "A001"://開關設備編號temp = new byte[1] { message[index] };index = index + 1;tempStr = TransCoding.ByteToHexStr(temp, 0);DataPacket.ObjectID = tempStr;break;case "A002"://開or關(開:00 關:11)temp = new byte[1] { message[index] };index = index + 1;tempStr = TransCoding.ByteToHexStr(temp, 0);DataPacket.IsOpen = tempStr;break;//case "其他標識符":// //對應信息// break;}temp = new byte[2] { message[index], message[index + 1] };index = index + 2;tempStr = TransCoding.ByteToHexStr(temp, 0);}break;//case "其他功能碼":// //對應功能// break;}#endregionreturn DataPacket;}
對于通信可靠性的驗證
對此,我制作了兩個簡單的demo,一個服務器端,一個客戶端。
客戶端可想服務器端循環發送數據,其中以0.5的概率夾雜著隨機長度隨機取值的干擾數據,以此來判斷本協議在實際應用中的可行性。
服務器端負責循環接收并處理顯示收到的數據
最終的運行結果如下圖:
由運行結果可以看出,服務器端完美屏蔽掉了客戶端發出的錯誤數據,全部解析出了客戶端發送的實際數據。證明本協議可以解決類似粘包,傳錯等等類似的通訊中的棘手問題。當然,協議中如果有不完美的地方,希望各位大神指教。另外,上面的demo只是為了驗證協議所做,還存在一些零零碎碎的小bug。
以上就是通信協議的全部核心內容。
具體實現的代碼中可能包含一些并未給出的不太重要的類,并不影響理解。
具體的demo我上傳到了http://download.csdn.net/detail/u011583927/8653701?
畢竟認真總結了好久,所以設置了積分大家不要介意哈
總結
以上是生活随笔為你收集整理的Socket通用TCP通信协议设计及实现(防止粘包,可移植,可靠)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 外设驱动库开发笔记0:EPD总体设计
- 下一篇: 外设驱动库开发笔记26:nRF24L01