有关recv端数据流分割问题的一点思考

xiaoxiao2021-02-27  364

       不管阻塞接收还是异步接收,总是绕不开数据流的分割问题。然而不知道什么原因,网上竟然很难找到现成、合用的代码,也许这种问题不值得付诸于代码,或者不太容易写出普遍通用的代码吧,但不论如何,轮到自己,个人觉得还是应该细致的做个总结。        抛开一些复杂因素,有助于清晰思路,更容易找到解决问题的办法,所以决定从最简单、也相当富有代表性的阻塞套接字开始研究,省略掉一些对所涉问题关系不是很大的代码,保留关键部分,如下: int i_save_size;//尾巴数据长度 char c_save[2000];//保存上次接收的尾巴数据 char c_recv[2000];//数据接收缓冲区 u_long _stdcall thread_recv(void *arg) { ... ... while(TRUE) { i_eax=::recv(h,c_recv,2000,0); if(SOCKET_ERROR==i_eax) { ::closesocket(h); break; } data_patition(c_recv,i_eax,c_save,&i_save_size);//分割数据流 } return 0; } 假设数据包格式为:4字节包头+不定长包体,那么我们假设一种情况,recv拷贝到长度2000字节的数据,其中包括两个整包(1500字节,498字节),一个残缺包(2字节),这种情况当然需要做一些处理,处理过程如下: int data_patition(char *c_data,int i_eax,char *c_save,int *p_save_size) { int i_rest=i_eax; char *p_rest=c_data; int i_size; …… //上来应该先处理前次接收所剩的尾巴数据,这里先省略,重点考虑第一次接收时的初始情况 while(TRUE) { i_size=*(int*)p_rest;//读取数据包长度(直接读取长度可能是有问题的) on_oprate_complete_data(p_rest,i_size);//将一个完整数据包送去处理(这种做法也有问题) p_rest+=i_size;//指针跳过一个数据包的长度 i_rest-=i_size;//剩余数据长度自减一个数据包的长度 if(i_rest<i_size) { ::memcpy(c_save,p_rest,i_rest); *p_save_size=i_rest; return 1;//缓存起来,留待下次处理 } } } 注释内容提到直接读取数据包长度然后送去处理是有问题的,问题无非就是包有没有收全,没收全的话,访问肯定越界了,但是,可不可能出现没收全的情况呢? 我们先从send端来考虑问题。 send函数有可能出现拷贝不完全的情况,MSDN上将send函数返回值说得很明确,返回的实际长度有可能小于请求的数据长度,换句话说,一个包装好、长度为1000字节的数据包,调用send提交给协议层,有可能只提交出去800字节,TCP协议只保证这800字节能够完整的发送到recv端。 这么一来,直接读取包头,然后就处理数据包显然是有问题的。 只是到目前为止,还没有遇到过send提交不完全的问题,网上也只能见到有人提问,问的是有没有那种可能,并没看到有人证实自己遇到了,也就更不会有人给出有说服力的证据了。 最重要的是,微软给出的所有例子(我读过的)都根本没有检查send的返回值,当然也就更不会循环发送了。

于是,我们有理由认为,send发送不完全的情况,虽然有可能发生,但可以不多去考虑,发生就直接closesocket,此来可以避免很多刁钻而繁琐发送、接收问题(同样的发送问题,也会再WSASend函数中遇到,IOCP模型中,这个问题尤为突出)。

如果考虑到send有可能发送不完全,那么处理起来就有点麻烦了,特殊情况下,我们无法否认send连一个4字节的包头都不能完整发送,从而导致recv端一次只收到1字节或两字节的数据,这时候去读包头长度字段就会读到脏数据,错误的认为这个包非常巨大,甚至长度为负数,所以我们不得不在处理的时候加以细致的判断。

简单起见,还是先只以阻塞套接字为模型寻找某种可能性,比如,假设对端提交了一个完整包,由TCP协议负责发送到本地,这种情况有没有可能导致recv读取不完全呢? 这个问题,可以参考一篇文章,地址是: http://www.cnitblog.com/donne/archive/2010/12/23/72500.html 文中有这么一段话,个人觉得其对socket编程最为有用,不过,由于原作者并没有认真组织语言,文章看起来有点磕,为了阅读性,这里尽量在不曲解本意的原则下,作了点修改: 如果调用socket函数send,阻塞发送大于1452字节的数据,那么发送端的IP协议层就会将数据分片,而接收端的IP协议层负责接收并重组数据,如果一个分片丢失,则整个TCP包都会重发。 也就是说,只要对端send没有出问题,本地recv申请读取的数据长度足够,还是可以得到完整数据包的。 想想看,只要对端send函数没出问题,ACK机制保证了数据能够完整的提交给上层,进入本地缓冲区,recv函数只是从中读取而已,那么只要请求读取的字节数足够,那么有什么理由只复制回来半个数据包? 缓冲区不足的话,一整个数据包都应该全丢了,读半个包是什么情况?

所以,综上所述,尾巴数据的问题,最极端的情况,是由对端send函数造成,带来的麻烦也最大,更普遍的情况,则是recv函数的接收缓冲区与系统缓冲区之间的问题,recv请求复制1000字节,系统缓冲区有1300字节,最后一个数据包500字节,那recv函数将1000字节复制过来,当然会把最后一个数据包会给拦腰斩断。

//如果不考虑send发送不完全的情况,接受缓冲区长度设置为4的整数倍,每个数据包长度也都是4的整数倍,连个包头都收不齐的情况应该可以避免

unsigned long _stdcall recv_func(void *arg) { SOCKET s=(SOCKET)arg; char *c_str=new char[2000]; char *c_cac=new char[2000]; int i_cac=0; while(TRUE) { int i_eax=::recv(s,c_str,2000,0); if(i_eax<=0) { ::printf("error(%d): can not recv data\n",::GetLastError()); break; } char *p=c_str; int i_ecx; if(i_cac>0) { i_ecx=*(int*)c_cac; int i_dis=i_ecx-i_cac; ::memcpy(c_cac+i_cac,p,i_dis); on_data(c_cac,i_ecx); p+=i_dis; i_eax-=i_dis; i_cac=0; } while(i_eax>0) { i_ecx=*(int*)p; if(i_ecx>i_eax) { ::memcpy(c_cac,p,i_eax); i_cac=i_eax; break; } on_data(p,i_ecx); p+=i_ecx; i_eax-=i_ecx; } } return 0; }

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

上面红字内容有严重错误,特更正如下:

recv端会因为某些不知名的原因,即便系统缓冲区内容足够,请求拷贝的数据长度也足够,还是会有数据拷贝不全的情况发生,这将导致若干次recv也未必能够凑成一个完整的数据包,于是,代码还需进一步修改,更正如下:

//c_cac:字符数组,缓存,用来存放上次没有处理完的数据,又称半包缓冲区

//i_cac:已经缓存下来的数据长度

unsigned long _stdcall recv_func(void *arg) { ...... char *c_str=new char[2000]; char *c_cac=new char[2000]; int i_cac=0; while(TRUE) { i_eax=::recv(s,c_str,2000,0); if(i_eax<4) { ::closesocket(s); break; } char *p=c_str; int i_ecx; int i_dis; if(i_cac>0) { if(i_cac<4) { i_dis=4-i_cac; ::memcpy(c_cac+i_cac,p,i_dis); i_cac=4; p+=i_dis; i_eax-=i_dis; } i_ecx=*(int*)c_cac; i_dis=i_ecx-i_cac; if(i_dis>i_eax) { ::memcpy(c_cac+i_cac,p,i_eax); i_cac+=i_eax; continue; } ::memcpy(c_cac+i_cac,p,i_dis); on_data(c_cac,i_ecx); p+=i_dis; i_eax-=i_dis; i_cac=0; } while(i_eax>0) { if(i_eax<4) { ::memcpy(c_cac,p,i_eax); i_cac=i_eax; break; } i_ecx=*(int*)p; if(i_ecx>i_eax) { ::memcpy(c_cac,p,i_eax); i_cac=i_eax; break; } on_data(p,i_ecx); p+=i_ecx; i_eax-=i_ecx; } } delete []c_cac; delete []c_str; return 0; }

转载请注明原文地址: https://www.6miu.com/read-5005.html

最新回复(0)