How to write a memcached server in less than 50 lines of code
Writing your own version of a server is always a good exercise. The interface is very well defined, clients exist everyone, and it proves you can master a programming language to write practical applications.
Memcached is a practical application which is extremely easy to implement from the ground up, and it’s what we’ll use for our example.
Step 1: Figuring out how to build a web server
In every programming language, this usually ends up with a copy-paste of something. Let’s do the same with Python! Apparently, Python 3 now has a cool new feature called
socketserver which we will use. After a quick Google search we find the following code:
The request handler class for our server.
It is instantiated once per connection to the server, and must
override the handle() method to implement communication to the
# self.request is the TCP socket connected to the client
self.data = self.request.recv(1024).strip()
# just send back the same data, but upper-cased
if __name__ == "__main__":
HOST, PORT = "localhost", 9999
# Create the server, binding to localhost on port 9999
with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
If we look at what’s going on in the example, we can see that in the
handle function, the main logic of this web server, which is running on port 9999, is to uppercase the text received:
Step 2: Reading about the protocol
The memcached is an extremely simple one. It is well documented in its wiki (https://github.com/memcached/memcached/wiki) but looking over the internet we can find a straightforward example, right here: https://www.tutorialspoint.com/memcached/memcached_set_data.htm
Reading through the tutorial we can start to understand just how simple the protocol is:
set tutorialspoint 0 900 9
VALUE tutorialspoint 0 9
OK, we can figure out most of it, as we expect memcached to store simple keys and values. First of all — we see some nice things, namely the fact that the protocol is very easy to parse, as the values are sent and retreived in between newlines, so the parsing is extremely easy and can basically be done using a string split.
What’s not obvious and requires some reading is the 3 numbers after the key name (0 900 9 in this example). The values are (copy pasted from the tutorial):
- key − It is the name of the key by which data is stored and retrieved from Memcached.
- flags − It is the 32-bit unsigned integer that the server stores with the data provided by the user, and returns along with the data when the item is retrieved.
- exptime − It is the expiration time in seconds. 0 means no delay. If exptime is more than 30 days, Memcached uses it as a UNIX timestamp for expiration.
- bytes − It is the number of bytes in the data block that needs to be stored. This is the length of the data that needs to be stored in Memcached.
- noreply (optional) − It is a parameter that informs the server not to send any reply.
- value − It is the data that needs to be stored. The data needs to be passed on the new line after executing the command with the above options.
This means that our cache data structure will need to store the flags and exptime variables aside from the value itself. The bytes is necessary for the protocol implementation as we need to know how much to read, as we will need to ignore newlines as terminators (similarly to HTTP).
Step 3: Set up the environment
First of all, before we actually start to code, let’s get a working memcached server running, and a test.
To get memcached simply run:
brew install memcached
Now, let’s write a simple test (don’t forget to
pip install pymemcache):
from pymemcache.client import base
client = base.Client(('localhost', 11211))
client.set('some_key', 'some value')
Note that 11211 is the default port for memcached, so if you want to run your version of memcached then I would suggest you have it running on a different port.
Now, all that’s left is a service to test this code against!
Step 4: Let’s code!
Here it is, the web server.
Now, run the server (note that it runs on port 9999), adjust the Python client to run on that port and then run the client.
First of all, you’ve seen just how easy it is to write a useful web server, in less than 50 lines of code, which has the basic features of memcached: saving, retreiving, setting a TTL and adhering to noreply.
But the most interesting thing about this small snippet is the methodology. We start out using the
MyTCPHandler example above, and then slowly turn it into a functioning web service. There are a few things which are not trivial, and do require some more knowledge in writing network services:
- Using a
StreamRequestHandlerinstead of a standard TCP server — this is to allow us to use the
readlinecommand, which reads a line from a stream until a
\r\nsequence has been read. This is useful for textual protocols such as memcached, where this is something that is expected from us (e.g. read one line exactly, as opposed to read 4 bytes for example). The
socketserverPython docs do contain information about this handler.
- Using a main loop in conjunction with a
peekcommand — texual web servers also expect that you read all lines which are in the buffer for each connection being made, which is why a
while Trueloop is necessary. However, we don’t want to keep the connection hanging if there’s nothing to read as part of a new sequence, so we can use
peekto determine if there’s anything to read right now.
The purpose of this exercise is to show you the basics of web servers, so if you’ll think about writing one, you won’t be afraid to do so. The reality is, however, that in the modern world there is almost no reason to write a new web server, especially using a programming language like Python, mostly because there are great implementations out there for just about anything you need, HTTP is great for REST APIs, gRPC is great for RPC, and there’s many more queue systems, databases, cache servers, and just about anything you can think of.
I hope you enjoyed and that your next move is to go and implement your very own Redis server :)