Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?
like image 637
Susam Pal Avatar asked Sep 26 '16 08:09

Susam Pal


People also ask

Why is recv blocking?

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.

What is returned by recv () from the server after it is done sending the HTTP request?

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.

What does recv () return?

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.

How does send and recv work?

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.


1 Answers

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.

like image 77
Ben Avatar answered Sep 29 '22 14:09

Ben