📜 ⬆️ ⬇️

How I integrated WebSockets into an existing PHP system

The article will be about how an uncharacteristic thing for PHP like web sockets can be integrated into an existing system using the example of CleverStyle CMS, and what nuances may arise.

Libraries


Writing a server and a client for web sockets is very difficult, fortunately there is a practically non-alternative Ratchet library that provides a server for web sockets. Under the hood, it uses several parts of ReactPHP and Guzzle (it also depends on the symfony components, but in this case they turned out to be completely redundant). Also we will use Pawl from the author of Ratchet, this is a client for web sockets.

Architecture


Since CleverStyle CMS can work with several servers, it was decided to support this functionality here. For this, a server pool is created. It is a MEMORY table in MySQL with a single column, a public address for connecting the wss: //web.site/WebSockets (store in MySQL or somewhere else does not matter, the main thing is that all servers have access to the same information) . Each server that is started adds itself to the pool, and if it’s not one there, it connects to the first as a master (if one becomes the master itself). If the master does not respond - it is removed from the pool and the next becomes a master, everyone will connect to it. This ensures stability against falls and complete decentralization.

When one of the server receives a message from the client - it sends it to the master, and the master sends to all the others. For simplicity, servers communicate with each other using web sockets too. When you need to send a message to a user with a specific id, it is also sent to the master further as well, therefore it does not matter at all what server and in how many tabs the user is connected.
')

Sending and receiving messages


The message format was simple:

{ "action" : "some/action", "details" : [] } 

The action is just a string, as the details there can be something scalar, then it is converted into an array with one element.

Common format for sending from client to server, and from server to client. The only difference is that the action for the client can end with the suffix : error , for convenience, the client can specify two callbacks - success and error .

At the client, receiving and sending messages is performed by a packet of cs.WebSockets object methods (which establishes and maintains the connection to the server automatically, and also authenticates itself using the current session):


It's so simple that it's probably easier nowhere. Since the transferred parts are an array, each element will be passed as a separate argument.

Everything is a little harder on the server. The message is sent using the \ cs \ modules \ WebSockets :: send_to_clients ($ action, $ details, $ send_to, $ target) method (you can send not only from under the server, but also on ordinary pages, in this case a connection with one from the servers, and the message will be transmitted in an internal format, after which it will reach the client).

Additional arguments allow you to specify which user or group, or several specific users, need to deliver the message.

For reception, the standard event system is used, you need to subscribe to the WebSockets / {action} event (the subscription is made when the WebSockets / register_action event occurs), where {action} is what was received from the client, the current user (the sender) is passed to the event , his session and language.

Example hello service:

 <?php use cs\Event, cs\modules\WebSockets\Server; // Register actions Event::instance()->on('WebSockets/register_action', function () { // If `hello` action from user Event::instance()->on('WebSockets/hello', function ($data) { $Server = Server::instance(); // Send `hello` action back to the same user with the same content if ($data['details']) { $Server->send_to_clients( 'hello', $data['details'], Server::SEND_TO_SPECIFIC_USERS, $data['user'] ); } else { $Server->send_to_clients( 'hello:error', $Server->compose_error( 400, 'No hello message:(' ), Server::SEND_TO_SPECIFIC_USERS, $data['user'] ); } }); }); ?> 

 // Since everything is asynchronous - lets add handler first cs.WebSockets.once('hello', function (message) { alert(message); }); // Now send request to server cs.WebSockets.send('hello', 'Hello, world!'); 

Everything is very simple.

Server startup


Here it is a little more difficult. It was decided to support the launch from the web interface (there is even a button in the admin panel), as well as from the command line. Moreover, if the execution of console commands in PHP is available, the web version will still launch the console version of the server under the hood. The server listens on the specified in the settings port to 0.0.0.0 or 127.0.0.1 to choose from (then Nginx, or what is in its place, sends all connections to wss: //web.site/WebSockets to this port, that is, the user uses 80 or 443 ports, other ports can be blocked in many situations like public Wi-Fi).

The web socket server has a simple supervisor option - an additional duplicate process that looks good with the server. In the console version, it restarts the server instantly when the process crashes, in the web version, with an interval of 10 seconds, it is checked whether the server is alive using a test connection, if not, it restarts.

The engine is designed so that the kernel is launched, then a certain module is executed that corresponds to the page, then an output is made. So, the web socket server is a regular module, but it does not reach the output, the event-loop starts and listens for connections.

Some pitfalls


  1. There can be only one event loop. In this regard, both the client and the server (since both can both send and receive messages when communicating a master / private server) use the same event-loop
  2. All preparatory actions need to be done before starting the server, after the event-loop has been started, the entire linear code is blocked until it stops (intersects with the first item, but still)
  3. It is necessary to protect the memory; if there are any caches in the objects - they need to be disabled, otherwise after a few days the process may consume too much memory, you can check the situation by loading the server using, for example, Siege
  4. Need to save time; since you most likely do not use non-blocking alternatives of built-in functions for everything, the next client will not be served before performing a blocking operation, so minimize the work of something under the server
  5. Prepare your code for a long run (in my case in some places a constant was used with the time the script was run, which ceased to be relevant in the case of a long-playing process)

That's all


Although web sockets and similar asynchronous things are not native to PHP, and when they are mentioned, Node.js is often remembered, as well as how to combine it with PHP, the latter itself can easily work with the same paradigm and not die after each request (with certain skills, you can use React to create an HTTP server in pure PHP ).

To try


If you want to play - Docker container is waiting for you:

 docker run --rm -p 8888:8888 -v /some_dir:/web nazarpc/cleverstyle-cms 

Then go to the localhost: 8888 address in the browser, enable the web sockets module, start the server. In the folder / some_dir add any modules, experiment, try, everything immediately falls into the demo. After stopping, it will only remain to delete / some_dir (the --rm key will remove the container itself automatically).

Source

Source: https://habr.com/ru/post/250085/


All Articles