We need to perform batches of HTTP requests. For example, we need to download several video files. We can start downloading them one by one, but it will take a lot of time since we need to wait for each request to be finished before we can start a new one. The larger the number of requests we are dealing with, the more this latency grows. We also cannot perform any other operations until all files will be downloaded.
In an asynchronous way, there is no need to wait until the last request is being finished. We can start processing the results immediately when any of the requests are being finished. ReactPHP has HttpClient component which allows you to send HTTP requests asynchronously.
ReactPHP HttpClient
The component itself is very simple. To start sending requests we need an instance of the React\HttpClient\Client class:
Client class is very simple and its interface consists of the one request() method. It accepts a request method, URL, optional additional headers and returns an instance of the React\HttpClient\Request class. Let’s create a GET request to https://php.net:
By this moment we are not executing any real requests, we have only prepared a request. Next step is to set up it.
React\HttpClient\Request class implements WritableStreamInterface, so we can attach handlers for some events of this stream. Now the most interesting for us is the response event. This event is emitted when the response headers were received from the server and successfully parsed:
The callback for this event accepts an instance of the \React\HttpClient\Response as an argument. This class implements ReadableStreamInterface which means that we can also consider it as a stream and read data from it. To receive the response body we can listen to the data event. The handler for this event receives a chunk of the response body as an argument.
But if we run this code we still get nothing. This is because there will be no execution till we call end() method on the request object:
This method indicates that we have finished sending the request.
Downloading File
We can use an instance of the \React\HttpClient\Response class as a readable stream and then pipe it to a writable stream as a source. As a result, we can read from the response and write it to a file. Create a filesystem object and then open a file in a write mode:
We unwrap the stream with \React\Promise\Stream\unwrapWritable() function. This function can be used to unwrap a Promise which resolves with a WritableStreamInterface. It returns a writable stream which acts as a proxy for the future promise resolution.
If you are new to streams check this article about ReactPHP streams.
In this example, we download a small video sample using a GET request and stream it to a local file. As soon as the request starts returning chunks of the downloading video it will pipe that data to the sample.mp4 file.
Note that this example uses fopen() for simplicity and demo purposes only! This should not be used in a truly asynchronous application because the filesystem is inherently blocking and each call could potentially take several seconds. Read this tutorial in case you need to work asynchronously with the filesystem in ReactPHP ecosystem.
As a next step, we can add a progress for our download. To track the progress we need to know the total size of the downloading file and the current downloaded size. We can use getHeaders() method of the response object to retrieve server headers. We need a Content-Length header, which contains the full size of the file:
To get the current downloaded size we can use the length of the received data. We start with zero and then every time we receive a new chunk of data we increase this value by the length of this data:
Now we need to merge streaming to the local file and tracking the progress. We can wrap the outputting the download progress into an instance of the \React\Stream\ThroughStream(). This sort of streams can be used to process data through the pipes, exactly what we need. We write data to this stream, the data is being processed and then we can read the processed data from it.
In this snippet, we read data from the response. Then pipe it to track the download progress and then pipe this data to the local file on disk. The progress is showing but the output doesn’t look great.
To fix this issue we can use cursor movement character. ANSI escape sequences allow moving the cursor around the screen. We can use this sequence to move the cursor N lines up \033[<N>A. In our case, we need to move the cursor one line up (\033[1A). And because we are moving a cursor one line up, we should add one line break before we start showing the progress:
With this simple changes, the output looks pretty nice!
Parallel Downloading
When our main logic for downloading file is ready we can extract it to a class and improve it for handling multiple parallel downloads. This class will have a single download() method which will accept an array of links. When we call download() it starts downloading specified files in parallel, like this:
This class will be a wrapper over the HTTP client. We also need an instance of the event loop to perform some async operations. So, we require them in the constructor:
The main process of handling multiple downloads will be the following. For every specified link we:
Instantiate a request.
Setup all handlers for this request.
Store this request in a property.
When all requests are instantiated and configured we walk through them and for each request call end() method to start sending data. The last step is to run() the event loop:
Our initRequest() method will be very similar to the code from the previous section where we download a single file:
The only difference is that now we need to show several lines of output. That’s why we need a $position variable which is used to format the output properly according to the specified number of links. All the rest code is the same: we create an instance of the request and setup the handlers. Then we store this request in the $requests property.
We can refactor this method and extract configuring an instance of the ThroughStream into its own method:
Now the last step is to run the requests. We need to call end() method on each request stored in the $requests property and then call run() method on the event loop. After that we clear the $requests property. This is the final version of Downloader class:
Then we init an event loop and a client, pass them into the constructor and call download() method with a list of links:
The files are downloaded in parallel:
Conclusion
When it comes to sending HTTP requests in an asynchronous way ReactPHP HttpClient can be a very useful component. It becomes really powerful when using it with streams and piping the data.
You can find examples from this article on GitHub.