WebSocket, the protocol that enables two-way asynchronous communication between the client and the host, has been around for quite a few years. And yet, despite many benefits it provides, PHP’s support for this technology is still shaky to say the least. But does it mean that we can’t use it with PHP at all? If we can, what are the best ways to implement it? I will try to answer these questions for you today.
Websockets has been well-known for a long time, yet we still don’t see it that often in actual PHP applications. Real-time data feed is important, improves user experience and allows for better performance, both on the frontend and backend side. Websockets cut down latency and help avoid HTTP roundtrips, because once opened, a socket stays open.
Also, it’s 2019 and we should use WebSockets. I will show you how to approach this problem in PHP.
WebSocket app with PHP & Symfony – possible solution
I found three popular ways to add WebSockets support to a Symfony application.
- Geniuses of Symfony WebSocket bundle, that is, Ratchet wrapped in a Symfony-friendly bundle infrastructure,
- Ratchet in its raw form with a custom wrapper,
- Swoole extension in its raw form, also with a custom wrapper.
GOS Bundle approach
The Geniuses Of Symfony WebSocket bundle uses the Ratchet WebSocket framework under the hood, but builds a lot on top of it. The bundle is highly integrated with the Symfony framework.
The architecture of the bundle is pretty complicated – uses controller routing and standard YAML configuration files. The architecture tries to impose its own way of writing real-time enabled applications, which might sound appealing, but instead could introduce problems you wouldn’t ever expect. Authorization is based on the Symfony security bundle and, quite honestly, I wasn’t able to get the authorization working in reasonable time.
The implementation tends to be a bit too verbose and scattered to be put into a gist, so here is a link to the official demo application that is provided.
GOS WebSocket Bundle – pros:
- Tightly integrated with Symfony and follows its architecture – good if you want to use Symfony only and have consistent codebase,
- If you can get it to do what you want, it will save you a lot of coding,
- Introduces assets with JS files for frontend; some might find it useful.
GOS WebSocket Bundle – cons:
- Low-quality documentation,
- almost no demo applications to check how to do what you want,
- if you want it to do something it was not exactly designed for, you could get into a situation where you will need to write more code as workaround than you would write with your own custom Ratchet wrapper,
- implementation follows several weird choices and naming conventions,
- tightly integrated with Symfony and its mechanisms – bad if you want to change either the framework or WebSocket implementation,
- seems to waste CPU while idle for an unknown reason,
- weird problems with Docker as it doesn’t accept hostnames for binding addresses – only IPs (As a result, php is not a valid host, while 172.22.0.3 is. It may cause problems with portability, which could probably be resolved on the fly if the config was not in YAML).
Ratchet approach to implementing WebSocket
Ratchet is a WebSocket framework and, in contrast to GOS, offers only a raw implementation, which turns out to be very flexible. Writing a custom wrapper might not seem like a good idea. After all, why repeat the work already done by the GOS team with their bundle? Makes sense, but there are situations when this is actually a good idea. At times, writing your own wrapper is easier, faster, safer and more maintenance-friendly than using a ready-to-use one, especially when the latter is not quite as good as you have hoped for.
At first glance, it might seem difficult to integrate a Symfony container and dependency injection with a system that doesn’t use any of these, but it really isn’t. Just run Ratchet server using a Symfony command. This way, the framework’s kernel is already booted once it starts Ratchet. You are good to go.
Authorization can be done in a stateful or stateless manner.
Stateful means that, just after connecting, the client needs to send authorization data, be it a username/password or token received from the standard endpoint. The wrapper needs to store connected clients anyway for broadcasting purposes. Once the client sends proper credentials, a wrapper can store information on the client’s authorization along with corresponding identification data.
Stateless means that the client needs to send credentials with every request. While it may seem easy, broadcasting can make it quite complicated, as the wrapper doesn’t know which client is authorized. I will discuss these problems later in the article.
A simple class wrapping the server might look like this:
The start method should be fired from the Symfony console command so that the Framework is booted up.
Ratchet – pros:
- Seems to be the least complicated solution,
- allows great flexibility.
Ratchet – cons:
- Custom wrapper is required,
- Ratchet’s internal structure may be hard to grasp.
Swoole approach for implementing WebSocket
Swoole is an extension for PHP written in C. Among other useful features, it offers a WebSocket server. I wrote more about Swoole here. The approach with Swoole is very similar to what we did with Ratchet. To the point that if we abstract the server, the same custom wrapper can be used with both Swoole in Ratchet.
However, there are two ways in which Swoole is different: it’s multi-process and incredibly fast. Of course, we cannot complain about speed. On the other hand, multiprocessing is a problem, because stateful applications can’t use process memory, since it’s not shared. Instead, we can use something like redis, memcached or a database to store the application’s state as a list of connected WebSockets.
The wrapper’s code for Swoole will look very similar to that of Ratchet. You can find it in a linked Swoole article I mentioned above.
Swoole – pros:
- It’s really fast,
- allows for great flexibility.
Swoole – cons:
- for some businesses, an extension made in China might not be an option,
- multiprocessing complicates stateful applications (it can be resolved by settings the process count to 1, at the expense of performance).
Common problems with WebSockets in Symfony
There are a couple of common problems that affect all the approaches. One of them is choosing between a stateful or stateless implementation and handling robustness in both variants. The stateful variant relies on storing clients’ authentication and other session data in the PHP WebSocket server memory. This gives speed, but has some drawbacks. For example, if a process dies, all the data is lost and scaling becomes difficult. Generally speaking, I can’t recommend this solution. The stateless variant can be implemented with a persistence layer. Employing solutions such as memcached and redis or reusing a database will affect the speed, but bring a lot of safety – servers can die, WebSockets can reconnect and continue operations, and scaling becomes less of an issue.
But there is still one more important issue, that is memory usage. PHP was not designed to support long-running processes. Instead, it expects to boot and die with every request. This decision affects memory management in the PHP core. And it turns out, memory leaks in case of WebSockets and Symfony are hilariously bad.
To analyze this issue, I created a very simple application that uses Doctrine for persistence and has only one endpoint on the WebSocket side. This endpoint creates one entity, persists it, then uses repository to search for some entities and sends them to all connected WebSockets. It’s irrelevant which WebSocket framework we use, because the leak is related to Symfony and (probably) Doctrine. Here is a graph that shows memory usage correlated with a request number:
Yes, I did try running GC manually, flushing the entity manager, clearing the entity manager and setting SQL logger to null – to no avail.
WebSockets implementations in PHP – comparison and conclusion
First of all, when it comes to throughput, the performance of all of the solutions I presented is not important that much. That’s because the majority of processing power will be used on Symfony itself and your application, not the code that handles WebSockets. While Swoole is technically the fastest, as it is the only solution that handles WebSockets with native code, the more complicated the app, the less it matters.
Still, tempting as it is, PHP is clearly not ready yet to support a long-running WebSocket server. Memory leaks are a huge issue; forcing you to restart the server. While this can be done, it might introduce hard-to-debug errors and inconsistency to to the application’s state if not done carefully.
What to do instead?
Despite what I’ve just said, there are still a couple of practical use cases for WebSockets now. Each of them comes with a few drawbacks, but that’s the price you’ve got to pay for now:
- A WebSocket server can be rebooted to keep memory usage in check. A lot of attention is required to handle rebooting correctly so that no message is dropped in the process. Rebooting scheduler would need to be written too (possibly not in PHP).
- We can also try to use SSE with PHP, but learning resources are hard to find and no main library emerged yet.
- Of course, you can just skip WebSockets altogether and go for the old school Ajax periodic calls instead 🙂