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

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

int shutdown(int socket, int how);

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

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

  • socket指定socket的文件描述符。

  • how指定关机的types。 数值如下:

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

shutdown(2)手册页说几乎相同的事情。

shutdown()调用会导致与sockfd关联的套接字上全部或部分全双工连接closures。 如果SHUT_RD how ,将不允许进一步的接待。 如果SHUT_WR how ,进一步传输将被禁止。 如果SHUT_RDWR how ,进一步的接收和传输将被禁止。

但是我想即使在shutdown(sockfd, SHUT_RD)调用之后我也能够接收数据。 这是我精心策划的testing和我观察到的结果。

 ------------------------------------------------------ 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 ------------------------------------------------------ 

以上是对上表的简要说明。

  • 时间:自testing开始以来经过的时间(以秒为单位)。
  • netcat(nc):通过netcat(nc)执行的步骤。 Netcat是在8888端口上进行监听,并接受来自C程序编译为./a.out的TCP连接。 Netcat在这里扮演着服务器的angular色。 它分别在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”。 查看“12s”和“20s”的行。

这是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,并按照上面的表格以特定的时间间隔对消息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程序即使在调用shutdown()之后也能够使用recv()接收消息。 shutdown()调用似乎已经受到影响的唯一行为是recv()调用是立即返回还是被阻塞,等待下一条消息。 通常,在shutdown()之前, recv()调用将等待消息到达。 但是在shutdown()调用之后,当没有新消息时, recv()立即返回0

我期待所有recv()调用后shutdown()以某种方式失败(比如说,返回-1 )由于我已经在上面引用的文档。

两个问题:

  1. 是否在我的实验中观察到的行为,即recv()调用能够接收shutdown()调用正确的新消息,正如根据POSIX标准和上面引用的shutdown(2)的手册页所述的那样?
  2. 为什么在调用shutdown()之后, recv()立即返回0 ,而不是等待新消息到达?

你问了两个问题:它是否符合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)则它是兼容的。

另一方面,严格阅读文本,似乎不符合规定。 但是,在进程在shutdown(SHUT_RD)后调用recv shutdown(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开发者选中了。