I am very pleased with the rapid development of PHP over the past few years. Probably you too. There are constantly new opportunities that keep enthusiasts staying on this platform. That only is the recent news about the release of
Hack .
Surely someone reading even the title of this article will grin and think: "Monsieur knows a lot about perversions!" Disputes about the coolness of a language never subside, but anyway, I personally see for myself 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
post about creating a chat on Tornado and I wanted to talk about how I solved a similar problem with PHP.
Prehistory
One day I decided to get acquainted with WebSockets. I was intrigued by the technology, although I wouldn’t say that it appeared only yesterday, and this coincided with the launch of a single chat service of socionic topics that suffered from a mass of flaws. It gave me the excitement to take part in a competitive race. Using web sockets was a fundamentally new and promising solution.
The connection is established permanent 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 via setInterval, excess 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 it became much better, however, in essence, the principle has not changed. The client part still polled the server with a predetermined frequency, except that now it was possible to abandon the use of the iframe and reduce the server load a little at the expense of less data being sent.
Actually, the mentioned chat service was a classic ajax chat.
There is a truth and the reverse side of the coin in the chosen approach:
- lack of support on older browsers
- manual control of maintaining the connection
- use of the daemon in the server part
If I didn’t worry about the first one, because my target audience was young people with modern computers and mobile gadgets, in which WebSockets support was implemented for a long time, then later on the difficulties that I’ll tell you later.
Using the same demon has a number of features:
- You can update the code only by restarting the daemon - respectively, for the chatlan, this happens to some extent noticeably
- Fatal errors and unhandled exceptions lead to the crash of the daemon - the code should be written "bulletproof"
- The daemon must use a dedicated free port - this is a problem for those who sit behind a strict firewall.
- Use non-blocking functions
Those who have never heard of what the "resident program" is, and wrote only the code for the web page that works on the "started-worked-died" principle, experience a break in the pattern when writing a demon for the first time. For example, it turns out that instantiated objects can “live” for a long time and store information without using database-type storage, and access to which can be obtained from different connections to the daemon. Perhaps it is when writing it that one can most acutely encounter the problem of blocking functions and simply the lack of sharpening PHP under asynchrony.
What is generally asynchronous? If in simple terms, this is the ability of the code to
"parallelize" ,
to execute several pieces of code independently of each other .
UPD:
alekciy rightly remarked:
Do not be confused. Asynchronous! = Parallel. Classic JS can work asynchronously, but not in parallel (on the one-thread VM). Useful reading: How timers work in JavaScript .
Asynchrony - the possibility of inconsistent code execution. Parallelism - the ability to execute the same code at the same time.
I hope that the reader is familiar with at least the basics of JavaScript. Most have ever written something like:
var myDomElement.onclick = function() { alert("I'm hit!"); }
Elementary, yes? The event handler for a click event 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 event object." The second “how to make sure that the object is constantly polled for this event?”. Well, let's say we make some endless loop in which this event will be polled. And then we will face a number of serious restrictions. First, the polling frequency should not be too low for the system to respond satisfactorily. And should not be too high, so as not to create problems with the load on the system. Secondly, when there are several events, there will be a problem with the fact that until the first handler runs out, the other will not start its work. And if you need to handle thousands of connections at the same time?
But
ReactPHP appears on the scene and does magic.
Ingredients
- The basis of the server part was the Ratchet package, which is essentially an add-on to ReactPHP for working with WebSockets.
- It was thought to use a javascript framework, something like AngularJS, but at that time I wanted to quickly launch a project and learning a new framework did not fit into a tight schedule. So at first there was naked javascript, then jQuery also connected.
- I didn’t want to bother with layout and design, so I turned to Twitter Bootstrap 3
- I considered that it would be rather important to use HTML5 Notifications, instead of blinking the page title or the sound notification.
- The resulting daemon required its own separate port, so I used nginx and set up WebSockets proxying to solve the problem with firewalls. For the sake of interest also screwed SSL-certificate
Brief structure
The server part consists of two parts of the code that are asymmetric in size: cassic web pages (index, password recovery) and a demon service.
The main page solves the tasks of downloading the client web application, as well as the session initialization.
The daemon is at the core of the implementation of the MessageComponentInterface interface from the Ratchet package as a class MyApp \ Chat. Implemented methods handle the
onOpen ,
onClose ,
onError, and
onMessage events .
Each of the handlers, with the exception of onError, is a Chain-of-Responsibility template. The most voluminous piece of code came in onMessage, where it was decomposed into controllers.
Problems encountered and solutions
- The first thing we had to face was that fatals, any errors without a custom handler, and unhandled exceptions kill the demon. 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 strong lack of time, but still it will be. Simple errors, probably, they themselves know, are solved simply by using custom ErrorHandler + logging.
- A problem was discovered when, after several days of operation, someone disconnected and the chat demon began to eat 100% of the CPU, although there were no brakes in the chat. Corrected a patch from the author Ratchet, found in GitHub. However, for some reason it is still not included in the ReactPHP package.
Patchdiff --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 or closed 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');
- Retention of connections - perhaps quite an important problem. On normal connections via a wired network or decent wi-fi, everything was fine. However, when entering 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 lightly loaded and everyone is silent in the chat, it could throw it away after 30 seconds. And it could not even throw out. So, for prevention, I added a cyclic sending of the “ping” command to the server to create activity. But as it turned out, with greater workload of the BS, it did not roll either.
In general, the implementation of the algorithm long ago suggested itself: a delayed disconnection of a user from an array of present users after a timeout expired. Obviously, this requires the use of asynchronous code. Naturally, no sleep () was suitable here. I wondered about all sorts of implementation options, including even the server queues. The solution was found and turned out to be simple and elegant: ReactPHP allows the use of timers that hang on the EventLoop. It looks like this:
private function handleDisconnection(User $user) { $loop = MightyLoop::get()->fetch();
- Connecting to the database in daemon mode makes sense to keep it open for reasons of performance and to minimize logging clutter with connection errors. In any case, we had to add a crutch method called before each request to the PDO wrapper to ensure connection with the database:
protected function checkConnection() { try { $this->dbh->query('select 1'); } catch (\Exception $e) { $this->init(); } }
Alas, I have not found a more elegant solution. We still have to experiment with Redis, especially since there is a ready-made package predis-async . - Each browser tab generates a new connection. And letting the user multiply by cloning somehow did not want to. It was necessary to prohibit connections with the same session. This behavior is different from the classic chat rooms, which make it easy to work simultaneously in an arbitrary number of windows or tabs with one session.
What can chat now and what else will learn
Of the main features:
- chat daemon takes up about 20mb in memory and this figure is stable. That's not bad;
- lack of mandatory registration, the user enters the chat immediately;
- registration, authorization and password recovery;
- can do private sessions and private messages (without creating a separate channel);
- personal blacklist;
- chat roulette on the basis of socionic type;
- imperceptibly to the user when the connection is broken, reconnection is made;
- avoid duplication of connections;
- flood control.
What is wrong:
- no decent ORM, samopal;
- the session handler is also self-made;
- no tests;
- no multithreading.
What is expected to finalize:
- to experiment with NoSQL DB, for example Redis;
- separate rooms-channels;
- downloadable avatars;
- setting up various types of notifications;
- setting personal notes on users;
- indication "now prints" in private channels.
What conclusions can be made after 2 months of project development? PHP still has potential. At least the beginning of work with the event-oriented paradigm is supposed to. But alas, so far the language is trying to catch up, not to become the head of the movement. If we compare the Ratchet and Tornado, then according to the possibilities they are not equal. Hopefully, development in this direction will continue with positive acceleration.
For the curious, the source code of the project can be seen
here .
Constructive comments are welcome.
PS
Article on performance comparison
Node.js vs ReactPHP .
Sample proxy
socket2http .