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通信协议设计及实现(防止粘包,可移植,可靠)相关推荐

  1. python socket.socket()函数 套接字详解及TCP、UDP程序示例(粘包等)

    文章目录 socket的定义 套接字的工作流程 socket函数使用 socket函数用法 服务端套接字函数 客户端套接字函数 公共用途的套接字函数 面向文件的套接字方法 打电话的流程演示 服务端.p ...

  2. TCP、UDP、TCP三次握手与四次挥手、TCP如何保证可靠传输、TCP异常分析、拆包和粘包等

    4.OSI模型 4.1.OSI七层模型 4.2.七层模型功能 ​ 物理层:利用传输介质为数据链路层提供物理连接,实现比特流的透明传输,如网线:网卡标准. ​ 数据链路层:接收来自物理层的位流形式的数据 ...

  3. Java包数据消息头消息尾_读Socket流时产生阻塞的解决方案(粘包拆包问题)

    转自:https://www.cnblogs.com/qhyuan1992/p/5385289.html 其实最终讨论的是TCP通信过程中的粘包拆包(半包)问题. 在用socket写一个服务器时遇到了 ...

  4. Python之路(第三十一篇) 网络编程:简单的tcp套接字通信、粘包现象

    一.简单的tcp套接字通信 套接字通信的一般流程 服务端 server = socket() #创建服务器套接字server.bind() #把地址绑定到套接字,网络地址加端口server.liste ...

  5. 4-10:TCP协议之面向字节流和粘包问题

    文章目录 一:面向字节流 二:粘包(应用层数据包)问题 三:TCP异常情况 一:面向字节流 经过前面的叙述,现在我们对TCP面向字节流的理解就更加深刻了. 创建一个TCP的socket,就会在内核中创 ...

  6. C# socket通信 接收缓冲区大小设置,以及粘包问题的解决

    C# socket通信 接收缓冲区大小,以及粘包问题的解决 一. Socket接收缓冲区无论: 1.buffer设置有多大: 2.同步接收还是异步接收: 3.发送超过 43690 也就是 42KB的字 ...

  7. Python3之socket编程(TCP/UDP,粘包问题,数据传输、文件上传)

    一.socket的定义 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后 ...

  8. socket的长连接、短连接、半包、粘包与分包

    socket的半包,粘包与分包的问题和处理代码:http://blog.csdn.net/qq_16112417/article/details/50392463 知乎关于长连接和短连接:https: ...

  9. 为什么tcp不采用停等协议_为什么 TCP 协议有粘包问题

    来自公众号:真没什么逻辑 链接:https://draveness.me/whys-the-design-tcp-message-frame/ 为什么这么设计(Why's THE Design)是一系 ...

最新文章

  1. 远程办公时,有哪些提高沟通效率的技巧?
  2. 设某一机器由n个部件组成_每日小课堂超级攻略!工业机器人知识点全知道
  3. 把女友升级为老婆的时候发生的BUG
  4. 日常python常见问题
  5. linux 显示文字在桌面背景,Linux下Xwindow的字体配置(所谓的字体美化)
  6. http协言和web本质
  7. 面试官:给我说一下你理解的分布式架构
  8. 快速解决 Android SDK Manager 无法下载或者下载速度慢
  9. spring security 认证与权限控制
  10. 可以接收数量不定的参数的函数
  11. MVC5 Action的view()直接输出字符串方式
  12. LOJ2336 JOI2017 绳 贪心、构造
  13. 眉山市谷歌高清卫星地图下载
  14. 2021年高压电工考试题及高压电工考试试卷
  15. 如何在android studio中调用mumu模拟器
  16. IOS 从项目学习Swift 开发(一)
  17. 为什么软件测试人员都不通过QQ、微信、邮件上报Bug?
  18. 文件管理(文件系统)
  19. 100°C TALK百度健康行业沙龙:大数据赋能大健康产业发展
  20. load opencv videoio gstreamer453 64d dll failed 打开摄像头慢

热门文章

  1. 八大排序:Java实现八大排序及算法复杂度分析
  2. JAVA:线程总结及多线程实现的两种方法
  3. 关于直播学习笔记-005-nginx-rtmp-win32在Win10上使用
  4. 接口自动化- 基于 Python
  5. [转载] 一致性哈希
  6. 转载(四).Net Framework中的委托与事件
  7. Javascript里使用Dom操作Xml
  8. 3.2_ 2_ 请求分页管理方式
  9. linux——线程(2)
  10. 操作系统——文件目录