A TCP server

Lesson 70 accepted one connection and stopped. A server accepts many, and it must decide which socket is ready before it reads. poll waits for a set of descriptors and reports the ready ones, which is the smallest event loop a C server needs.

poll over connections

The server binds port zero, listens, and forks two clients so the test runs without another terminal. The parent polls the listening socket, accepts each connection, reads a word, answers with another and closes. The clients print the answer they received.

#include <arpa/inet.h> #include <netinet/in.h> #include <poll.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void) { int server = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; if (bind(server, (struct sockaddr *) &address, sizeof address) != 0) { return 1; } listen(server, 2); socklen_t length = sizeof address; getsockname(server, (struct sockaddr *) &address, &length); pid_t children[2]; for (int i = 0; i < 2; i++) { children[i] = fork(); if (children[i] == 0) { int client = socket(AF_INET, SOCK_STREAM, 0); if (connect(client, (struct sockaddr *) &address, sizeof address) != 0) { _exit(1); } write(client, "hello", 5); char reply[8] = {0}; read(client, reply, sizeof reply - 1); printf("client %d got %s\n", i + 1, reply); close(client); _exit(0); } } struct pollfd entry = {server, POLLIN, 0}; int handled = 0; while (handled < 2) { if (poll(&entry, 1, 1000) <= 0) { break; } int connection = accept(server, NULL, NULL); char buffer[8] = {0}; read(connection, buffer, sizeof buffer - 1); write(connection, "pong", 4); close(connection); handled++; printf("server handled %s\n", buffer); } printf("connections: %d\n", handled); close(server); for (int i = 0; i < 2; i++) { waitpid(children[i], NULL, 0); } return 0; }

gcc -Wall -Wextra poll.c -o poll
./poll

client 1 got pong
server handled hello
client 2 got pong
server handled hello
connections: 2

The order of the client and server lines depends on the scheduler, so it varies between runs; the counts do not. The listening socket is ready whenever a connection is pending, and the server accepts it immediately. A real loop keeps the connection descriptors in the poll set as well, so a slow client does not block the others.

A request and a response

HTTP is a text protocol on top of TCP: the client sends a request line, headers and a blank line, and the server answers with a status line, headers and a body. The example does the minimum of each and prints the request line it received and the status line the client got back.

#include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void) { int server = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; if (bind(server, (struct sockaddr *) &address, sizeof address) != 0) { return 1; } listen(server, 1); socklen_t length = sizeof address; getsockname(server, (struct sockaddr *) &address, &length); pid_t pid = fork(); if (pid == 0) { int client = socket(AF_INET, SOCK_STREAM, 0); const char *request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"; connect(client, (struct sockaddr *) &address, sizeof address); write(client, request, strlen(request)); char reply[64] = {0}; read(client, reply, sizeof reply - 1); printf("client got: %s", reply); close(client); _exit(0); } int connection = accept(server, NULL, NULL); char request[256] = {0}; read(connection, request, sizeof request - 1); printf("request: %.16s\n", request); const char *response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"; write(connection, response, strlen(response)); close(connection); close(server); waitpid(pid, NULL, 0); return 0; }

gcc -Wall -Wextra http.c -o http
./http

request: GET / HTTP/1.1
client got: HTTP/1.1 200 OK
Content-Length: 2

ok

The server read the whole request in one call because it is small and arrived in one packet; the client printed the headers and then the two bytes of the body. A real server reads until the blank line, parses the request line, and sends a body sized by Content-Length.

Closing the server

A server stops by closing the listening descriptor: the queue disappears, waiting clients are reset, and a further accept on the closed number fails with EBADF. The program accepts one client, closes the listening socket, and calls accept again to show the error.

#include <arpa/inet.h> #include <errno.h> #include <netinet/in.h> #include <pthread.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> static struct sockaddr_in address; static void *client(void *argument) { (void) argument; int socket_fd = socket(AF_INET, SOCK_STREAM, 0); connect(socket_fd, (struct sockaddr *) &address, sizeof address); write(socket_fd, "bye", 3); close(socket_fd); return NULL; } int main(void) { int server = socket(AF_INET, SOCK_STREAM, 0); memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; bind(server, (struct sockaddr *) &address, sizeof address); listen(server, 1); socklen_t length = sizeof address; getsockname(server, (struct sockaddr *) &address, &length); pthread_t thread; pthread_create(&thread, NULL, client, NULL); int connection = accept(server, NULL, NULL); char buffer[4] = {0}; read(connection, buffer, sizeof buffer - 1); printf("accepted %s\n", buffer); close(connection); pthread_join(thread, NULL); close(server); errno = 0; int again = accept(server, NULL, NULL); printf("second accept: %d (%s)\n", again, strerror(errno)); return 0; }

gcc -Wall -Wextra -pthread shutdown.c -o shutdown
./shutdown

accepted bye
second accept: -1 (Bad file descriptor)

The error is the expected result, not a failure of the program: the descriptor is closed, so it cannot be used. A long-running server stops the accept loop with a flag set by a signal handler (68), closes the listening socket, and joins or detaches the handlers before it exits.

Binding the same port twice

One address and port can belong to one listening socket at a time. The program binds the first socket to a port chosen by the kernel, reads the number back, and tries to bind a second socket to the same address. The kernel refuses with EADDRINUSE, and the message comes from strerror.

#include <arpa/inet.h> #include <errno.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void) { int first = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in address; memset(&address, 0, sizeof address); address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; if (bind(first, (struct sockaddr *) &address, sizeof address) != 0) { return 1; } socklen_t length = sizeof address; getsockname(first, (struct sockaddr *) &address, &length); int second = socket(AF_INET, SOCK_STREAM, 0); if (bind(second, (struct sockaddr *) &address, sizeof address) != 0) { printf("second bind: %s\n", strerror(errno)); } close(second); close(first); return 0; }

gcc -Wall -Wextra bindtwice.c -o bindtwice
./bindtwice

second bind: Address in use

The message is the platform text for EADDRINUSE. A server that restarts too quickly can meet the same error while the old socket lingers in the kernel; the usual fix is SO_REUSEADDR before the bind, not a different port. The error is reported before any client is involved, which makes it easy to see in a startup log.

Exercises

  1. Add a third client to the poll program and raise the backlog and the poll count.
  2. Make the HTTP server answer 404 for a request line that does not start with GET /.
  3. Keep both connection descriptors in the poll set and echo each line until the client closes, instead of answering once.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does poll report to the server?

    Answer

    Which descriptors are ready for an operation

    The call fills the revents field of each entry, and the server accepts or reads only the descriptors that are ready.

  2. What does listen do for a TCP socket?

    Answer

    It turns the socket into a queue of pending connections

    After bind and listen, the socket accepts connections, and the backlog bounds the pending queue.

  3. What ends the headers of an HTTP request?

    Answer

    A blank line after the header fields

    The request line and headers are followed by an empty line, and the body, when present, comes after it.

  4. What error does a second bind to a port already in use report?

    Answer

    EADDRINUSE, printed as Address in use

    One listening socket owns the address, and the kernel refuses the duplicate with the address-in-use error.

  5. What number does this program print after handling two connections? #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(s,(struct sockaddr*)&a,sizeof a);listen(s,2);socklen_t l=sizeof a;getsockname(s,(struct sockaddr*)&a,&l);for(int i=0;i<2;i++)if(fork()==0){int c=socket(AF_INET,SOCK_STREAM,0);connect(c,(struct sockaddr*)&a,sizeof a);write(c,"hello",5);close(c);_exit(0);}int handled=0;while(handled<2){int c=accept(s,0,0);char b[8]={0};read(c,b,7);write(c,"pong",4);close(c);handled++;}printf("connections: %d\n",handled);close(s);return 0;}

    Answer

    connections: 2

    The loop accepts and handles two connections, then prints the count.

  6. What request line does this program print? #include <arpa/inet.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/wait.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(s,(struct sockaddr*)&a,sizeof a);listen(s,1);socklen_t l=sizeof a;getsockname(s,(struct sockaddr*)&a,&l);pid_t p=fork();if(p==0){int c=socket(AF_INET,SOCK_STREAM,0);const char*r="GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";connect(c,(struct sockaddr*)&a,sizeof a);write(c,r,strlen(r));close(c);_exit(0);}int c=accept(s,0,0);char r[256]={0};read(c,r,255);printf("%.16s\n",r);write(c,"HTTP/1.1 200 OK\r\n\r\nok",25);close(c);close(s);waitpid(p,0,0);return 0;}

    Answer

    GET / HTTP/1.1

    The client sends GET / HTTP/1.1, and the server prints the first sixteen characters.

  7. What does this program print for the second accept? #include <arpa/inet.h> #include <errno.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void){int s=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(s,(struct sockaddr*)&a,sizeof a);listen(s,1);close(s);errno=0;int c=accept(s,0,0);printf("%d %s\n",c,strerror(errno));return 0;}

    Answer

    -1 Bad file descriptor

    The listening descriptor was closed, so using it again fails with EBADF.

  8. What does this program print? #include <arpa/inet.h> #include <errno.h> #include <netinet/in.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> int main(void){int f=socket(AF_INET,SOCK_STREAM,0);struct sockaddr_in a;memset(&a,0,sizeof a);a.sin_family=AF_INET;a.sin_addr.s_addr=htonl(INADDR_LOOPBACK);a.sin_port=0;bind(f,(struct sockaddr*)&a,sizeof a);socklen_t l=sizeof a;getsockname(f,(struct sockaddr*)&a,&l);int s=socket(AF_INET,SOCK_STREAM,0);if(bind(s,(struct sockaddr*)&a,sizeof a))printf("%s\n",strerror(errno));close(s);close(f);return 0;}

    Answer

    Address in use

    The first socket still owns the port, and the kernel refuses the second bind with EADDRINUSE.

  9. What is the usual fix for EADDRINUSE after a quick restart?

    Answer

    Set SO_REUSEADDR before the bind

    The old socket may linger in the kernel, and the reuse option lets the new server bind the same address.

  10. Why does a server close the listening descriptor on shutdown?

    Answer

    It stops new connections and releases the port

    Closing the listening socket removes the queue, and later calls on the descriptor fail instead of accepting clients.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image