Writing high-performance servers in modern C++.
Firstly, thanks for all the feedback from my first post — Starting a tech startup with C++. Also a big shout-out to the redditors who gave me valuable advice. It has really motivated me to write more posts.
TL;DR I show how to build a modern C++ high-performance, asynchronous echo server that can be written with just 48 lines of code.
I mentioned in my previous post that I was able to build a prototype database engine within one day using Facebook’s Wangle so this post explains how I managed that. By the end of this post, you will be able to write a high-performance C++ server using Wangle. This post also serves as a tutorial which will be merged into Wangle’s README.md.
I will be showing how to write an echo server in modern C++ — which is the distributed systems’ equivalent of “hello world”. The server will respond to each message by sending the same message directly back. We will also write a client to send messages to our echo server. You can find the source code for the example here.
Wangle is a client/server application framework to build asynchronous, event-driven modern C++ services. Its fundamental abstraction of Wangle is the Pipeline. Once you have fully understood this abstraction, you will be able to write all sorts of sophisticated modern C++ services. Another important abstraction is Service, which is an advanced version of a pipeline but it’s out of scope for this post.
Pipeline
The pipeline is the most important and powerful abstraction of Wangle. It offers immense flexibility to customize how requests and responses are handled by your service.
A pipeline is a chain of request/response handlers. I was thinking of a real-world analogy of a pipeline and the only one I could come up with is a production line in a factory. A production line works in sequential order where each worker receives an object, does only one function with it, and passes it upstream to the next worker until the product is fully manufactured. It’s not really a good analogy because it can only go upstream and not downstream as a pipeline can also handle the opposite way — downstream — from manufactured item to raw materials.
A Wangle handler handles both downstream (handling a response) and upstream (handling a request) events. Once you chain handlers together, it provides a flexible way to convert a raw data stream into the desired message type (class) and the inverse — desired message type to raw data stream.
For example, in the pipeline for the echo service, we will create a pipeline that roughly has the following handlers.
- Handler 1
Upstream: Receives a raw binary stream from a socket and reads it into a zero-copy byte buffer and sends it up to handler 2.
Downstream: Receives a zero-copy byte buffer and writes its contents to the the socket.
2. Handler 2
Upstream: Receives a zero-copy byte buffer from handler 1, decodes the byte buffer into a string and sends the string up to handler 3.
Downstream: Receives a string from handler 3, encode the std::string into a zero-copy byte buffer and sends it down to handler 1.
3. Handler 3
Upstream: Receives a string from handler 2, and then send it backs down the pipeline to write back to the client. It sends the string back to handler 2.
Downstream: Receives a std::string from the upstream and passes it down to handler 2.
A key importance is that one handler should only do one thing and only one thing. If you have a handler that is doing more than one function — like decoding a string directly from the raw byte stream — then you need to split it into several handlers. This is really important for maximizing maintainability and flexibility.
Oh yeah, handlers are not thread safe so do not add any shared state that’s not guarded by a mutex, atomic lock, etc. If you want to use a thread-safe container then I really recommend Folly’s lock-free data structures, which you can easily import because they are a dependency of Wangle and are blazing fast.
Don’t worry if this doesn’t make too much sense yet — it should be clearer when you see the actual code.
Echo Server
I will start showing the code to build your first C++ echo server with Wangle. I assume you have already installed Wangle. I must warn that it unfortunately doesn’t build on Max OS X as of yet so I recommend to virtualize Ubuntu 14.04 to install it.
Here’s the echo handler: it receives a string, prints it to stdout, and sends it back downstream in the pipeline. It’s really import to add the line delimiter because our pipeline will use a line decoder — splits a byte buffer into a line-separated byte buffer.
This needs to be the final handler in the pipeline. Now we need to create a PipelineFactory which is where we define our pipeline to handle requests and responses.
It is very important in that you are very strict in the order you insert them because they are ordered by insertion This pipeline has 4 handlers:
- AsyncSocketHandler:
Upstream: Reads a raw data stream from the socket and converts it into a zero-copy byte buffer.
Downstream: Writes the contents of a zero-copy byte buffer to the underling socket.
2. LineBasedFrameDecoder:
Upstream: receives a zero-copy byte buffer and splits on line-endings
Downstream: just passes the byte buffer to AsyncSocketHandler
3. StringCodec:
Upstream: receives a byte buffer and decodes it into a std::string and pass up to the EchoHandler.
Downstream: receives a std::string and encodes it into a byte buffer and pass down to the LineBasedFrameDecoder.
4. EchoHandler:
Upstream: receives a std::string and writes it to the pipeline — which will send the message downstream.
Downstream: receives a std::string and forwards it to StringCodec.
Now that all needs to be done is plug the pipeline factory into a ServerBootstrap and that’s pretty much it. Bind a port and wait for it to stop.
We’ve written a high performance, asynchronous C++ server in under 48 LOC.
Echo Client
The code for the echo client is very similar to the Echo Server.
Notice that we override other methods — readException and readEOF. There are few other methods that are can be overided. If you need to handle a particular event, just override the corresponding virtual method.
Here’s the client’s pipeline factory. It is identical the server’s pipeline factory apart from EventBaseHandler — which handles writing data from an event loop thread.
Here’s what it looks like when it’s put altogether for the client.
It reads input from stdin in a loop and writes it to the pipeline and it blocks until the response is processed. It blocks by calling .get() from the returned future.
Summary
I’ve shown how to quickly write a basic, high-performance server in modern C++ by using Wangle. You should now know the fundamentals of Wangle and it should give you confidence to write your own service in C++. I strongly recommend to understand the Service abstraction as you can build really sophisticated servers with it.