使用C++中的'recv'和'MSG_PEEK'获取套接字中可用字节数

15

C++有以下函数可以从套接字中接收字节,它可以使用MSG_PEEK标志检查可用的字节数。使用MSG_PEEK,'recv'返回的值是套接字中可用字节数:

#include <sys/socket.h>
ssize_t recv(int socket, void *buffer, size_t length, int flags); 

我需要获取套接字中可用的字节数,但不想创建buffer(不想为buffer分配内存)。这是否可能,如何实现?


1
注意:该标志不仅返回可用字节数。MSG_PEEK实际上会将数据复制到给定的缓冲区中! - Christoph Rackwitz
4个回答

39
你需要的是 ioctl(fd,FIONREAD,&bytes_available),在Windows下使用 ioctlsocket(socket,FIONREAD,&bytes_available)
需要注意的是,操作系统不能保证为您缓冲多少数据,因此,如果您要等待大量数据,则最好在数据到达时读取数据并将其存储在自己的缓冲区中,直到你拥有足够处理的所有内容。
通常做法是分块读取数据,例如:
char buf[4096];
ssize_t bytes_read;
do {
     bytes_read = recv(socket, buf, sizeof(buf), 0);
     if (bytes_read > 0) {
         /* do something with buf, such as append it to a larger buffer or
          * process it */
     }
} while (bytes_read > 0);

如果你不想坐在那里等待数据,你应该研究一下selectepoll,以确定何时可以读取数据,如果你想确保永远不会在接收上被阻塞,那么使用套接字的O_NONBLOCK标志非常方便。


套接字接收的数据应该是“连续的字节流”还是“一系列TCP数据包”? - jondinham
1
正确,这就是为什么第一种方法更可取,虽然除非您在以千兆比特每秒的速度运转,否则您永远不会注意到它。 - hexist
1
传入数据会逐渐到达。如果对端发送的数据包很小,它们很可能会一次性全部出现,但这绝不是保证。所以从接收方的角度来看,这是一个持续的字节流,你需要知道何时接收到了所有期望的字节。 - hexist
1
请注意,当使用UDP(即SOCK_DGRAM)时,您应该一次性读取发送的消息,因为部分读取将丢弃多余的长度;因此,在这种情况下,ioctl FIONREAD似乎是正确的方法。 - Klamer Schutte
2
@KlamerSchutte 在SOCK_DGRAM上,您可以使用MSG_PEEK | MSG_TRUNK。Peek表示数据不会被丢弃,trunk表示即使大于缓冲区,也会返回消息的实际大小。然后只需传入一个1字节的缓冲区并忽略它即可。 - Goswin von Brederlow
显示剩余2条评论

3

使用 FIONREAD 时要小心!使用 ioctl(fd, FIONREAD, &available) 的问题在于,在某些系统上它将始终返回套接字缓冲区中可读取的总字节数。

对于流式套接字(TCP)来说,这没有问题,但对于数据报套接字(UDP)来说却会误导人。对于数据报套接字的读取请求被限制为缓冲区中第一个数据报的大小,当读取少于第一个数据报大小时,该数据报的所有未读字节仍将被丢弃。因此,您最好只了解缓冲区中下一个数据报的大小。

例如,在 macOS/iOS 上有文档记录指出,FIONREAD 总是返回总量(请参阅有关 SO_NREAD 的注释)。要仅获取下一个数据报的大小(对于流式套接字则为总大小),您可以使用以下代码:

int available;
socklen_t optlen = sizeof(readable);
int err = getsockopt(soc, SOL_SOCKET, SO_NREAD, &available, &optlen);

在Linux上,FIONREAD文档 只返回UDP套接字中下一个数据报的大小
在Windows上,ioctlsocket(socket, FIONREAD, &available)文档始终提供总大小:
如果传递给s参数的套接字是面向消息的(例如,类型为SOCK_DGRAM),则FIONREAD返回可读取的字节的总数,而不是排队在套接字上的第一个数据报(消息)的大小。 来源:https://learn.microsoft.com/en-us/windows/win32/api/ws2spi/nc-ws2spi-lpwspioctl 我不知道在Windows上如何仅获取第一个数据报的大小。

3
在Windows上,您可以使用带有FIONREAD标志的ioctlsocket()函数来查询套接字可用的字节数,而无需读取/查看实际的字节。返回的值是最小的recv()函数可以不阻塞地返回的字节数。在您实际调用recv()函数时,可能会有更多的字节到达。

1
这个答案完全是错的。文档中很清楚地说明,在Windows上,FIONREAD并不返回从recv获取而没有阻塞的字节数。在Windows上,recv可以从较低层(例如过滤器和带有自己缓冲区的队列)拉取数据,而FIONREAD则不会这样做,只检查顶层。参见第12项 - David Schwartz
2
@DavidSchwartz: 根据MSDN:"*FIONREAD:用于确定网络输入缓冲区中待处理的数据量,可以从套接字s读取... FIONREAD返回可以在单个调用recv函数中读取的数据量,这可能与套接字上排队的总数据量不同。*" - Remy Lebeau
很遗憾,出于我在两个评论中解释的原因,这是不正确的。FIONREAD 调用无法执行任何不可逆的操作,而 recv 可能会返回一个较小的数字。微软对非本机套接字操作的支持在许多方面都存在问题,这就是其中之一。这就是为什么强烈建议旨在在 Windows 上运行的软件使用 Windows API,而不是旨在使用 POSIXy 的那些 API。 - David Schwartz
@DavidSchwartz:我对网络过滤器一无所知,但如果您已经在读取数据,那么它是否通过过滤器并不重要?数据只会朝一个方向流动,并最终进入套接字的输入缓冲区。我不明白为什么反转逻辑会成为限制因素,除非可能使用recv()调用MSG_PEEK,但这不常见,而且我希望它能够通过过滤器读取数据并将其存储在缓冲区中,然后只需窥视缓冲区并将数据留在那里以供稍后的非窥视读取来删除它。 - Remy Lebeau
让我们在聊天中继续这个讨论 - Remy Lebeau
显示剩余4条评论

1
The short answer is : this cannot be done with MS-Windows WinSock2,
as I can discovered over the last week of trying. 

Glad to have finally found this post, which sheds some light on the issues I've been having, using latest Windows 10 Pro, version 20H2 Build 19042.867 (x86/x86_64) :

On a bound, disconnected UDP socket 'sk' (in Listening / Server mode):

1. Any attempt to use either ioctlsocket(sk, FIONREAD, &n_bytes)
   OR WsaIoctl with a shifted FIONREAD argument, though they succeed,
   and retern 0, after a call to select() returns > with that 
  'sk' FD bit set in the read FD set,
   and the ioctl call returns 0 (success), and n_bytes is > 0, 
   causes the socket sk to be in a state where any
   subsequent call to recv(), recvfrom(), or ReadFile() returns 
   SOCKET_ERROR with a WSAGetLastError() of :
   10045, Operation Not Supported, or ReadFile
   error 87, 'Invalid Parameter'.

Moreover, even worse:

2. Any attempt to use recv or recvfrom with the 'MSG_PEEK' msg_flags 
   parameter returns -1 and WSAGetLastError returns :
   10040 : 'A message sent on a datagram socket was larger than 
    the internal message buffer or some other network limit, 
    or the buffer used to receive a datagram into was smaller 
    than the datagram itself.
       ' .

   Yet for that socket I DID successfully call:
setsockopt(s, SOL_SOCKET, SO_RCVBUF, bufsz = 4096 , sizeof(bufsz) )
   and the UDP packet being received was of only 120 bytes in size.

   In short, with modern windows winsock2 ( winsock2.h / Ws2_32.dll) , 
   there appears to be absolutely no way to use any documented API
   to determine the number of bytes received on a bound UDP socket
   before calling recv() / recvfrom() in MSG_WAITALL blocking mode to
   actually receive the whole packet.

   If you do not call ioctlsocket() or WsaIoctl or 
   recv{,from}(...,MSG_PEEK,...) 
   before entering recv{,from}(...,MSG_WAITALL,...) ,
   then the recv{,from} succeeds.

   I am considering advising clients that they must install and run
   a Linux instance with MS Services for Linux under their windows
   installation , and developing some
   API to communicate with it from Windows, so that reliable 
   asynchronous UDP communication can be achieved - or does anyone 
   know of a good open source replacement for WinSock2 ? 

   I need access to a "C" library TCP+UDP/IP implementation for 
   modern Windows 10  that conforms to its own documentation, 
   unlike WinSock2 -  does anyone know of one ?

请问您能否将您的回答格式化一下,我发现很难阅读。 - Героям слава

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接