关于recv和读取缓冲区-C Berkeley套接字

10

我正在使用伯克利套接字和TCP(SOCK_STREAM套接字)。

过程如下:

  1. 连接到远程地址。
  2. 向其发送一条消息。
  3. 从其收到一条消息。

假设我正在使用以下缓冲区:

char recv_buffer[3000];
recv(socket, recv_buffer, 3000, 0);

问题如下:

  • 调用recv第一次后,如何知道读取缓冲区是否为空?如果不为空,则必须再次调用recv,但是如果在缓冲区为空时进行调用,将会阻塞很长时间。
  • 如何知道已经读取了多少字节到recv_buffer中?我不能使用strlen,因为我接收到的消息可能包含null字节。

谢谢。

4个回答

12
你可以使用 selectpoll 系统调用以及你的套接字描述符来判断是否有数据等待从套接字中读取。不过通常应该有一项约定俗成的协议,发送方和接收方都遵循该协议,以便双方知道要传输多少数据。例如,可能发送方首先发送一个2字节的整数表示它将发送的字节数量。接收方然后先读取这个2字节的整数,以便知道要从套接字中读取多少字节。不管怎样,在如下情况下,强大的应用程序应该在每次调用 recv(或使用非阻塞套接字)之前结合头部长度信息和轮询套接字的额外数据来使用:这将防止你的应用程序在以下情况下被阻塞:例如,你从标题中知道仍应该有100字节剩余要读取,但对等方由于某种原因(也许对等方计算机意外关闭)未能发送数据,从而导致你的 recv 调用被阻塞。

我如何知道在第一次调用recv之后读取缓冲区是否为空?如果它不为空,我必须再次调用recv,但如果缓冲区为空,那么我会让它阻塞很长时间。

recv 系统调用将返回读取的字节数,如果发生错误则返回-1。从 recv(2)的手册页面中可以看出:

[接收] 返回已读取的字节数,或者如果发生错误,则返回-1。

如何知道我已经读取了多少字节到recv_buffer中?我不能使用strlen,因为我接收到的消息可能包含空字符。

返回值为接收到的数据字节数,如果出现错误则返回-1。当对等方执行有序关闭时,返回值将为0。


read(2)手册页与recv(2)有什么关联性?它们说的话很相似,但引用相关页面会更好。 - Jonathan Leffler
2
@Jonathan,当描述符类型为套接字时,readrecv相同,除了recv允许额外的标志参数。但我编辑了我的答案,使用recv来避免混淆。 - Charles Salvia
只是一个小问题,关于一个微妙的、可能是无意的暗示:"select/poll / however message-length in header"错误地暗示这样的头文件解决了阻塞问题,而实际上应该将select/poll、非阻塞套接字或线程与消息长度头文件或哨兵数据结合使用。 - Tony Delroy

2

如何知道第一次调用 recv 后读取缓冲区是否为空?

即使是第一次(在接受客户端后),如果客户端连接已丢失,则 recv 可能会阻塞并失败。你必须:

  • 使用 select 或 poll(BSD sockets)或某些特定于操作系统的等效方法,它可以告诉你特定套接字描述符上是否有可用数据(以及异常条件和可以写更多输出的缓冲区空间)
  • 将套接字设置为非阻塞状态,这样 recv 将仅返回立即可用的内容(可能为空)
  • 创建一个线程,允许其 block 接收数据,同时其他线程将继续进行你关心的其他工作

如何知道我读取到了多少字节进入 recv_buffer?我不能使用 strlen,因为我接收到的消息可能包含 null 字节。

recv() 返回读取的字节数,或者在错误时返回 -1。

请注意,TCP 是一个“字节流”协议,这意味着您只能保证按正确顺序从中读取和写入字节,但无法保证消息边界得到保留。所以即使发送方已经向他们的套接字进行了大量的单向写入,它也可能在传输过程中被分段并以几个较小的块到达,或者几个较小的 send() /write() 可以通过一个 recv()/read() 检索和合并。

因此,请确保循环调用 recv,直到获得所有所需的数据(例如可以处理的完整逻辑消息)或发生错误。您应该准备好/能够处理从客户端获取部分/全部后续 send 的情况(如果您没有协议,在其中每一方仅在从另一方获取完整消息后才发送,并且未使用带有消息长度的标头)。请注意,对消息头(包含长度)和消息体进行接收可能会导致更多的对 recv() 的调用,可能会对性能产生不利影响。

这些可靠性问题通常被忽略。当在单个主机、可靠和快速的 LAN 上运行时,涉及的路由器和交换机较少,消息较少或无并发时,它们出现的频率会较低。然而,当在负载下和更复杂的网络中运行时,它们可能会失效。


0

使用 FIONREAD 选项的 ioctl() 函数可以告诉你当前可以无阻塞地读取多少数据。


ioctl()函数并不是POSIX标准的一部分,尽管它出现在Single UNIX规范的STREAMS部分中作为一个过时的接口(参见ioctl())。实际上,在大多数UNIX衍生平台上都可以使用,但它相当于特定于平台。 - Jonathan Leffler
@Jonathan Leffler:同意(虽然OP没有提到POSIX)。FIONREAD或其变体得到了广泛支持,以至于Java可以在其所有平台上的套接字上提供available()。 - user207421

0
  1. 如果recv()返回少于3000个字节,则可以假定读取缓冲区为空。如果它在您的3000字节缓冲区中返回3000字节,则最好知道是否继续。大多数协议包括TLV(类型、长度、值)的某些变体。每个消息都包含消息类型的指示器、一些长度(如果长度固定,则可能由类型隐含),以及该值。如果在读取接收到的数据时发现最后一个单元不完整,则可以假定还有更多内容需要读取。您还可以将套接字转换为非阻塞套接字;然后,如果没有数据可供读取,则recv()将失败并显示EAGAIN或EWOULDBLOCK。

  2. recv()函数返回读取的字节数。


不正确。你可以假设读取操作已经清空了接收缓冲区,但是在下一次调用 recv() 之前不能假设没有数据到达。 - user207421
@EJP:'wrong' 是一个非常强烈的说法 - 在我看来,你无法确定数据是否已经在你的 recv() 调用之后到达,但也许这种基本、显而易见的陈述确实需要指出。 - Jonathan Leffler

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