填坑之recvfrom实际返回的数据长度小于设置的长度导致数据包解析不正确

问题描述

ps:使用recvfrom接收tcp数据,没错,就是tcp,你没看错,what???,recvfrom?tcp?

接收数据时,收到的实际数据长度小于传入的参数,但由于某种魔性的原因,需要实现没有头没有尾的定长tcp包的解析,都知道tcp就爱粘包,就得折腾你,so,尝试填坑,这个坑没什么难度,只是稍微麻烦一点。

下文来源于网络,暂时借用,后续整理
首先对于recvfrom的原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

返回值为读取到的字节长度,这里有一个坑点,我们在接收时需要传入一个buffer用于拷贝接收到的数据,传入参数包括buffer的首地址和长度,如果这里buffer长度小于这个udp包的长度会如何呢,recvfrom是否会返回一个小于0的值提示我们调用失败呢?测试代码,客户端:

发送一个4096字段的udp包,此外我们不可以设置socket的不分片策略,否则会出现发送失败,提示msg too large,服务端程序如下:

最终运行结果为在服务端收到了2048个字节之后,程序阻塞在了第二次recvfrom这里,第一次没有接收完的部分第二次并不能接收。

 

原因分析
这个需要深入到recvfrom的源码来进行分析,这里只截取了内核源码中的关键部分,如下:

从这里可以看到,当取到这个skb之后,判断了实际包大小与buffer大小关系,取最小值,从而只把部分包COPY到缓存中,其它部分被丢弃了,因此在实际应用中,recvfrom传入的buffer大小应该是一个大于udp单个包大小的值,大于65536,这样的话无论如何都不会出现问题。

其他方法:在使用recvfrom之前先获取一下内核buffer的数据长度,这应该是一个不错的解决办法

设置缓冲区大小

有时候,需要在recv或者是recvfrom函数调用之前,获取缓冲区内数据的长度。

这个时候,数据已经到达了内核缓冲区,但是还没有到达应用程序的MessageBuf,因此是可以获取数据长度的。
 

//Get the Length in the socket before use receive_from function
int sock_nread(SOCKET sck)
{
   int ires;
   ulong nread = 0;

  
   fcntl(sock,F_SETFL,O_NONBLOCK));

#ifdef __WIN32
   ires= ioctlsocket ( sck, FIONREAD, &nread );
 #else
   ires = ioctl(sck,FIONREAD,&nread);
 #endif

   if ( ires != 0 ) 
       return 0;
   return nread;
}

在windows下面可以使用 ioctlsocket ( sck, FIONREAD, &nread )来实现;在Linux下面可以使用ioctl(sck,FIONREAD,&nread)来实现。

问题扩展
在实际应用过程中,我们在进行UDP发包时通常会考虑小于MTU,正常MTU一般为1500,其实如果大于这个值UDP包也是可以正常发送的,在上述测试过程中,抓包结果如下:

可以看到包发出后,实际上发生了IP分片,后两个udp包为分片包,到达源端之后,被IP层组装后再交给UDP层,在实际传输过程中,应该尽量避免底层产生拆包,如果一个分片丢掉的话,整个包都无法交付给上层。
参考:https://blog.csdn.net/itachi0/article/details/85248450

你可能感兴趣的:(网络)