Building ReactPHP Memcached Client: Making Requests And Handling Responses
This is the first article from the series about building from scratch a streaming Memcached PHP client for ReactPHP ecosystem. The library is already released and published, you can find it on GitHub.
Before writing any code we should think about our future client’s API:
- How we are going to use it.
- What methods it is going to have.
The client is going to be used in ReactPHP asynchronous ecosystem, so I’m going to provide a promise-based interface for it (when methods return promises). Also, we are building a streaming client. Under the hood, we will open a socket connection and use it as a stream. The client itself will be a wrapper on this binary stream communication. That means that it is our job to manually parse Memcached protocol to write and read data with sockets. So, having all of this in mind, let’s start.
Client Factory
Our client has two dependencies:
- a stream, which represents a binary socket connection between client and server
- some Memcached protocol parser to create requests and parse responses.
So, we need somehow to build and pass these dependencies to the client. The best option for it will be a factory. The factory creates dependencies and then uses them to create a client. The first dependency is a streaming socket connection. ReactPHP Socket Component has Connector
class which can be used to create streaming connections. The connector itself depends on the event loop, so it will be passed to the factory as a dependency:
The last step is to implement createClient($address)
method. It is going to accept Memcached connection string like localhost:11211
(server address by default) and will return a promise. If the connection is successfully established the promise resolves with an instance of our client. Otherwise, the promise rejects with an exception:
I don’t want to cover Memcached protocol in these articles because it will take too long to mention all the details: how the request is constructed and how we should parse responses. Here are the official protocol description and a nice article with all commands summary. Take a look if you are interested. The implementation of the protocol parser is beyond this article, but it is available in the source code on GitHub. And we will continue with asynchronous code and integration with ReactPHP ecosystem.
Although we haven’t yet created Client
class, the factory itself is ready. To create our future client we should call the factory like this:
Factory’s createClient()
method returns a promise which resolves with an instance of our client. Next, let’s move on implementing a client.
Client
Making requests
The client communicates with Memcached server via a duplex (readable and writable) stream. That means that we are sending raw data to it, and the server returns one or many raw responses.
So, the client depends on a duplex stream and Memcached protocol parser:
ReactPHP Stream Component already has an interface for a duplex stream (React\Stream\DuplexStreamInterface
), so we can type hint it int the constructor. I don’t want to implement wrappers for all Memcached commands in the client. Instead, we can use __call()
magic method and consider all calls to methods that are not implemented in the client as Memcached commands.
To execute these commands asynchronously and don’t wait for the results we are going to use deferred objects and promises. Just to refresh in memory:
- A promise is a placeholder for the initially unknown result of the asynchronous code.
- A deferred represents the code which is going to be executed to receive this result.
If you are new to ReactPHP promises check this article, it completely describes them.
The logic is the following. When we call a method that is not implemented in Client
, the __call()
method is being executed. In this method, we create an instance of the React\Promise\Deferred
class. Then we parse the called method’s name and passed arguments into the actual Memcached command. This command is written to the connection stream. The deferred object is stored in the client’s state as a pending request and its promise is returned from the method. For storing deferred objects we use a wrapper - class Request
. It represents a command which was sent to the server and a deferred object that should be resolved with the response for this command:
And here is the implementation of the client’s __call()
method:
We create a request with a command name. Then the protocol parser creates a query string that is sent to the connection and a pending request is stored in the state. The next step is resolving pending requests.
Handling responses
In the state of the client we store deferred objects and appropriate commands, so we can later resolve their promises with appropriate responses from the server. When we receive some data from the connection stream, we consider it as a response data from Memcached server. Then we can use these responses to resolve the pending requests.
To process received from Memcached server data we need to attach a handler to the duplex stream’s data
event:
Every time the connection receives data, this handler will be called with this chunk of data. The process of handling the received data consists of two steps:
- The protocol parser parses the raw data into a batch of responses (because we can receive responses for several commands at once).
- We resolve pending requests with these responses.
- If there are no pending requests but we have received a response, that means that something went wrong and we throw an exception.
And that is all. The first very primitive version of the streaming client is ready. To check it, we can use a simple example. Let’s put something into the cache and then retrieve it:
If we run this check script the result is the following:
The client is not limited only to set()
/get()
commands. Because of the magic __call()
method, it accepts any existing Memcached command. For example, we can get version()
of the server like this:
Conclusion
The client is almost ready. You can call any Memcached command on it and asynchronously receive the result. But the client is still very simple and should be improved. For example, there is no way to manually close the connection nor to handle errors. We can call any non-existing command and there is no check for valid response when resolving pending requests. All these improvements will be implemented in the next articles.
Continue reading with Building ReactPHP Memcached Client: Errors And Connection Handling.
This Memcached client was inspired by Christian Lück and his php-redis-react library.
Interested in ReactPHP? Check ReactPHP Series for more articles about asynchronous PHP.