While many developers write web applications on a daily basis, the actual inner workings of HTTP, TCP and web servers is for many magic glue that connects fetch and your endpoints together. In this article I will open the black box and show you how to build a real web server on top of the sockets in your OS*.

*Reasoning from an UNIX point of view.

Sockets and TCP

Sockets allow processes on your computer to communicate with each other via the file system. They are a special kind of file where the processes read from and write to by using the normal file system api. TCP is an additional standard for using sockets over a network to enable communication between multiple machines. This standard is what powers HTTP and with it the internet.

Sockets work with a client-server system. The first thing that happens when running a server is the creation of a listening socket. This socket is configured with an IP and a port. The listening socket is a special socket in the sense that it is used to communicate between the OS and the server and not between the server and the clients. After the creation of the listening socket the server sends an accept command to this socket. Now the OS will reply with the next attempt to connect to our IP and port back to the server.

When this happens a new socket will be created for the incoming connection. This socket is a client socket and will be used to transfer the actual data between the server and the client. After the server is done with the client connection it will again send the accept command and the next connection will be returned to the server*.

*Later on we will look to multi threaded servers that deal with multiple connections at the same time.

This is where the concept of the backlog comes in. The backlog is a queue that the operating system manages for us with all the clients we still have to process. When creating a listing socket we define how many clients there can be on this queue before the system refused to connect with more clients. This number is a compromise between not spending to many resources on queuing the clients and not refusing clients when there is a peek load.

Sockets, Node and net

In Node we can use sockets by using the net package in the standard library of Node. This package allows us to connect with sockets, write to sockets, read from sockets and to launch a server. We will start of with creating the server socket and listening on an IP and port. listen will keep sending the socket the accept command and read everything it gets back.

The next step is to do something when a client connects to our socket. The net library relies strongly on the event-listener pattern that all JavaScript developers known and love. So listening to incoming connections will be done by listening to the connection event:

Every time a client connects the callback will be called with the client socket of that connection as a parameter. With this socket we can read the data the client has send us and send a response back. For this we use the data event on the connection socket and give it a callback that accepts a Buffer with the data the client send. This buffer can be read as a string and we can call write on the socket to send data back.

A last very important task is to close ( end ) the connection when we are done. Otherwise we could end up with an overflow of open connections if the clients aren’t closing them. This logic isn’t dealing yet with multi-part bodies and the Keep-Alive header. Those aren’t really necessary for our aim to write a simple and understandable server and they could also be added in later.

This is all we need to work with TCP connections in Node. If you call the address with curl you will see the response showing up. Opening localhost:3000 in your browser isn’t going to work yet, for that we first need to implement the HTTP standard.

HTTP

HTTP is a standard for communicating via TCP sockets. It describes how the messages are formatted and how the server should manage connections. A HTTP message from the client to the server (a request) looks as follows:

Here we see more of the familiar concepts you use when making API calls with for example fetch . The message starts of with the verb (method): GET . After this we find the URI, in this case the homepage / . If we would write a framework behind our server we will use this for the routing and find the correct controller and action with it. The last thing on the first line is the HTTP version. This is used for compatibility with older clients. We will be ignoring it in this article to keep things simple.

Starting from the second line we find the headers. The headers are key-value pairs that go with the request. They are all written on their own line and separate the key from the value with : . After the last header there is a blank line. Below this the request body starts. Here it is empty because we are looking to a get request, but here could be a form or JSON over there. All the information I just listed can be described by the following interface:

And parsing the request string to an object of the interface above can be done with some string-magic that is shown bellow (I will not dive into effective ways of parsing strings as it is not the topic of this article). With this function we could start calling controllers with a system that looks a lot like writing endpoints in your favorite back-end framework.

A HTTP response follows the same principles as the HTTP request. The main difference is in the first line where we see what the result of the request was instead of which resource we are requesting. After this we find the familiar headers and body.

From this information we can create the Response interface and write a function to turn it into a string before sending it back over the socket:

If we corporate this back into the server code we get the following code that allows us to view a website in our browser, served by our very own server! There are some more things to HTTP, for example cookies. They are retrieved from the Cookie header, which is possible to do with our implementation. Setting cookies is done via the Set-Cookie header, which can occur multiple times in a response. This is something you will have to add to the Response interface because ATM we don’t allow multiple headers with the same name.

It is a real server now!

Multi Threaded Servers

At the beginning of the article I mentioned the backlog that is connected with our listing socket. At this moment our server runs on only one process which means that when it is processing one request all the incoming connections are piling up on the backlog. We can see this in action if we update our code to first calculate the Fibonacci sequence of 100 before sending the response back. If you open multiple tabs in your browser and view the console output you will see that only the first tab actually connects and the others will stay on the backlog until the server is done calculating the result for the first tab.

This can become a serious issue if we would have an application of which some endpoints are extremely slow (e.g. an export function). They will block the entire server and effectively you will have downtime until your export is complete. There are many ways to work around this, but they all resolve around some concurrency model that allows the server to still accept incoming connections while your heavy export endpoint is still running. Many ‘production-grade’ servers will put the client sockets on a queue and let other threads processes them.

For sake of keeping this article somewhat short and simple I will settle for using a workerpool for doing the heavy calculations and keep all the connection-stuff like it is in the main process. The workerpool enables us to keep a pool of background workers in Node that we can give tasks to do on a background process. The pool will find and assign a worker to do the work. The exec function of the pool returns a promise that will resolve with the result when the task is completed. In this way we don’t block the new incoming connections from being processed while we calculate the Fibonacci sequence of 100:

If you now open the site in multiple tabs you will see all the connections getting accepted in the terminal. They now all have their calculations running on their own worker which means no more blocking of requests by heavy tasks. Here we did send the result of fibonacci directly, normally you would but your entire back-end framework on a worker and let it do all its querying and processing without blocking your server from doing its job.

Conclusion

In this article we have gone from looking at some low level operating system concepts to building a multi threaded server that can serve complete websites to your browser. Of course there are many more things left to explore. The concurrency model for our server could easily fill its own article. The same can be said about implementing a framework behind the server to deal with routing, redirects, files, etc. The goal of this article was to show you how your browser is connected to your endpoints and I hope your learned something from it.