Work asynchronously in PHP or another chat history

I am very pleased with how rapidly PHP has developed rapidly over the past few years. Probably you too. There are constantly new opportunities that keep enthusiasts from staying on this platform. What is the recent news about the release of Hack .

Surely someone reading even the title of this article would grin and think: "Monsieur knows a lot about perversions!" The debate about the coolness of a particular language never subsides, but be that as it may, for myself I personally see not so many conditions for changing the language, because I like to squeeze out all the possibilities before radically changing the whole stack. Recently there was a publication about creating a chat on Tornado, and I wanted to talk about how I solved a similar problem using PHP.

Background

One fine day, I decided to get to know WebSockets. I was intrigued by the technology, although I could not say that it appeared only yesterday, and this coincided with the launch of one socionics-related chat service, which suffered from a lot of flaws. It gave me the excitement to take part in a competitive race. Using web sockets seemed like a brand new and promising solution.

The connection is established constant and bidirectional, and on the client side, the work is reduced to processing 4 events: onopen , onclose , onerror, and of course onmessage . No more requests through setInterval, excessive traffic and server load.

Let me make a small digression here for those who do not understand what it is about.
Those who are familiar with the Runet of the early 2000s may remember the variety of chat services, where everything slowed down and worked awkwardly.
AJAX appeared a bit later and became much better, but in essence the principle has not changed. The client part still polled the server at a certain frequency according to a timer, unless it was now possible to refuse to use iframes and reduce the load on the server a little due to the smaller amount of data sent.
Actually, the chat service mentioned was a classic ajax chat.

There is the truth and the flip side of the coin in the chosen approach:
  • lack of support on older browsers
  • manual connection control
  • use of the daemon in the server side


If I didn’t really worry about the first one, since my target audience was young people with modern computers and mobile gadgets in which WebSockets support has been implemented for a long time, then just about the second one there were difficulties in the future, which I will tell later.
Using the daemon has a number of features:
  1. You can update the code only by restarting the daemon - accordingly, for the chatlan, this happens to one degree or another noticeably
  2. Fatal errors and unhandled exceptions cause the daemon to crash - code needs to be written “bulletproof”
  3. The daemon must use a dedicated free port - this is a problem for those who are sitting behind a strict firewall
  4. Use non-blocking features


Those who have never heard of what a "resident program" is, and wrote only code for a web page that works on the principle of "run-run-run-die," experience a pattern break when writing a daemon for the first time. For example, it turns out that instantiated objects can "live" for a long time and store information without using a database type storage, and access to which can be obtained from different connections to the daemon. Perhaps it is during his writing that one can most acutely encounter the problem of blocking functions and simply the lack of PHP sharpening for asynchrony.

What is asynchrony in general? If in a simple way, then this is the ability of the code to "parallelize" , to execute several pieces of code independently of each other .
UPD: alekciy rightly remarked:
No need to be confused. Asynchronous! = Parallel. Classic JS can run asynchronously, but not in parallel (using the single-threaded VM). Useful reading: How timers work in JavaScript .

Asynchrony - the possibility of inconsistent code execution. Concurrency - the ability to execute the same code at the same time.


I hope the reader is at least familiar with the basics of JavaScript. Most at least once wrote something like:
var myDomElement.onclick = function() {
    alert("I'm hit!");
}


Elementary, right? The event handler for clicking on a page element is determined. But what if we try to do something similar in PHP?

The first question will arise "where to determine the events of the object." The second “how to make sure that the object is constantly polled for a given event?” Well, let's say we make a certain endless cycle in which this event will be polled. And then we will face a number of serious limitations. First, the polling frequency should not be too low for the system response to be satisfactory. And it should not be too high so as not to create problems with the load on the system. Secondly, when there will be several events, there will be a problem with the fact that until the first handler works, the other will not start its work. And if you need to process thousands of connections at the same time?

But ReactPHP appears on the scene and does the magic.

The ingredients

  • The server side was based on the Ratchet package , which is essentially an add-on on ReactPHP for working with WebSockets.
  • There was an idea to use a javascript framework, something like AngularJS, but at that time I wanted to start a project as quickly as possible and learning a new framework did not fit into a tight schedule. So, in the beginning, there was bare javascript, then still connected jQuery.
  • With layout and design I did not want to bother, so I turned to Twitter Bootstrap 3
  • I thought that it would be important enough to use HTML5 Notifications, instead of blinking the page title or sound notification.
  • The resulting daemon required its own separate port, so to solve the firewall problem, I used nginx and set up WebSockets proxying. For the sake of interest, I also screwed the SSL certificate


Brief structure


The server part consists of two asymmetric code parts in size: cassic web pages (index, password recovery) and a chat service demon.
The main page solves the tasks of loading the client web application, as well as initializing the session.

The daemon is basically an implementation of the MessageComponentInterface interface from the Ratchet package in the form of the MyApp \ Chat class. Implemented methods handle onOpen , onClose , onError, and onMessage events .
Each of the handlers, with the exception of onError, is a Chain-of-Responsibility pattern. The most voluminous piece of code was onMessage, where it was decomposed into controllers.

Problems and solutions


  1. The first thing I had to face was that fatals, any errors without a custom handler, and unhandled exceptions kill the daemon. With fatals and exceptions, the problem is solved only with the help of tests. To my shame, the hands did not reach the tests due to a severe lack of time, but still it will be. Simple errors, probably you yourself know, are solved simply using custom ErrorHandler + logging.
  2. A problem was revealed when, after several days of operation, someone disconnected and the chat daemon began to eat 100% of the CPU, although there were no brakes in the chat operation. Corrected a patch from the author Ratchet, found on GitHub. However, for some reason it is still not included in the ReactPHP package.
    Patch
    diff --git a / vendor / react / stream / React / Stream / Buffer.php b / vendor / react / stream / React / Stream / Buffer.php
    index e516628..4560ad9 100644
    @@ -83.8 +83.8 @@ class Buffer extends EventEmitter implements WritableStreamInterface

    public function handleWrite ()
    {
    - if (! is_resource ($ this-> stream) || ('generic_socket' === $ this-> meta ['stream_type'] && feof ($ this -> stream))) {
    - $ this-> emit ('error', array (new \ RuntimeException ('Tried to write to closed or invalid stream.')));
    + if (! is_resource ($ this-> stream)) {
    + $ this-> emit ('error', array (new \ RuntimeException ('Tried to write to invalid stream.'), $ this));

    return
    }
    @@ -107.6 +107.12 @@ class Buffer extends EventEmitter implements WritableStreamInterface
    return;
    }

    + if (0 === $ sent && feof ($ this-> stream)) {
    + $ this-> emit ('error', array (new \ RuntimeException ('Tried to write to closed stream.'), $ this));
    +
    + return;
    +}
    +
    $ len = strlen ($ this-> data);
    if ($ len> = $ this-> softLimit && $ len - $ sent <$ this-> softLimit) {
    $ this-> emit ('drain');

  3. Connection retention is perhaps an important issue. On normal connections through a wired network or decent wi-fi, everything was fine. However, when calling from the mobile Internet, it was revealed that mobile operators do not like permanent connections and cut them off, apparently, depending on several conditions. For example, if the BS is weakly loaded and everyone is silent in the chat, it could be thrown out after 30 seconds. And it might not even be thrown away. So, for prevention, I added a cyclic sending of the ping command to the server to create activity. But as it turned out, with a more busy BS, this did not work.
    In general, the implementation of the algorithm was asking for a long time: delayed disconnection of the user from the array of users present after the timeout. Obviously, this requires the use of asynchronous code. Naturally, no sleep () was good here. I wondered all kinds of implementation options, including even a queue server. The solution was found and turned out to be simple and elegant: ReactPHP allows you to use timers that are hung on EventLoop. It looks something like this:
    private function handleDisconnection(User $user)
    {
    	$loop = MightyLoop::get()->fetch(); // получили одиночку EventLoop, на котором также работают сокеты
    	$detacher = function() use ($user) {
    		// обработка удаления пользователя из реестра посетителей в онлайне
    		...	
    	};
    	if ($user->isAsyncDetach()) {
    		$timer = $loop->addTimer(30, $detacher); // 30 секунд
    		$user->setTimer($timer);
    	} else {
    		$detacher();
    	}
    	$user->getConnection()->close();
    }
    

  4. Connecting to the database in daemon mode makes sense to keep it open for performance reasons and to minimize clogging of logs with connection errors. In any case, I had to add a crutch method to the PDO wrapper that was called before each request to guarantee a connection to the database:
    protected function checkConnection()
    {
    	try {
    		$this->dbh->query('select 1');
            } catch (\Exception $e) {
    		$this->init(); 
    	}
    }
    

    Alas, I did not find a more elegant solution. We still need to experiment with Redis, especially since there is a ready-made predis-async package .
  5. Each browser tab generates a new connection. And somehow I did not want to allow the user to propagate by cloning. I had to ban connections with the same session. This behavior is different from classic chats, which make it easy to work simultaneously in an arbitrary number of windows or tabs with one session.


What chat can do now and what else to learn

Of the main features:
  • chat daemon takes about 20mb in memory and this figure is stable. It's not bad;
  • lack of mandatory registration, the user enters the chat immediately;
  • registration, authorization and password recovery;
  • able to do private sessions and private messages (without creating a separate channel);
  • personal blacklist;
  • socionic type chat roulette;
  • invisibly to the user, when the connection is disconnected, reconnection is made;
  • prevention of duplication of connections;
  • flood control.

What is wrong:
  • no decent ORM, self-made;
  • the session handler is also self-made;
  • no tests;
  • no multithreading.

What is expected to be finalized:
  • experiment with NoSQL databases, for example Redis;
  • separate channel rooms;
  • downloadable avatars;
  • setting up various types of notifications;
  • setting personal notes on users;
  • “now prints” indication in private channels.


What conclusions can be drawn after 2 months of project development? PHP still has potential. At least the beginning of work with an event-oriented paradigm has been laid. But alas, so far the language is trying to catch up, and not become the head of the movement. If you compare Ratchet and Tornado, then they are still not equal in capabilities. Let us hope that development in this direction will continue with positive acceleration.

For the curious, the source code for the project can be seen here .
Constructive comments are welcome.

PS
An article about comparing Node.js vs ReactPHP performance . Socket2http
proxy example .

Also popular now: