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
- Add a third client to the poll program and raise the backlog and the poll count.
- Make the HTTP server answer 404 for a request line that does not start with GET /.
- 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.
-
What does
pollreport to the server?Answer
Which descriptors are ready for an operation
The call fills the
reventsfield of each entry, and the server accepts or reads only the descriptors that are ready. -
What does
listendo for a TCP socket?Answer
It turns the socket into a queue of pending connections
After
bindandlisten, the socket accepts connections, and the backlog bounds the pending queue. -
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.
-
What error does a second
bindto a port already in use report?Answer
EADDRINUSE, printed as Address in useOne listening socket owns the address, and the kernel refuses the duplicate with the address-in-use error.
-
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.
-
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. -
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. -
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. -
What is the usual fix for
EADDRINUSEafter a quick restart?Answer
Set
SO_REUSEADDRbefore the bindThe old socket may linger in the kernel, and the reuse option lets the new server bind the same address.
-
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