对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包.

一.为什么基于TCP的通讯程序需要进行封包和拆包.

TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

二.为什么会出现B.C.D的情况.
"粘包"可发生在发送端也可发生在接收端.
1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.象C和D的情况就有可能是Nagle算法造成的.
2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据.

三.怎样封包和拆包.
    封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.

相当于:对逻辑业务包进行两次接收,首先接收自己定义的一个逻辑包头,般知道本次需要处理的逻辑业务包有多长了,接着便接收包体。对于非阻塞接收,要是一次接收没有完成,可以将接收的数据保存在一个缓冲区内,等待下一次接包,当接收完成整个逻辑业务包后就进行处理。注意:TCP是采用的流式数据发送,所以一个业务包和另外一个业务包是依次到达(按照你发包的顺序),不会出现第一个业务包内嵌套另外一个业务包的内容(除非你在发送方故意发送),所以请放心接包。

动态缓冲区暂存拆包方式:

之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.
大概过程描述如下:
A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.
B,当接收到数据时首先把此段数据存放在缓冲区中.
C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.
D,根据包头数据解析出里面代表包体长度的变量.
E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.
F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.
这种方法有两个缺点.1.为每个连接动态分配一个缓冲区增大了内存的使用.2.有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.这种拆包的改进方法会解决和完善部分缺点.
下面给出相关代码.

先看包头结构定义

  1. #pragma pack(push,1)    //开始定义数据包, 采用字节对齐方式
  2. /*----------------------包头---------------------*/
  3. typedef struct tagPACKAGEHEAD
  4. {
  5. BYTE Version;
  6. WORD Command;
  7. WORD nDataLen;  //包体的长度
  8. }PACKAGE_HEAD;
  9. #pragma pack(pop) //结束定义数据包, 恢复原来对齐方式

然后看存放数据和"取"数据函数.

[c-sharp] view plaincopy
  1. /*****************************************************************************
  2. Description:添加数据到缓存
  3. Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度
  4. Return: 如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回
  5. TRUE。
  6. ******************************************************************************/
  7. BOOL CDataBufferPool::AddBuff( char *pBuff, int nLen )
  8. {
  9. m_cs.Lock();///临界区锁
  10. if ( nLen < 0 )
  11. {
  12. m_cs.Unlock();
  13. return FALSE;
  14. }
  15. if ( nLen <= GetFreeSize() )///判断剩余空间是否足够存放nLen长的数据
  16. {
  17. memcpy(m_pBuff + m_nOffset, pBuff, nLen);
  18. m_nOffset += nLen;
  19. }
  20. else///若不够则扩充原有的空间
  21. {
  22. char *p = m_pBuff;
  23. m_nSize += nLen*2;//每次增长2*nLen
  24. m_pBuff = new char[m_nSize];
  25. memcpy(m_pBuff,p,m_nOffset);
  26. delete []p;
  27. memcpy(m_pBuff + m_nOffset, pBuff, nLen);
  28. m_nOffset += nLen;
  29. m_cs.Unlock();
  30. return FALSE;
  31. }
  32. m_cs.Unlock();
  33. return TRUE;
  34. }
  35. /*****************************************************************************
[cpp] view plaincopy
  1. /*****************************************************************************
  2. Description:获取一个完整的包
  3. Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度
  4. Return: 1、当前缓冲区不够一个包头的数据 2、当前缓冲区不够一个包体的数据
  5. ******************************************************************************/
  6. int CDataBufferPool::GetFullPacket( char *Buf, int& nLen )
  7. {
  8. m_cs.Lock();
  9. if ( m_nOffset < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
  10. {
  11. m_cs.Unlock();
  12. return 1;
  13. }
  14. PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;
  15. if( (m_nOffset-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲区不够一个包体的数据
  16. {
  17. m_cs.Unlock();
  18. return 2;
  19. }
  20. //判断包的合法性
  21. /* int IsIntegrallity = ValidatePackIntegrality(p);
  22. if( IsIntegrallity != 0 )
  23. {
  24. m_cs.Unlock();
  25. return IsIntegrallity;
  26. }
  27. */
  28. nLen = m_PacketHeadLen+p->nDataLen;
  29. memcpy( Buf, m_pBuff, nLen );
  30. m_nOffset -= nLen;
  31. memcpy( m_pBuff, m_pBuff+nLen, m_nOffset );
  32. m_cs.Unlock();
  33. return 0;
  34. }

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.
用代码来说明.注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改.

[cpp] view plaincopy
  1. int CCircularBufferPool::PutData(TCHAR *pData, int len)
  2. {
  3. if( len <= 0 )
  4. return 1;
  5. EnterCriticalSection(&m_cs);
  6. while (IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
  7. {
  8. BufferResize(len);///若不够,则扩充缓冲区.
  9. }
  10. if (IsIndexOverFlow(len))///判断"尾"指针的位置.
  11. {
  12. int FirstCopyLen = m_iBufSize-m_iTailPos;
  13. int SecondCopyLen = len - FirstCopyLen;
  14. CopyMemory(m_pBuffer+m_iTailPos, pData, FirstCopyLen);
  15. if (SecondCopyLen)
  16. {
  17. CopyMemory(m_pBuffer, pData+FirstCopyLen, SecondCopyLen);
  18. m_iTailPos = SecondCopyLen;
  19. }
  20. else
  21. m_iTailPos = 0;
  22. }
  23. else
  24. {
  25. CopyMemory(m_pBuffer+m_iTailPos, pData, len);
  26. m_iTailPos += len;
  27. }
  28. LeaveCriticalSection(&m_cs);
  29. return 0;
  30. }
  31. void CCircularBufferPool::GetData(TCHAR *pData, int len, bool Delete)
  32. {
  33. if (len < m_iBufSize-m_iHeadPos)
  34. {
  35. CopyMemory(pData, m_pBuffer+m_iHeadPos, len);
  36. if(Delete==true)
  37. m_iHeadPos += len;
  38. }
  39. else
  40. {
  41. int fc, sc;
  42. fc = m_iBufSize-m_iHeadPos;
  43. sc = len - fc;
  44. CopyMemory(pData, m_pBuffer+m_iHeadPos, fc);
  45. if (sc) CopyMemory(pData+fc, m_pBuffer, sc);
  46. if(Delete==true)
  47. m_iHeadPos = sc;
  48. if(m_iHeadPos >= m_iBufSize)
  49. m_iHeadPos = 0;
  50. }
  51. }
  52. //
  53. //进行自定义包的解析
  54. //
  55. int CCircularBufferPool::GetFullPacket( TCHAR *Buf, int &nLen )
  56. {
  57. EnterCriticalSection(&m_cs);
  58. if( GetValidCount() < m_PacketHeadLen )//当前缓冲区不够一个包头的数据
  59. {
  60. LeaveCriticalSection(&m_cs);
  61. return 1;
  62. }
  63. GetData(Buf,m_PacketHeadLen,false);
  64. PACKAGE_HEAD *p = (PACKAGE_HEAD *)Buf;
  65. if( (GetValidCount()-m_PacketHeadLen) < (int)p->nDataLen )//当前缓冲
  66. 区不够一个包体的数据
  67. {
  68. LeaveCriticalSection(&m_cs);
  69. return 2;
  70. }
  71. //判断包的合法性
  72. int IsIntegrallity = ValidatePackIntegrality(p);
  73. if( IsIntegrallity != 0 )
  74. {
  75. LeaveCriticalSection(&m_cs);
  76. return IsIntegrallity;
  77. }
  78. GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
  79. nLen = m_PacketHeadLen+p->nDataLen;
  80. LeaveCriticalSection(&m_cs);
  81. return 0;
  82. }

网络通讯的封包和拆包相关推荐

  1. k8s集群网络(14)-flannel underlay overlay 网络通讯对比

    在前面的几篇文章里我们介绍了基于flannel的underlay网络和overlay网络,包括host-gw模式的underlay网络,基于vxlan的overlay网络,基于udp的overlay网 ...

  2. 使用BeetleX构建基础的SSL网络通讯

    BeetleX的使用非常简单,通过Stream的数据流模式可以让你轻松处理网络数据:在处理SSL加密通讯的时候组件的使用也是非常方便,只需要简单的配置证书即可完成基于SSL的网络安全通讯,接下来介绍一 ...

  3. 【网络通讯与网络安全】网络通讯中的随机数如果不随机会怎么样?(RT-Thread技术论坛优秀文章)

    文章目录 1 写在前言 2 问题描述 3 场景再现 3.1 复现环境搭建 3.2 复现问题的说明 4 问题分析 4.1 从大到小:理解软件架构 4.2 从小到大:抛开现象看本质 4.3 要放大招:三板 ...

  4. 如何在Windows系统上用抓包软件Wireshark截获iPhone等网络通讯数据

    http://www.jb51.net/os/windows/189090.html 今天给大家介绍一种如何在Windows操作系统上使用著名的抓包工具软件Wireshark来截获iPhone.iPa ...

  5. 网络通讯面试题及答案

    文章目录 1. BIO 与 NIO 的区别 2. select 与 poll 的区别 3.请概述 OSI 网络模型 4. TCP 和 UDP 的区别 5.请概述 TCP 的三次握手四次挥手机制 6.为 ...

  6. TCP/IP 中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议

    原文地址:http://hi.baidu.com/albyuyrgqgbbhoq/item/65006d2d002ab33195f62ba1 TCP/IP(Transmission Control P ...

  7. 用C#实现基于TCP协议的网络通讯

    TCP协议是一个基本的网络协议,基本上所有的网络服务都是基于TCP协议的,如HTTP,FTP等等,所以要了解网络编程就必须了解基于TCP协议的编程.然而TCP协议是一个庞杂的体系,要彻底的弄清楚它的实 ...

  8. 在网络通讯中应用Protobuf

    Protobuf的设计非常适用于在网络通讯中的数据载体,它序列化出来的数据量少再加上以K-V的方式来存储数据,对消息的版本兼容性非常强:还有一个比较大的优点就是有着很多的语言平台支持.下面讲解一下如何 ...

  9. 家电 计算机和电讯领域 英语,网络通讯及计算机英语词汇.doc

    网络通讯及计算机英语词汇 0-9 3DDS 三维数字交互宽带网络系统 A absolute positioning 绝对寻址 abstract windows toolkit 抽象窗口工具库 accu ...

最新文章

  1. hdoj1242(dfs 剪枝 解法)
  2. 浩鲸科技携手阿里云原生共同打造“场域运营数字化解决方案”
  3. QT Creator应用程序开发——QT程序设计基本知识
  4. 不要仅仅依靠单元测试
  5. flink sql设置并行度_《从0到1学习Flink》—— Flink parallelism 和 Slot 介绍
  6. shell编程题(四)
  7. c语言中width获取窗体宽度,获取屏幕宽高width(),outerWidth,innerWidth,clientWidth的区别...
  8. c++ public 函数名相同_C++虚函数、重载、覆盖
  9. Exchange2010部署 配置证书
  10. TRNSYS与CONTAM3.4耦合过程
  11. word论文排版和写作02:插入算法的伪代码
  12. C++ builder 添加资源文件
  13. 2017ICPC北方邀请赛H题 MJF wants to work(贪心)
  14. 大数据工程师和数据分析师有何区别
  15. 日有所思(5)——校正装置的理解和设计
  16. 常用数据挖掘工具简介
  17. minio操作,文件上传下载
  18. 跟叶子学把妹——教程序猿把妹第七集
  19. 2021年危险化学品经营单位主要负责人考试技巧及危险化学品经营单位主要负责人试题及解析
  20. cc登录怎么显示服务器超时,CC登录常见问题帮助说明

热门文章

  1. elementui中组件el-dialog宽度调整方法
  2. pc端用微信扫一扫实现微信第三方登陆
  3. 苹果海外现金量“超微软和高通之和”
  4. 一份给数据分析小白的指南
  5. 计算机一级b教程execl,全国计算机等级考试一级教程B第四章Excel教案.ppt
  6. CSS设置超链接样式常用
  7. Android通过wifi连接Intermec PB50打印机进行条码打印
  8. 用弗雷歇距离(Fréchet Distance)进行音质和视质度量
  9. JavaScript之内存释放
  10. 世界服务器无限刷钱,辐射4无限刷钱刷物资方法讲解