有关recv端数据流分割问题的一点思考
抛开一些复杂因素,有助于清晰思路,更容易找到解决问题的办法,所以决定从最简单、也相当富有代表性的阻塞套接字开始研究,省略掉一些对所涉问题关系不是很大的代码,保留关键部分,如下:
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字节或两字节的数据,这时候去读包头长度字段就会读到脏数据,错误的认为这个包非常巨大,甚至长度为负数,所以我们不得不在处理的时候加以细致的判断。
这个问题,可以参考一篇文章,地址是: 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;
}
上一篇: python json库序列化支持中文
推荐阅读