Capstone: a threaded HTTP server
The last capstone joins the sockets of lesson 70 with the threads of lesson 72: a server accepts a connection, hands it to a thread, and the thread reads a request and writes a response. The test clients are threads in the same program, so everything runs on the loopback interface without another machine.
A listening socket
A server starts with a socket, a bind to an address, and a listen that turns the socket into a queue of pending connections. Port zero asks the kernel for a free one and getsockname reads it back, which is how the tests below know where to connect.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.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;
}
if (listen(server, 4) != 0) {
return 1;
}
socklen_t length = sizeof address;
getsockname(server, (struct sockaddr *) &address, &length);
printf("listening on %d\n", ntohs(address.sin_port));
close(server);
return 0;
}
gcc -Wall -Wextra listen.c -o listen
./listen
listening on 44049
The port is assigned by the kernel and changes on every run, so this number is only the one that run received. The program closes the socket and exits at once; a server keeps the descriptor open and calls accept on it, which is the next section.
One thread per connection
The listening socket stays in the main thread; every accepted connection becomes the argument of a handler thread, which reads, answers and closes. Two client threads connect and send a GET line, and the main thread joins all four threads before it closes the server. The port and the order of the lines vary between runs.
#include <arpa/inet.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 *handler(void *argument)
{
int connection = (int) (long) argument;
char request[256] = {0};
read(connection, request, sizeof request - 1);
printf("handler: %.14s\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);
return NULL;
}
static void *client(void *argument)
{
long id = (long) argument;
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
const char *request = "GET /page HTTP/1.1\r\n\r\n";
connect(socket_fd, (struct sockaddr *) &address, sizeof address);
write(socket_fd, request, strlen(request));
char reply[64] = {0};
read(socket_fd, reply, sizeof reply - 1);
printf("client %ld: %.15s\n", id, reply);
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;
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);
printf("port %d\n", ntohs(address.sin_port));
pthread_t clients[2];
for (long i = 0; i < 2; i++) {
pthread_create(&clients[i], NULL, client, (void *) (i + 1));
}
pthread_t handlers[2];
for (int i = 0; i < 2; i++) {
int connection = accept(server, NULL, NULL);
pthread_create(&handlers[i], NULL, handler, (void *) (long) connection);
}
for (int i = 0; i < 2; i++) {
pthread_join(handlers[i], NULL);
pthread_join(clients[i], NULL);
}
close(server);
printf("server done\n");
return 0;
}
gcc -Wall -Wextra -pthread serve.c -o serve
./serve
port 36313
handler: GET /page HTTP
handler: GET /page HTTP
client 2: HTTP/1.1 200 OK
client 1: HTTP/1.1 200 OK
server done
The handler prints the first fourteen characters of the request line and every client prints the start of the status line. The order of handlers and clients depends on the scheduler; the count of each is fixed. A server that outlives a single run loops over accept and detaches or pools the handlers instead of joining two of them.
Routing the request
A real server looks at the path and chooses the response. The handler scans the request line with sscanf, compares the path, and returns 200 for the root and 404 for everything else. The client prints the status code and the two bytes of the body.
#include <arpa/inet.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 *handler(void *argument)
{
int connection = (int) (long) argument;
char request[256] = {0};
char path[16] = {0};
read(connection, request, sizeof request - 1);
sscanf(request, "GET %15s", path);
if (strcmp(path, "/missing") == 0) {
const char *not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 2\r\n\r\nno";
printf("handler: %s 404\n", path);
write(connection, not_found, strlen(not_found));
} else {
const char *ok = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
printf("handler: %s 200\n", path);
write(connection, ok, strlen(ok));
}
close(connection);
return NULL;
}
static void *client(void *argument)
{
long id = (long) argument;
const char *path = id == 1 ? "/" : "/missing";
char request[64];
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
snprintf(request, sizeof request, "GET %s HTTP/1.1\r\n\r\n", path);
connect(socket_fd, (struct sockaddr *) &address, sizeof address);
write(socket_fd, request, strlen(request));
char reply[64] = {0};
read(socket_fd, reply, sizeof reply - 1);
char status[16] = {0};
sscanf(reply, "HTTP/1.1 %15s", status);
printf("client %ld: %s %s\n", id, status, strstr(reply, "\r\n\r\n") + 4);
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;
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);
pthread_t clients[2];
for (long i = 0; i < 2; i++) {
pthread_create(&clients[i], NULL, client, (void *) (i + 1));
}
pthread_t handlers[2];
for (int i = 0; i < 2; i++) {
int connection = accept(server, NULL, NULL);
pthread_create(&handlers[i], NULL, handler, (void *) (long) connection);
}
for (int i = 0; i < 2; i++) {
pthread_join(handlers[i], NULL);
pthread_join(clients[i], NULL);
}
close(server);
return 0;
}
gcc -Wall -Wextra -pthread routes.c -o routes
./routes
handler: / 200
client 1: 200 ok
handler: /missing 404
client 2: 404 no
The two handlers run independently, so the pairs can interleave in another order. The response always carries a Content-Length, which is what lets the client know where the body ends; a server that omits it leaves the client waiting for the connection to close.
A short read
A TCP read returns what has arrived, not what the sender will eventually send. The client writes "GET " and the rest of the request in two pieces with a pause, and the server reads once. It sees only the first piece, finds no end of the headers, and answers 400.
#include <arpa/inet.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, "GET ", 4);
usleep(50000);
write(socket_fd, "/ HTTP/1.1\r\n\r\n", 15);
char reply[64] = {0};
read(socket_fd, reply, sizeof reply - 1);
printf("client: %.15s\n", reply);
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 request[256] = {0};
read(connection, request, sizeof request - 1);
printf("request: [%s]\n", request);
if (strstr(request, "\r\n\r\n") == NULL) {
const char *bad = "HTTP/1.1 400 Bad Request\r\nContent-Length: 3\r\n\r\nbad";
write(connection, bad, strlen(bad));
}
close(connection);
pthread_join(thread, NULL);
close(server);
return 0;
}
gcc -Wall -Wextra -pthread partial.c -o partial
./partial
request: [GET ]
client: HTTP/1.1 400 Ba
The fix is a read loop: keep reading into the buffer until the end of the headers, the buffer is full or the client closes. The server must also handle a request split across many packets, which is the normal case on a network. The 400 answer is at least honest about the problem.
Exercises
- Loop the server over accept until a flag is set by the signal handler from lesson 68, then close and exit.
- Serve two paths from files, reading the body with fread and sending it with write.
- Add a second read loop so the server accepts a request sent in three pieces and still answers 200.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does binding port zero ask the kernel to do?
Answer
Choose a free local port
The kernel selects an unused port, and
getsocknamereads the number back for the clients. -
What does the server do with each accepted connection?
Answer
It passes the descriptor to a handler thread
The main thread accepts and creates a handler, and each handler reads, answers and closes its own connection.
-
How does the router choose between 200 and 404?
Answer
It compares the parsed path with
/missingThe handler scans the request line with
sscanf, and any path other than/missinggets the 200 response. -
What does a server do when a request arrives in pieces?
Answer
It reads until the end of the headers
TCP has no message boundaries, so the server loops until the blank line, the buffer is full or the client closes.
-
What does binding a TCP socket to port zero let the server do?
Answer
Ask the kernel to choose a free local port
The kernel selects an unused port, and
getsocknamereads the number back for the clients. -
What does the threaded server print for its two clients?
Answer
Two handler lines and two client status lines
Each accepted connection gets a handler, and each client prints the start of its status line; the order varies.
-
What does this program print?
#include <stdio.h> #include <string.h> int main(void){const char*paths[2]={"/","/missing"};for(int i=0;i<2;i++){if(!strcmp(paths[i],"/missing"))printf("handler: %s 404\n",paths[i]);else printf("handler: %s 200\n",paths[i]);}return 0;}Answer
handler: / 200 / handler: /missing 404
The root gets the 200 response and the missing path gets 404.
-
What does this program print for the split request?
#include <stdio.h> #include <string.h> int main(void){char request[256]="GET ";if(strstr(request,"\r\n\r\n")==NULL)printf("request: [%s]\nclient: HTTP/1.1 400 Bad Request\n",request);else printf("client: HTTP/1.1 200 OK\n");return 0;}Answer
request: [GET ] / client: HTTP/1.1 400 Bad Request
The buffer holds only
GET, finds no blank line, and answers 400. -
What is the mistake in a server that reads the request once?
Answer
A TCP read may return only part of the data
The stream delivers whatever has arrived, so the server needs a loop until the request is complete.
-
Why does the response carry
Content-Length?Answer
It tells the client where the body ends
Without the length the client cannot tell the body from the next response on a persistent connection.
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