Simplicity has its place in software design. Yet we seem to be in the age of framework dependent design. For obvious reasons engineers are encouraged to reuse existing code and not reimpliment the wheel. We have all heard this hackneyed expression, but should we take it to heart? Short of being completely cynical there is a trade off where writing your own solution may be simpler, faster and easier to maintain.
I recently read an article called “A Web Server in 30 Lines of C”. The 30 line solution uses 0MQ to implement a “raw” socket that can support a streaming protocol such as HTTP. This article describes three camps, “the good”, those who have never built a messaging system, “the bad,” those who’ve built a messaging system and relied on a framework, and “the ugly,” those who’ve built a messaging system but did it the ugly way, using sockets. In the context of that article I am squarely in the “ugly” camp. I don’t simply trust packages and tools I download from the Internet to make my code perform better or improve stability. In fact, I can cite numerous counter examples.
There are certain uglies among us who prefer a straight
C solution rather than implementing whatever the framework du jour happens to be. Yet, these cursed souls are frequently vilified for wastefulness and being behind the times. They refuse to yield to the march of progress. For shame, for shame!
The first question that came to mind in reading this “30 line” blog post is how many lines of
C does it take to create such a trivial “web server?” It is not hard to create an empty shell that does nothing outside of reading some bytes off of a socket and serving something rudimentary in response. I know a lot of developers that would assume implementing any TCP server is non-trivial but they do so more often out of laziness than personal experience.
My approach to this problem is the same that I would employ in building a much more sophisticated web server. I’m going to write a single threaded event reactor using
epoll. This is similar to the model used in nginx and other very high performance network applications.
Worlds Smallest Web Server
First we must bind the server socket, we are going to be using asynchronous I/O because otherwise I/O operations would block our single thread and cause starvation. Here is the function that does this. It sets the O_NONBLOCK flag on the file descriptor and returns. – 10 lines
Next I bind the server socket for the local interface. – 26 lines
Then I set the server socket to non blocking mode and tell the operating system that I would like to create a new instance of
epoll_wait system call will notify us when there is a socket state change event. – 5 lines
Finally, I configure
epoll to be edge triggered and tell it that I am interested in read events. In
epoll, edge triggered means that you will only be notified once when there is a change in state on the socket. If you didn’t read all the data out of the buffer you won’t get a callback until the next event. After registering
epoll, I am free to call next_event, the reactor event handler, forever. – 20 lines
The next_event function queries
epoll and handles the subsequent accept and read events on the socket. – 57 lines
This code calls the parse_http function with the socket buffer and file descriptor. In the case of this simple example parse_http is trivial, however in a real web server the details could readily be filled in. – 30 lines
That is my solution, a web server in 124, rather than 30, lines of C. No framework, no catch. This solution has the advantage of being a very high performance single threaded reactor and the compiled binary is only 10k on a Linux box.
It is very hard to accomplish anything useful in 30 or even 100 lines of code. This tells me that simply getting something to work, such as decoding and dispatching an HTTP request does not satisfy any sort of reasonable definition of accomplishment.
Essentially the whole reinvent the wheel argument is a straw man. Once you have avoided reinventing the wheel, you still haven’t done any real work. For any real software problem, regardless of your foundation you will still have some very real work in implementing your custom solution.
I’m not going to take time and effort to introduce a dependency on some 3rd party without having it eliminate a substantial amount of code, perhaps 1000 lines or more. Many developers set this threshold at 0 lines: they always go for the framework approach. In many cases this leads to difficult projects with dependencies and interdependencies that simply confound progress.
I frequently think of the implementer of framework solutions and how many of those are implementing their framework because they rejected the previous generation’s framework. Apparently at some point they decided it was a good idea to reinvent the wheel. I’m going to go out on a limb and say that often it is a great idea to reinvent the wheel. In fact there is a word for it: innovation.
I don’t view the
C api for
epoll as somehow more opaque than the raw socket extension to 0MQ. But I do view it as straightforward, and already installed in my entire environment. It’s also battle tested and hardened by thousands of implementations, including all the network frameworks that implement it.
Perhaps some of you would argue that framework solutions allow people to solve problems they wouldn’t ordinarily know how to solve on their own. I regard this argument as tantamount to loading a gun and are pointing it at your foot. Don’t pull that trigger.