Search code examples
linuxunixtcpcsockets

Why listen() (before accept() is called) is enough for an application to complete a 3 way handshake?


I am debugging a very basic tcp server in C at Linux. I stopped the execution right before the line where accept() is called. To my suprise, when the client sends a SYN, tcpdump shows that the server responds with a SYN-ACK (which is readily replied with a final ACK from the client).

The ss command indeed shows that the application is already listening on the bound port.

I understand that I already called listen(), so the app will, well, listen to the bound port. But then, by the same semantics, accept() should be called before the server could, well, accept connection.

In the listen() man pages it reads (italics are mine):

listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).

While the accept() man pages says:

It extracts the first connection request on the queue of pending connections for the listening socket

From that, one could understand that accept() should be called before a connection should be established.

What am I missing here? If this is the standard behaviour, could I be pointed out to a primary source? Or is it just implementation specific?

Below is the code I'm using. If I stop its execution right before calling listen(), using netcat shows a SYN sent being replied with a RST. But if I do the same after listen() is executed, tcpdump will show the server replying with a SYN-ACK.

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

void error(const char* message) {
        printf("%s %s\n", message, strerror(errno));
}

int main(int argc, char** argv) {
        const int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if ( sockfd == -1 ) {
                error("Socket error:");
                return 1;
        }

        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(12345);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

        if ( bind(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) == - 1 ) {
                error("Bind error:");
                return 1;
        }
        if ( listen(sockfd, 5) == -1 ) {
                error("Listen error: ");
                return 1;
        }

        printf("Ready.\n");

        struct sockaddr_in cliaddr;
        socklen_t cliaddrlen = sizeof(cliaddr);
        char response[512];
        while (1) {
                const int connfd = accept(sockfd, (struct sockaddr*) &cliaddr, &cliaddrlen);
                if ( connfd == -1 ) {
                        printf("Accept error: %s\n", strerror(errno));
                        return 1;
                }
                const pid_t pid = fork();
                if ( pid == -1 ) {
                        printf("Fork error: %s\n", strerror(errno));
                        continue;
                }
                if ( pid == 0 ) {
                        close(sockfd);
                        char buffer[16];
                        inet_ntop(AF_INET, &cliaddr.sin_addr, buffer, 16);
                        printf("Connection from %s accepted.\n", buffer);
                        while ( 1 ) {
                                int nread = read(connfd, response, 512);
                                if ( nread == -1 ) {
                                        printf("%s\n", strerror(errno));
                                }
                                if (nread == 1 && response[0] == '\n') {
                                        break;
                                }
                                write(connfd, response, nread);
                                //write(STDIN_FILENO, response, nread);
                        }
                        printf("Good bye!\n");
                        close(connfd);
                        return 0;
                }
                close(connfd);
                wait(NULL);
        }
        return 0;
}

Solution

  • The 'backlog' integer parameter to listen() – the one where you're specifying 5 – determines how many connections can be accepted like this; after reaching that limit, new SYNs will be ignored or rejected.

    It allows those connections to be queued up, e.g. in a single-threaded server that would've been likely to find when the BSD sockets API was initially designed (which, if I have my history right, predates threading). The server might have to fork() before it can accept() the next client, or it might even decide to service the request immediately in the same thread if it's a simple protocol (e.g. a basic Whois or QotD server) before getting to the next client.

    If the server didn't respond at all, the client would quickly reach a connection timeout; but once the connection has been established, the timeout can be much longer (e.g. SMTP defines a timeout of five minutes until the greeting). That being said, I don't know what kind of delays would have been "normal" at the time this API was created, so there's a bit of speculation in this.