The Birth of an Apilator is part of a case study in modern client-server programming for the web. Part I focuses on the overall design and discusses the main decisions for the back-end construction. Get and try Apilator yourself here!
Introduction
Work for a recent project lead me to the conclusion that the optimal design for a network-based environment which requires frequent access (ranging anywhere from web based management systems to social networks) should be devised the following way:
- Front-end, which is a JavaScript application running in the browser (effectively turning the browser from an HTML renderer into a application virtual machine). The application is loaded into the browser at start-up (including all required HTML templates to be used for data presentation), and
- Back-end, which main purpose is to exchange pure data with the Front-end (i.e. in JSON format as it is faster to create and process than, say, XML).
Back-end Requirements and Their Solutions
At this point the main requirements to a fast and reliable back-end began to shape out. The back-end should consist of multiple nodes working in parallel with the ability to add/remove nodes as necessary (probably using some front-end-facing load-balancer mechanism, be it anything from HAProxy to a DNS round-robin). While in theory the back-end can be a pure TCP server (maybe with some custom protocol on top of it), it will be more convenient to have the backend node speak HTTP, because thus we’ll be able to use it for additional purposes like feeding the application itself, any static content it may need or handle uploaded data. A list of the requirements will be:
- HTTP up to 1.1 support.
- Minimum GET, POST, PUT, DELETE methods support.
- For POST requests, support x-www-urlencoded and multipart/form-data uploads.
- Support large number of active clients but with relatively small amount of data exchange per client.
- Minimise the time and resources needed to serve a client request.
- Ability to interact as seamlessly as possible with external systems (like databases etc.)
A quick look at the available solutions lead to the following conclusions:
- A typical web server was not a good solution because of the high expenses to serve each request (e.g., fire up a framework from ground zero, establish required DB connections etc.). We needed something that will be up & running all the time, with database connections set up and waiting to execute queries.
- So, we needed a stand-alone engine much like an application server. Removing scripting languages as being too slow, we’re left with 3 obvious choices: C, C++, Java. C is the best computer language ever, but writing business logic in C will be too slow, hence we’re left with C++ and Java. C++ has the advantage of providing access to C-level tools when needed, but C++ is not often used for BL programming, hence tools may be scarce, skilled people too. Java has become much more popular and thus becomes our primary choice.
- Java seems to have the advantage of being able to run inside an application server like Tomcat, JBoss etc. However, we’re not going to use this: first, they are designed to be very versatile and we prefer something simpler, but faster; second, they impose limitations ( e.g., Tomcat can only run JSP and servlets) and we want all the power we can get our hands on.
- For the same reason of simplicity and power we’ll not employ an excellent, but way too heavy platform like Apache Commons; we’ll rather build our own stack from ground up.
Having satisfied the requirement for minimal footprint and stateful operation, we can now move to another one. The front-end/back-end split and the application field both call for a scenario where there will be multiple active clients (potentially many thousands), but each will call the back-end relatively rarely; because we’ll only send data to be rendered (and neither HTML nor styles), the amount data that will be exchanged per client will be relatively low. The classic networking approach says the server should spawn a worker for each separate client; in our case this will mean many, many workers staying idle most of the time. To utilize resources better, we’ll resort to entirely non-blocking I/O with only a handful of threads, serving all requests in a round-robin fashion.