Susam Pal Susam Pal - 1 month ago 11
C Question

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

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


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.


  • Time: Time elapsed (in seconds) since the beginning of the test.

  • netcat (nc): Steps performed via netcat (nc). Netcat was made to listen
    on port 8888 and accept TCP connections from my C program compiled to
    ./a.out. Netcat plays the role of the server here. It sends three
    messages "aa", "bb" and "cc" to the C program after 4s, 10s and 18s,
    respectively, have elapsed.

  • C (a.out): Steps performed by my C program compiled to ./a.out. It
    performs 6 recv() calls after 6s, 12s, 14s, 16s, 20s and 22s have
    elapsed.

  • Result observed: The result observed in the output of the C program.
    It shows that it is able to recv() the message "bb" that was sent
    after
    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:


  1. Is the behaviour observed in my experiment, i.e.
    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?

  2. Why is it that after
    shutdown()
    is called,
    recv()
    returns
    0
    immediately instead of waiting for a new message to arrive?


Ben Ben
Answer

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

  1. calls to recv should return an error if data is available, or
  2. calls to 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.

  • In the case of unix sockets, data may be buffered on the receiving side, by the operating system, or on the sending side.
  • In the case of TCP, data may be buffered by the receiving process, by the operating system, by the network card hardware buffer, packets may be in transit at intermediate routers, buffered by the sending network card hardware, by sending operating system or sending process.

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.