Introduction
A while ago I started work on a web server written in C. The project is available here. Before I had written a web server in rust (found here) so I was a bit familiar with HTTP and since this wasn’t my first ever C project I of course knew a bit about basic file handling and error handling. The main new things to learn where the following:
- Working with sockets
- Using a thread pool with:
- Worker threads
- An event queue for incoming requests
- Parsing a configuration file
Technical details
Sockets
While I was somewhat familiar with sockets I hadn’t used them in C (I used them
in a small python web server). This turned out not to be a big problem. The
harder part was the protocol parsing but I restricted myself to HTTP/1.1
connections that close immediately after being processed. And I didn’t look
into non-blocking sockets. This is one of the things I might explore later in a
smaller form factor and then either try to work into this project or write a
new one from scratch.
Because the server also has SSL functionality I wanted to combine SSL and normal connections into one API and came up with the connector design:
typedef enum {
HTTPSERV_NET_CONNECTOR_SSL,
HTTPSERV_NET_CONNECTOR_SCK,
} httpserv_net_connector_kind_t;
typedef union {
httpserv_ssl_t *ssl_conn;
int sck_conn;
} httpserv_net_connector_t;
The connector is a struct holding the file descriptor of the underlying socket and a pointer to additional OpenSSL information:
typedef struct {
SSL_CTX *ssl_ctx;
SSL_METHOD *method;
SSL *ssl;
char *certchains;
char *privkey;
} httpserv_ssl_t;
A Thread pool
A thread pool is a great way to do multi-threaded applications like web servers. The main benefit of course being that threads can be reused and they will have minimal overhead. From a technical standpoint there are a few things to take into consideration:
- How do threads become a part of the thread pool?
- How does the thread pool receive tasks?
- How does the thread pool distribute tasks among it’s threads?
This is the definition of a thread pool:
typedef struct {
threadpool_worker_t **workers;
size_t numthreads;
threadpool_job_queue_t *queue;
pthread_mutex_t *mutex;
pthread_cond_t *cond;
} threadpool_t;
The main part is based on an array of workers which are wrappers around the
individual threads. Additionally the pool has a job queue where tasks are
added. A job is simply a wrapper around a function that returns a pointer and
has a single pointer as argument (these are optional of course). The job queue
is implemented in a thread-safe manner to allow for multiple threads to add and
remove jobs. Every worker thread will look into the job queue for a new job.
And if one is found it executes it. If not it will listen on a
pthread_cond_t for when a new job is added.
Parsing the configuration file
This part is a little harder to explain without drowning in technical details so here is the gist of it:
A configuration file is read in and represented by a tree data structure where every node can have associated data. This data is then evaluated by the implementation for things like IP address and port as well as IPv6 support and SSL. The syntax of the configuration file itself is just key value pairs with hierarchies being represented by dots:
# this is a comment
http.some.value = hello
http.ip = 127.0.0.1
http.port = 8080
Binary and library implementation
The project is divided into two parts:
- A library for most of the technical details like networking, parsing the HTTP protocol and parsing the configuration file
- A binary using the library to provide a basic web server
This separation of concerns allows the library to be used independently of the web server binary (although it’s not going to be of much use as something other than a web server).