How I Integrated WebSockets into an Existing PHP System

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

    Libraries


    Writing a server and client for web sockets is very difficult, fortunately there is almost no 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). We will also 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 tablet in MySQL with one column, a public address for connecting like wss: //web.site/WebSockets (to store in MySQL or somewhere else is not important, the main thing is that all servers have access to the same information) . Each launched server adds itself to the pool, and if it is not there alone, it connects to the first as a master (if one, it becomes a master itself). If the master does not respond, it is removed from the pool and the next one becomes the master, everyone will connect to it. This ensures stability from falls and complete decentralization.

    When one of the servers receives a message from the client, it sends it to the master, and the master sends it to everyone else. For simplicity, the 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, among other things, and so it doesn’t matter which server and in how many tabs the user is connected to.

    Sending and receiving messages


    The message format was chosen simple:

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

    An action is just a string, there can be something scalar as a part, then it is converted to an array with one element.

    The format is common 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, you can specify two callbacks on the client - success and error .

    On the client, messages are received and sent by a bunch of methods of the cs.WebSockets object (which establishes and maintains a connection to the server automatically, also authenticates itself using the current session):

    • on (action, callback, error)
    • off (action, callback, error)
    • once (action, callback, error)
    • send (action, details)

    It’s so simple that it’s probably nowhere simpler. Since the parts passed are an array - each element will be passed as a separate argument.

    Everything is a bit more complicated on the server. A 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 will be established with one from servers, and the message will be transmitted in the internal format, after which it will reach the client).

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

    To receive the standard event system is used, you need to subscribe to the WebSockets / {action} event(subscription is made upon the occurrence of the WebSockets / register_action event ), where {action} is what is received from the client, the current user (sender), its session and language are also sent to the event.

    Hello service example:

    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 start


    Here it is already a little more complicated. It was decided to support starting 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, then the web version will still launch a console version of the server under the hood. The server listens on the port specified in the settings to 0.0.0.0 or 127.0.0.1 to choose from (then Nginx, or whatever is in its place, sends all connections to wss: //web.site/WebSockets to this port , that is, that user is used same port 80 or 443, 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 to see if everything is fine with the server. In the console version, it restarts the server instantly when the process crashes, in the web version, it is checked with an interval of 10 seconds whether the server is alive using a test connection, if not, it restarts.

    The engine is designed so that the kernel starts, then a certain module corresponding to the page is executed, then the conclusion is made. So the web socket server is an ordinary module, but it doesn’t 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 send and receive messages when communicating with the master / member server) use the same event-loop
    2. All preparatory actions must be done before the server starts, after the start of the event-loop all the 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 objects, you need to disable them, otherwise after a few days the process can eat up too much memory, the situation can be checked by loading the server using, for example, Siege
    4. Need to save time; since you most likely do not use non-blocking alternative options of built-in functions for everything, the next client will not be served until a blocking operation is performed, so minimize the work of something under the server
    5. Prepare your code for long-term execution (in my case, in some places a constant was used with the script launch time, 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 often mentioned, Node.js is also mentioned, 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 utilize React to create an HTTP server in pure PHP ).

    To try


    If you want to play, the Docker container is waiting for you:

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

    Then go to localhost: 8888 in the browser, turn on the web socket module, start the server. In the / some_dir folder, add any modules, experiment, try, everything immediately gets into the demo. After stopping you will only have to delete / some_dir ( --rm switch will delete the container itself automatically).

    Source

    Also popular now: