为什么客户端调用shutdown(sockfd, SHUT_RD)后,客户端程序中的recv()可以接收到发送给客户端的消息?

Why can recv() in the client program receive messages sent to the client after the client has invoked shutdown(sockfd, SHUT_RD)?

来自 POSIX.1-2008/2013 documentation of shutdown():

int shutdown(int socket, int how);

...

The shutdown() function shall cause all or part of a full-duplex connection on the socket associated with the file descriptor socket to be shut down.

The shutdown() function takes the following arguments:

  • socket Specifies the file descriptor of the socket.

  • how Specifies the type of shutdown. The values are as follows:

    • SHUT_RD Disables further receive operations.
    • SHUT_WR Disables further send operations.
    • SHUT_RDWR Disables further send and receive operations.

...

手册页 shutdown(2) 说的差不多。

The shutdown() call causes all or part of a full-duplex connection on the socket associated with sockfd to be shut down. If how is SHUT_RD, further receptions will be disallowed. If how is SHUT_WR, further transmissions will be disallowed. If how is SHUT_RDWR, further receptions and transmissions will be disallowed.

但我认为即使在 shutdown(sockfd, SHUT_RD)打电话。这是我精心策划的测试 我观察到的结果。

------------------------------------------------------
Time  netcat (nc)  C (a.out)   Result Observed
------------------------------------------------------
 0 s  listen       -           -
 2 s               connect()   -
 4 s  send "aa"    -           -
 6 s  -            recv() #1   recv() #1 receives "aa"
 8 s  -            shutdown()  -
10 s  send "bb"    -           -
12 s  -            recv() #2   recv() #2 receives "bb"
14 s  -            recv() #3   recv() #3 returns 0
16 s  -            recv() #4   recv() #4 returns 0
18 s  send "cc"    -           -
20 s  -            recv() #5   recv() #5 receives "cc"
22 s  -            recv() #6   recv() #6 returns 0
------------------------------------------------------

这里简单介绍一下上面的table.

这里是C程序(客户端程序)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>

int main()
{
    struct addrinfo hints, *ai;
    int sockfd;
    int ret;
    ssize_t bytes;
    char buffer[1024];

    /* Select TCP/IPv4 address only. */
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    if ((ret = getaddrinfo("localhost", "8888", &hints, &ai)) == -1) {
        printf("getaddrinfo() error: %s\n", gai_strerror(ret));
        return EXIT_FAILURE;
    }

    if ((sockfd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol)) == -1) {
        printf("socket() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    /* Connect to localhost:8888. */
    sleep(2);
    if ((connect(sockfd, ai->ai_addr, ai->ai_addrlen)) == -1) {
        printf("connect() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }

    freeaddrinfo(ai);

    /* Test 1: Receive before shutdown. */
    sleep(4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #1 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    sleep(2);
    if (shutdown(sockfd, SHUT_RD) == -1) {
        printf("shutdown() error: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }
    printf("shutdown() complete\n");

    /* Test 2: Receive after shutdown. */
    sleep (4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #2 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 3. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #3 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 4. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #4 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 5. */
    sleep (4);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #5 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);

    /* Test 6. */
    sleep (2);
    bytes = recv(sockfd, buffer, 1024, 0);
    printf("recv() #6 returned %d bytes: %.*s\n", (int) bytes, (int) bytes, buffer);
}

以上代码保存在名为 foo.c.

的文件中

这是一个很小的 ​​shell 脚本,它编译并 运行 上面的程序和 调用 netcat (nc) 监听 8888 端口并响应客户端 根据 table 在特定时间间隔发送消息 aabbcc 如上所示。以下 shell 脚本保存在名为 run.sh.

set -ex
gcc -std=c99 -pedantic -Wall -Wextra -D_POSIX_C_SOURCE=200112L foo.c
./a.out &
(sleep 4; printf aa; sleep 6; printf bb; sleep 8; printf cc) | nc -vvlp 8888

当上面的shell脚本为运行时,观察到如下输出

$ sh run.sh 
+ gcc -std=c99 -pedantic -Wall -Wextra -D_POSIX_C_SOURCE=200112L foo.c
+ nc -vvlp 8888
+ sleep 4
listening on [any] 8888 ...
+ ./a.out
connect to [127.0.0.1] from localhost [127.0.0.1] 54208
+ printf aa
+ sleep 6
recv() #1 returned 2 bytes: aa
shutdown() complete
+ printf bb
+ sleep 8
recv() #2 returned 2 bytes: bb
recv() #3 returned 0 bytes: 
recv() #4 returned 0 bytes: 
+ printf cc
recv() #5 returned 2 bytes: cc
recv() #6 returned 0 bytes: 
 sent 6, rcvd 0

输出显示 C 程序能够接收消息 recv() 即使在调用 shutdown() 之后。唯一的行为 shutdown() 调用似乎影响了 recv() 立即呼叫 returns 或被阻止等待下一条消息。 通常,在 shutdown() 之前,recv() 调用会等待一个 消息到达。但是在 shutdown() 调用之后,recv() returns 0 当没有新消息时立即。

我原以为 shutdown() 之后的所有 recv() 调用都会以某种方式失败(比如, return -1) 由于我在上面引用的文档。

两个问题:

  1. 是在我的实验中观察到的行为,即 recv() 能够接收 shutdown() 调用正确后发送的新消息 我拥有的 POSIX 标准和 shutdown(2) 的手册页 上面引用?
  2. 为什么调用shutdown()后,recv()returns0,而不是等待新消息到达?

你问了两个问题:是否符合posix标准,为什么recv return 0而不是阻塞

关机标准

shutdown 的文档说:

The shutdown() function disables subsequent send and/or receive operations on a socket, depending on the value of the how argument.

这似乎意味着进一步的 read 调用不会 return 任何数据。

但是 recv 的文档指出:

If no messages are available to be received and the peer has performed an orderly shutdown, recv() shall return 0.

一起阅读这些内容可能意味着在远程对等方调用 shutdown

之后
  1. 如果数据可用,调用 recv 应该 return 一个错误,或者
  2. 如果 "messages are available to be received".
  3. ,对 recv 的调用可以在 shutdown 之后继续 return 数据

虽然这有点模棱两可,但第一种解释没有意义,因为不清楚错误的用途。所以正确的解释是第二种。

(请注意,在堆栈中的任何位置进行缓冲的任何协议都可能具有传输中的数据,这些数据尚无法读取。shutdown 的语义使您能够在调用 shutdown.)

然而,这是指对等方调用 shutdown,而不是调用进程。如果调用进程调用 shutdown,这是否也适用?

那么合规还是什么

标准不明确。

如果进程调用 shutdown(fd, SHUT_RD) 被认为等同于 peer 调用 shutdown(fd, SHUT_WR) 那么它是兼容的。

另一方面,严格看文,好像不太符合。但是对于进程在 shutdown(SHUT_RD) 之后调用 recv 的情况,没有错误代码。错误代码是详尽的,这意味着这种情况不是错误,所以应该 return 0 就像对等方调用 shutdown(SHUT_WR).

的相应情况一样

不过,这就是您想要的行为 - 如果您需要,可以接收传输中的消息。如果你不想他们不要打电话给 recv

在某种程度上这是模棱两可的,它应该被认为是标准中的一个错误。

为什么 post-shutdown recv 数据不限于传输中的数据

在一般情况下,无法知道传输中的数据是什么。

  • 在 unix 套接字的情况下,数据可能在接收方、操作系统或发送方进行缓冲。
  • 在TCP的情况下,数据可能被接收进程、操作系统、网卡硬件缓冲区缓冲,数据包可能在中间路由器传输,被发送网卡硬件缓冲,通过发送操作系统或发送进程。

背景

  • posix 提供了一个 api 用于与不同类型的流进行统一交互,包括匿名管道、命名管道以及 IPv4 和 IPv6 TCP 和 UDP 套接字...和原始以太网、令牌环和 IPX/SPX、X.25 和 ATM...

  • 因此 posix 提供了一组功能,广泛涵盖了大多数流媒体和基于数据包的协议的主要功能。

  • 然而,并非所有协议都支持所有功能

从设计的角度来看,如果调用者请求底层协议不支持的操作,有多种选择:

  • 进入错误状态,并禁止对文件描述符进行任何进一步的操作。

  • Return 调用错误,否则忽略它。

  • Return 成功,做最近有意义的事情。

  • 实施某种包装器或填充器以提供缺少的功能。

posix 标准排除了前两个选项。显然,Linux 开发人员选择了第三个选项。