欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-02 13:03:42
...
       不管阻塞接收还是异步接收,总是绕不开数据流的分割问题。然而不知道什么原因,网上竟然很难找到现成、合用的代码,也许这种问题不值得付诸于代码,或者不太容易写出普遍通用的代码吧,但不论如何,轮到自己,个人觉得还是应该细致的做个总结。


       抛开一些复杂因素,有助于清晰思路,更容易找到解决问题的办法,所以决定从最简单、也相当富有代表性的阻塞套接字开始研究,省略掉一些对所涉问题关系不是很大的代码,保留关键部分,如下:


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;
}