libwebsockets: Simple HTTP server

I was quiet surprised that there aren't many web socket libraries for C. I made a small research and found this nice list of various implementations on wikipedia. It says that there are only 2 implementations of websockets in pure C (not C++) so I made a quick look at both of them and they look pretty complicated but because I didn't have any other option I stayed with libwebsockets which seems to be up to day (according to the git log) and it can be easily used as a web server as well. The only caveat here is that there is no "Hello world" tutorial for complete dummies (well, that's what I'm trying to put right).

I don't have any particular reason to use C because I could do the same way more easily with node.js I was just curious how complicated it is to make web socket or web server based on some library and compare performance to node.js, nginx or Apache. Later maybe add support for threads, scaling and whatever I want to play with.

Writing a web server

This tutorial is going to be only about writing a simple single-threaded web server. Maybe like a little extended "Hello world" demo from node.js website.
In order to compile this tutorial you have to download libwebsockets and compile them. You don't have to use any IDE since this is going to be just a single C file.

When I saw this only example using libwebsocket I felt like this.
Well, it turned out that it's really not that complicated and if you spend a while playing with it, it all makes sense.

Ok, first include some header files.

 1 
 2 
 3 
 4 
 5 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libwebsockets.h>

The most important function is our callback that is called by the libwebsocket service every time a request occurs (well, in this case a better word could be probably "event").

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
static int callback_http(struct libwebsocket_context *context,
                         struct libwebsocket *wsi,
                         enum libwebsocket_callback_reasons reason, void *user,
                         void *in, size_t len)
{
   
    switch (reason) {
        // http://git.warmcat.com/cgi-bin/cgit/libwebsockets/tree/lib/libwebsockets.h#n260
        case LWS_CALLBACK_CLIENT_WRITEABLE:
            printf("connection established\n");
           
        // http://git.warmcat.com/cgi-bin/cgit/libwebsockets/tree/lib/libwebsockets.h#n281
        case LWS_CALLBACK_HTTP: {
            char *requested_uri = (char *) in;
            printf("requested URI: %s\n", requested_uri);
           
            if (strcmp(requested_uri, "/") == 0) {
                void *universal_response = "Hello, World!";
                // http://git.warmcat.com/cgi-bin/cgit/libwebsockets/tree/lib/libwebsockets.h#n597
                libwebsocket_write(wsi, universal_response,
                                   strlen(universal_response), LWS_WRITE_HTTP);
                break;

            } else {
                // try to get current working directory
                char cwd[1024];
                char *resource_path;
               
                if (getcwd(cwd, sizeof(cwd)) != NULL) {
                    // allocate enough memory for the resource path
                    resource_path = malloc(strlen(cwd) + strlen(requested_uri));
                   
                    // join current working direcotry to the resource path
                    sprintf(resource_path, "%s%s", cwd, requested_uri);
                    printf("resource path: %s\n", resource_path);
                   
                    char *extension = strrchr(resource_path, '.');
                    char *mime;
                   
                    // choose mime type based on the file extension
                    if (extension == NULL) {
                        mime = "text/plain";
                    } else if (strcmp(extension, ".png") == 0) {
                        mime = "image/png";
                    } else if (strcmp(extension, ".jpg") == 0) {
                        mime = "image/jpg";
                    } else if (strcmp(extension, ".gif") == 0) {
                        mime = "image/gif";
                    } else if (strcmp(extension, ".html") == 0) {
                        mime = "text/html";
                    } else if (strcmp(extension, ".css") == 0) {
                        mime = "text/css";
                    } else {
                        mime = "text/plain";
                    }
                   
                    // by default non existing resources return code 400
                    // for more information how this function handles headers
                    // see it's source code
                    // http://git.warmcat.com/cgi-bin/cgit/libwebsockets/tree/lib/parsers.c#n1896
                    libwebsockets_serve_http_file(wsi, resource_path, mime);
                   
                }
            }
           
            // close connection
            libwebsocket_close_and_free_session(context, wsi,
                                                LWS_CLOSE_STATUS_NORMAL);
            break;
        }
        default:
            printf("unhandled callback\n");
            break;
    }
   
    return 0;
}

LWS_CALLBACK_HTTP is the only callback we'll care about. Definition in libwebsockets.h is pretty clear about it. Default response for / will be Hello, World! and for any other request it tries to find corresponding file on disk relatively from the current working directory.
libwebsockets_serve_http_file() writes automatically response headers like Content-length and HTTP version and then takes resource's absolute path and dumps it's content as response. It can't detect resource's MIME type so you have to specify it manually. For more information on how this function works and how you can send your own headers see its source code.

If I wanted to make this tutorial very simple I could leave the switch statement just like:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
switch (reason) {
    case LWS_CALLBACK_HTTP: {
        void *universal_response = "Hello, World!";
        libwebsocket_write(wsi, universal_response, strlen(universal_response), LWS_WRITE_HTTP);
        break;
    }
}

This callback would reply to every request with Hello, World! but this would be too easy :).

The second thing we have to do is to define a structure that tells libwebsocket what protocols are we going to implement.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
// list of supported protocols and callbacks
static struct libwebsocket_protocols protocols[] = {
    // first protocol must always be HTTP handler
    {
        "http-only",        // name
        callback_http,      // callback
        0                   // per_session_data_size
    },
    {
        NULL, NULL, 0       // end of list
    }
};

Just note that the { NULL, NULL, 0} is mandatory because deep inside libwebsockets, it needs to know where the protocols array ends.

The last thing is creating the libwebsockets context and main loop:

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
int main(void) {
    // server url will be http://localhost:9000
    int port = 9000;
    const char *interface = NULL;
    struct libwebsocket_context *context;
    // we're not using ssl
    const char *cert_path = NULL;
    const char *key_path = NULL;
    // no special options
    int opts = 0;
    
    // create libwebsocket context representing this server
    context = libwebsocket_create_context(port, interface, protocols,
                                          libwebsocket_internal_extensions,
                                          cert_path, key_path, -1, -1, opts);
    
    if (context == NULL) {
        fprintf(stderr, "libwebsocket init failed\n");
        return -1;
    }
    
    printf("starting server...\n");
    
    // infinite loop, to end this server send SIGTERM. (CTRL+C)
    while (1) {
        libwebsocket_service(context, 50);
        // libwebsocket_service will process all waiting events with their
        // callback functions and then wait 50 ms.
        // (this is a single threaded webserver and this will keep our server
        // from generating load while there are not requests to process)
    }
    
    libwebsocket_context_destroy(context);
    
    return 0;
}

I think this is self-explanatory. You can try to set different dalays than 50 ms and see what happens when you run some stress benchmark like ab.

In order to compile this example you can read this short article.
If you were successful, you can run it and see what it prints into your console. In my case it was this:

Compiled without SSL support, serving unencrypted
Listening on port 8080
unhandled callback
unhandled callback
starting server…

Then open http://localhost:8080 in your browser and you should see just Hello, World! like on this screenshot.

As you can see in your console, libwebsockets service calls more callbacks than we handle. But this is right, we are interested in just one event and don't need to care about the rest.

At the end I tried all combinations that can occur. That's an image, a HTML page and a text file. Here you can see that it was processed by the browser as expected.

There's maybe one caveat. This web-server tries to find all resources relatively from the current working directory which is not very safe (you can use /../../.. and get into any directory you want) but also it's not very practical when debugging. For instance Xcode generates very long paths like this /Users/martin/Library/Developer/Xcode/DerivedData/libwebsockets-webserver-catfyrvtlzsybdakzdgpqzeiroyc/Build/Products/Debug/hello.txt and it's a bit annoying to copy my test resources into that directory by hand (previous article about compiling libwebsockets project also shows you how you can let Xcode do this for you automatically).

By the way, full source code in one file is on gist.github.com

Conclusion

I think libwebsockets is a very nice library that helps you a lot even when you could do the same functionality like this web-server by your-self. I doesn't store any per-user data (that's what the void *user pointer can be user for) and doesn't support multithreading.
As I think about multithreading I'm a bit afraid that this library is tailor-made for forking processes (which probably doesn't work as this library requires on OS X) but maybe I'm wrong and there's something that I don't see right now.

blog comments powered by Disqus