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

Sus*_*Pal 15 c sockets linux posix network-programming

POSIX.1-2008/2013下载shutdown()的文档:

int shutdown(int socket, int how);

...

shutdown()函数将导致与文件描述符套接字关联的套接字上的全双工连接的全部或部分关闭.

shutdown()函数采用以下参数:

  • socket 指定套接字的文件描述符.

  • how 指定关闭的类型.值如下:

    • SHUT_RD 禁用进一步的接收操作.
    • SHUT_WR 禁用进一步的发送操作.
    • SHUT_RDWR 禁用进一步的发送和接收操作.

...

手动页 shutdown(2) 说,几乎同样的事情.

shutdown()调用导致与sockfd关联的套接字上的全双工连接的全部或部分关闭.如果howSHUT_RD,将不允许进一步接待.如果howSHUT_WR,则不允许进一步传输.如果howSHUT_RDWR,将不允许进一步的接收和传输.

但我认为即使在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
------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)

以下是上表的简要说明.

  • 时间:自测试开始以来经过的时间(以秒为单位).
  • netcat(nc):通过netcat(nc)执行的步骤.Netcat用于侦听端口8888并接受来自我编译为./a.out的C程序的TCP连接.Netcat在这里扮演服务器的角色.它分别在4s,10s和18s之后向C程序发送三条消息"aa","bb"和"cc".
  • C(a.out):由我的C程序执行的步骤编译为./a.out.它在6s,12s,14s,16s,20s和22s过去后执行6次recv()调用.
  • 观察结果:在C程序的输出中观察到的结果.它表明它能够recv()shutdown()成功完成后发送的消息"bb" .查看"12 s"和"20 s"的行.

这是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);
}
Run Code Online (Sandbox Code Playgroud)

上面的代码保存在一个名为的文件中foo.c.

这是一个小的shell脚本,它编译并运行上面的程序,并调用netcat(nc)监听端口8888并使用消息响应客户端aa,bbcc按照上面的表格按特定的时间间隔.以下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
Run Code Online (Sandbox Code Playgroud)

运行上述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
Run Code Online (Sandbox Code Playgroud)

输出显示C程序recv()即使在调用之后也能够接收消息 shutdown().shutdown()呼叫似乎影响的唯一行为是呼叫是recv() 立即返回还是被阻塞等待下一条消息.通常,在此之前shutdown(),recv()呼叫将等待消息到达.但是在shutdown()通话结束后, 当没有新消息时立即recv()返回0.

由于我上面引用的文档,我期待之后的所有recv()调用shutdown()以某种方式失败(例如,返回-1).

两个问题:

  1. 是否在我的实验中观察到了这种行为,即根据POSIX标准以及我上面引用的手册页,recv()呼叫能够接收shutdown()呼叫后发送的新消息shutdown(2)吗?
  2. 为什么在shutdown()调用之后立即recv()返回0而不是等待新消息到达?

Ben*_*Ben 6

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

关机标准

文档shutdown说:

shutdown()函数禁用套接字上的后续发送和/或接收操作,具体取决于how参数的值.

这似乎意味着没有其他read电话会返回任何数据.

但是国家的文件recv:

如果没有可用的消息被接收并且对等体已经执行了有序关闭,则recv()将返回0.

一起阅读这些可能意味着在远程对等方呼叫之后 shutdown

  1. recv如果数据可用,则调用应返回错误,或
  2. 如果"可以接收消息" recv,shutdown则呼叫可以继续返回数据.

虽然这有点含糊不清,但第一种解释没有意义,因为不清楚错误的用途是什么.因此,正确的解释是第二个.

(请注意,任何在堆栈中任何位置缓冲的协议都可能包含传输中尚无法读取的数据.这些语法shutdown使您在调用后仍能接收此数据shutdown.)

然而,这指的是对等呼叫shutdown,而不是呼叫进程.如果调用进程调用,这是否也适用shutdown

它是合规的还是什么的

标准含糊不清.

如果进程调用 shutdown(fd, SHUT_RD)被认为等同于对调用,shutdown(fd, SHUT_WR)则它是兼容的.

另一方面,严格阅读文本,似乎不符合要求.但后来有对于其中一个进程调用的情况下没有错误代码recvshutdown(SHUT_RD).错误代码是详尽的,这意味着此方案不是错误,因此应该返回0对等方调用的相应情况shutdown(SHUT_WR).

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

如果这是模棱两可的,则应将其视为标准中的错误.

为什么后期shutdown recv数据不限于传输中的数据

在一般情况下,无法知道传输中的数据.

  • 在unix套接字的情况下,数据可以在接收方,操作系统或发送方缓冲.
  • 在TCP的情况下,数据可以由接收过程,操作系统,网卡硬件缓冲器缓冲,分组可以在中间路由器中传输,由发送网卡硬件缓冲,通过发送操作系统或发送处理.

背景

  • POSIX提供了与不同类型的流,包括匿名管道,命名管道,以及IPv4和IPv6的TCP和UDP套接字...和原始以太网和令牌环和IPX/SPX和X.25和ATM均匀交互的API. ..

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

  • 但是,并非所有协议都支持所有功能

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

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

  • 从通话中返回错误,但否则忽略它.

  • 返回成功,并做最接近的事情.

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

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