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 

毕竟认真总结了好久,所以设置了积分大家不要介意哈


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499567.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

浅谈 Adaboost 算法

注&#xff1a;本文全文引用自http://blog.csdn.net/carson2005/article/details/41444289 当然作者也是转载的&#xff0c;原文是http://blog.csdn.net/haidao2009/article/details/7514787 写的很好所以转载过来以便之后再次翻阅。 一 Boosting 算法的起源 boost 算法系列的起…

如何理解离散傅里叶变换(一)实数形式傅里叶变换

如何理解离散傅里叶变换&#xff08;一&#xff09; ——实数形式傅里叶变换 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 本文…

快速傅里叶变换(FFT)

快速傅里叶变换&#xff08;FFT&#xff09; ------------------------------------------------------------------------------------------------------------------- 作者&#xff1a;随煜而安 时间&#xff1a;2015/7/21 注&#xff1a;本文为作者原创文章&#xff0c…

风机桨叶故障诊断(一) 样本的获取

风机桨叶故障诊断&#xff08;一&#xff09; 样本的获取今天团队接了个新项目&#xff0c;做一个风机桨叶故障诊断系统。虽然马上就是准备考研的关键期了&#xff0c;可是一想到这是我学习了机器学习后遇到的第一个实际项目&#xff0c;我觉得参与进来&#xff0c;也帮导师分担…

风机桨叶故障诊断(二) 获取图像几何主方向

风机桨叶故障诊断&#xff08;二&#xff09; 获取图像几何主方向 昨天&#xff0c;我将视频资源按帧抽取并筛选得到了可以用来提取样本的图像库。今天还是进行项目的准备工作。当我们拿到一张图片&#xff0c;我们的软件要做的大致可以分为三个步骤&#xff1a;从原图中识别桨…

风机桨叶故障诊断(三) 识别桨叶——初步构建BP神经网络

风机桨叶故障诊断&#xff08;三&#xff09; 识别桨叶——初步构建BP神经网络 新的一天&#xff0c;希望有好的运气。今天开始着手系统的第一个模块&#xff0c;从一幅图像中寻找到桨叶所在的位置。第一直觉我们的识别任务属于难度比较大&#xff0c;干扰因素多的了&#xff…

风机桨叶故障诊断(五) 修改隐含层神经元个数的尝试

风机桨叶故障诊断&#xff08;五&#xff09; 修改隐含层神经元个数的尝试 我们已经为训练一个更为稳健的神经网络做好了样本的准备工作&#xff0c;那么我们开始下一步的工作吧&#xff01; 我们已经有了样本集&#xff0c;目前我筛选出来了247个正样本&#xff0c;652个负样本…

风机桨叶故障诊断(六) 利用自编码器进行特征学习

风机桨叶故障诊断&#xff08;六&#xff09; 利用自编码器进行特征学习 在之前的工作中&#xff0c;我已经初步构建了三层的BP神经网络&#xff0c;并已经从样本集的选取&#xff0c;模型的选择&#xff08;隐含层神经元个数&#xff09;&#xff0c;和输出层神经元阈值选择这…

风机桨叶故障诊断(七) 滑动窗与非极大值抑制NMS

风机桨叶故障诊断&#xff08;七&#xff09;滑动窗与非极大值一直NMS 到目前为止&#xff0c;我已经利用自编码神经网络提取特征后训练得到了BP神经网络&#xff08;参见&#xff1a;点击打开链接&#xff09;&#xff0c;且在测试样本集上表现不错。下面我们就要应用到实际中…

Distinctive Image Features from Scale-Invariant Keypoints-SIFT算法译文

本文全篇转载自如下博客&#xff0c;感谢博主的无私分享 http://www.cnblogs.com/cuteshongshong/archive/2012/05/25/2506374.html ------------------------------------------------------------------------------------------------------ 从尺度不变的关键点选择可区分的…

将图像绘制成3维立体散点图

matlab源代码&#xff1a; Iimread(F:\绝缘子识别\绝缘子红外test图片\test (50).jpg); Irgb2gray(I); [wd,len]size(I); interval10; %设置绘制散点图的间隔&#xff0c;全部绘出会很卡 x[]; y[]; z[]; numfloor((len-1)/interval)1;%计算在当前间隔下图像的每一行…

C#灰度图转伪彩色图

/// <summary>/// 伪彩色图像构造器/// </summary>public class PseudoColorImageBuilder{/// <summary>/// 铁红色带映射表/// 每一行代表一个彩色分类&#xff0c;存放顺序是RGB/// </summary>public static byte[,] ironTable new byte[128, 3] {{…