澳门新葡萄京娱乐场 2

澳门新葡萄京娱乐场网络通讯的封包和拆包

某大公司接了一个政府部门的软件业务,这个软件的其中一个需求就是:提供一个接口,让政府部门的子部门通过接口向软件中传输数据。

转载: 

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

该公司设计了一个同步方案,文档中提到:

网络编程:要以多线程异步的方式来考虑问题。

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

  • 先发送一个包头:1 代表新增、2 代表修改、3 代表删除。
  • 再发送一个包体:包体是个 XML 格式,其中规定了具体业务数据,比如:姓名节点是必填项、出生年月是必填项。接口在接收到包体后,先校验
    XML,校验不通过,不允许执行包头的操作。

比如我用tcp发送数据包的时候:一般定义包头,和包体的方式

TCP是个”流”协议,所谓流,就是没有界限的一串数据,大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。由于TCP”流”的特性以及网络状况,在进行数据传输时会出现以下几种情况。

看似没有问题,但当我们要删除时,姓名、出生年月仍然是必填项,大哥,这不是我们的风格呀,删除情况下,在我们村都是只传 ID 的呀。

包头中一般包括:

假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).

包头的特殊字段(magic):用来指明包头的开始(定义好之后,包头一定要是固定长度,又用取到完整长度的包头)。主要是防止粘包。

A.先接收到data1,然后接收到data2.

比如在接受方读取数据的时候,一个半的数据包己经到达,一个通过特殊字段可以判断我数据开始就是包头,第二个是我可以得到后边的半包的开始。如果我包头和包体封装到一块,数据长度就不确定,我要是全部取出来,岂不是认为我这一个半的数据包就是一个数据包。解析就是有问题的

B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.

sequence字段:这个用于标识发送的数据包的序列号

C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.

比如我异步多线程方式发送数据,每一个数据包有一个sequence,在我发送过去到服务器之后,由于一般我的每个包的功能不一样,解析不同,执行的功能不同,所以我反馈给前端的信息的时间先后不一样,这样我怎么知道我这个反馈信息是我那个请求的反馈。就是通过sequence

D.一次性接收到了data1和data2的全部数据.

在比如我同一个命令控制字段,发送了多个(3个)请求数据包,由于服务器反馈回你的信息的系列不确定,假如服务器解析两个正确一个不正确,反馈回去之后,光有命令控制字段,也不能判断是那个正确,那个错误.  (觉得这里可以类似:
TCP 三次握手中序列号的使用方法; 它的底层通信序列号的使用办法.
发送端和接收端各维护自己的一个序列号, 用于主动发送数据包;
同时给对端应答回复时, 使用接收到的对端数据包序列号.)

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

命令控制字:用来标识这段数据的功能用途。

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

包体的信息长度:用于指明包体的长度。

 

一般情况下是包体和包头一块发送,接收的时候(先读包头,正确,再读包体),是先读取完整的包头(因为的固定的长度,所以可以取到完整的包头)。解析包头里边的包体长度,和crc校验值。然后通过包体长度。在读取包体,crc校验值的比较,判断包体是否正确,如果不正确,就直接丢弃。如果正确,再按照获得的包体长度读取完整的包体信息。

澳门新葡萄京娱乐场,为什么会出现B.C.D的情况

包体信息的crc校验值字段:主要是为了保证我传输过来的数据是正确的。传输过来的数据在本地做一个crc校验,然后和这个校验值比较,如果一样,说明数据是正确的。

1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.
C和D的情况就有可能是Nagle算法造成的.

自己认为重要的包体的信息:自己定义

2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据.

 

 

二.TCP底层通信协议序列号的使用方法

怎样封包和拆包

建立连接后,两台主机就可以相互传输数据了。如下图所示:

最初遇到”粘包”的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决。这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠。后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决B的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷.
再后来就是对数据包进行封包和拆包的操作。

澳门新葡萄京娱乐场 1
图1:TCP 套接字的数据交换过程

 

上图给出了主机A分2次(分2个数据包)向主机B传递200字节的过程。首先,主机A通过1个数据包发送100个字节的数据,数据包的
Seq 号设置为 1200。主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack
号设置为 1301。

封包

为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

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

此时 Ack 号为 1301 而不是 1201,原因在于 Ack
号的增量为传输的数据字节数。假设每次 Ack
号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认
Ack 号:

 

Ack号 = Seq号 + 传递的字节数 + 1

拆包

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。

对于拆包目前我最常用的是以下两种方式:

下面分析传输过程中数据包丢失的情况,如下图所示:

(1)动态缓冲区暂存方式。之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。

澳门新葡萄京娱乐场 2
图2:TCP套接字数据传输过程中发生错误

大概过程描述如下:

上图表示通过 Seq 1301
数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于
Seq 1301 的ACK确认,因此尝试重传数据

A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

B,当接收到数据时首先把此段数据存放在缓冲区中.

C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

D,根据包头数据解析出里面代表包体长度的变量.

E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

F,取出整个数据包.这里的”取”的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

 

这种方法有两个缺点.

1) 为每个连接动态分配一个缓冲区增大了内存的使用.

2) 有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.第二种拆包的方法会解决和完善这些缺点.

前面提到过这种方法的缺点.下面给出一个改进办法,
即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.

环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.

 

(2)利用底层的缓冲区来进行拆包

由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化。

对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。

编程实现见:


这个问题产生于编程中遇到的几个问题:

1、使用TCP的Socket发送数据的时候,会出现发送出错,WSAEWOULDBLOCK,在TCP中不是会保证发送的数据能够安全的到达接收端的吗?也有窗口机制去防止发送速度过快,为什么还会出错呢?

2、TCP协议,在使用Socket发送数据的时候,每次发送一个包,接收端是完整的接受到一个包还是怎么样?如果是每发一个包,就接受一个包,为什么还会出现粘包问题,具体是怎么运行的?

3、关于Send,是不是只有在非阻塞状态下才会出现实际发送的比指定发送的小?在阻塞状态下会不会出现实际发送的比指定发送的小,就是说只能出现要么全发送,要么不发送?在非阻塞状态下,如果之发送了一些数据,要怎么处理,调用了Send函数后,发现返回值比指定的要小,具体要怎么做?

4、最后一个问题,就是TCP/IP协议和Socket是什么关系?是指具体的实现上,Socket是TCP/IP的实现?那么为什么会出现使用TCP协议的Socket会发送出错。


这个问题第1个回答:

1应该是你的缓冲区不够大,

2 tcp是流,没有界限.也就没所谓的包.

3阻塞也会出现这种现象,出现后继续发送没发送出去的.

4tcp是协议,socket是一种接口,没必然联系.错误取决于你使用接口的问题,跟tcp没关系.


这个问题第2个回答:

1、应该不是缓冲区大小问题,我试过设置缓冲区大小,不过这里有个问题,就是就算我把缓冲区设置成几G,也返回成功,不过实际上怎么可能设置那么大

3、出现没发送完的时候要手动发送吧,有没有具体的代码实现?

4、当选择TCP的Socket发送数据的时候,TCP中的窗口机制不是能防止发送速度过快的吗?为什么Socket在出现了WSAEWOULDBLOCK后没有处理?


这个问题第3个回答:

1.在使用非阻塞模式的情况下,如果系统发送缓冲区已满,并示及时发送到对端,就会产生该错误,继续重试即可。

3.如果没有发完就继续发送后续部分即可。


这个问题第4个回答:

1、使用非阻塞模式时,如果当前操作不能立即完成则会返回失败,错误码是WSAEWOULDBLOCK,这是正常的,程序可以先执行其它任务,过一段时间后再重试该操作。

2、发送与接收不是一一对应的,TCP会把各次发送的数据重新组合,可能合并也可能拆分,但发送次序是不变的。

3、在各种情况下都要根据send的返回值来确定发送了多少数据,没有发送完就再接着发。

4、socket是Windows提供网络编程接口,TCP/IP是网络传输协议,使用socket是可以使用多种协议,其中包括TCP/IP。


这个问题第5个回答:

发送的过程是:发送到缓冲区和从缓冲区发送到网络上

WSAEWOULDBLOCK和粘包都是出现在发送到缓冲区这个过程的

发表评论

电子邮件地址不会被公开。 必填项已用*标注