From 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....
The manual page for
shutdown(2)
says pretty much the same thing.
The
shutdown()
call causes all or part of a full-duplex connection on the socket associated with sockfd to be shut down. Ifhow
isSHUT_RD
, further receptions will be disallowed. Ifhow
isSHUT_WR
, further transmissions will be disallowed. Ifhow
isSHUT_RDWR
, further receptions and transmissions will be disallowed.
But I think I am able to receive data even after a
shutdown(sockfd, SHUT_RD)
call. Here is the test I orchestrated and
the results I observed.
------------------------------------------------------
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
------------------------------------------------------
Here is a brief description of the above table.
shutdown()
completed successfully. See rows for "12 s" and
"20 s".Here is the C program (client program).
#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);
}
The above code was saved in a file named foo.c
.
Here is a tiny shell script that compiles and runs the above program and
invokes netcat (nc
) to listen on port 8888 and respond to the client
with messages aa
, bb
and cc
at specific intervals as per the table
shown above. The following shell script is saved in a file called
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
When the above shell script is run, the following output is observed.
$ 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
The output shows that the C program is able to receive messages with
recv()
even after it has called shutdown()
. The only behaviour that
the shutdown()
call seems to have affected is whether the recv()
call returns immediately or gets blocked waiting for the next message.
Normally, before shutdown()
, the recv()
call would wait for a
message to arrive. But after the shutdown()
call, recv()
returns 0
immediately when there is no new message.
I was expecting all recv()
calls after shutdown()
to fail in some way (say,
return -1
) due to the documentation I have quoted above.
Two questions:
recv()
call being
able to receive new messages sent after shutdown()
call correct as per
the POSIX standard and the manual page for shutdown(2)
that I have
quoted above?shutdown()
is called, recv()
returns 0
immediately instead of waiting for a new message to arrive?If data is not available for the socket socket, and socket is in blocking mode, the recv() call blocks the caller until data arrives. If data is not available and socket is in nonblocking mode, recv() returns a -1 and sets the error code to EWOULDBLOCK.
If successful, recv() returns the length of the message or datagram in bytes. The value 0 indicates the connection is closed. If unsuccessful, recv() returns -1 and sets errno to one of the following values: Error Code.
RETURN VALUE Upon successful completion, recv() shall return the length of the message in bytes. If no messages are available to be received and the peer has performed an orderly shutdown, recv() shall return 0. Otherwise, -1 shall be returned and errno set to indicate the error.
After your connection is set up, the OS manages the packets entering and leaving your system, the recv() call just reads the packet buffer, and the send() call just queues the packets.
You asked two questions: Is it compliant with the posix standard, and why does recv
return 0 instead of blocking.
Standard for shutdown
The documentation for shutdown
says:
The shutdown() function disables subsequent send and/or receive operations on a socket, depending on the value of the how argument.
This appears to imply that no further read
calls will return any data.
However the documention for recv
states:
If no messages are available to be received and the peer has performed an orderly shutdown, recv() shall return 0.
Reading these together this could mean that after the remote peer calls shutdown
recv
should return an error if data is available, or recv
can continue to return data after shutdown
if "messages are available to be received".While this is somewhat ambiguous, the first interpretation does not make sense, as it's not clear what purpose an error would serve. Therefore the correct interpretation is the second.
(Note that any protocol which buffers at any point in the stack might have data in transit which cannot yet be read. The semantics of shutdown
enable you to still receive this data after calling shutdown
.)
However this refers to the peer calling shutdown
, rather than the calling process. Should this also apply if the calling process called shutdown
?
So is it compliant or what
The standard is ambiguous.
If a process calling shutdown(fd, SHUT_RD)
is to be considered equivalent to the peer calling shutdown(fd, SHUT_WR)
then it is compliant.
On the other hand, reading the text strictly, it seems not to be compliant. But then there is no error code for the case where a process calls recv
after shutdown(SHUT_RD)
. The error codes are exhaustive, which implies that this scenario is not an error, so should return 0
as in the corresponding situation where the peer calls shutdown(SHUT_WR)
.
Nevertheless, this is the behaviour you want - message in transit can be received if you want them. If you don't want to them don't call recv
.
To the extent that this is ambiguous, it should be considered a bug in the standard.
Why isn't post-shutdown
recv
data limited to data which was in transit
In the general case it is not possible to know what data is in transit.
Background
posix provides an api for uniformly interacting with different types of streams, including anonymous pipes, named pipes, and IPv4 and IPv6 TCP and UDP sockets... and raw Ethernet, and Token Ring and IPX/SPX, and X.25 and ATM...
Therefore posix provides a set of functionality which broadly covers the main capabilities of most streaming and packet-based protocols.
However not every capability is supported by ever protocol
From a design point of view, if a caller requests an operation which is not supported by the underlying protocol, there are a number of options:
Enter an error state, and forbid any further operations on the file descriptor.
Return an error from the call, but otherwise disregard it.
Return success, and do the nearest thing that makes sense.
Implement some sort of wrapper or filler to provide the missing functionality.
The first two options are precluded by the posix standard. Clearly the third option has been chosen by Linux developers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With