
The theme of
Websocket `s has been touched on more than once in Habré, in particular, options for implementing in PHP were considered. However, more than a year has passed since the
last article with a review of various technologies, and the PHP world
has something to boast of over time.
In this article I want to introduce
Swoole to the Russian-speaking community - Asynchronous Open Source framework for PHP, written in C, and delivered as a pecl extension.
')
View the resulting application (chat), you can:
here .
Sources on github .
Why Swoole?
Surely there will be people who, in principle, will be against using PHP for such purposes, but in favor of PHP they can often play:
- Reluctance to breed a zoo of various languages on the project
- The ability to use the already established code base (if the project is in PHP).
Nevertheless, even comparing with node.js / go / erlang and other languages natively offering the asynchronous model, Swoole - a framework written in C and combining the low threshold of entry and powerful functionality can be quite a good candidate.
Framework features:- Event, asynchronous programming model
- Asynchronous TCP / UDP / HTTP / Websocket / HTTP2 client / server APIs
- Support IPv4 / IPv6 / Unixsocket / TCP / UDP and SSL / TLS
- Fast serialization / deserialization of data
- High performance, expandability, support for up to 1 million simultaneous connections
- Task Scheduler accurate to milliseconds
- Open source
- Coroutines support (Coroutines)
Possible uses:- Microservices
- Game servers
- Internet of things
- Live communication systems
- WEB API
- Any other services that require instant response / high speed / asynchronous execution
Code samples can be seen on the
main page of the site . In the documentation section for more information about the entire functionality of the framework.
Getting Started
Below I will describe the process of writing a simple Websocket server for online chat and the possible difficulties.
Before you begin: More information about the classes
swoole_websocket_server and
swoole_server (The second class is inherited from the first).
Sources of the chat.Installing the frameworkLinux users
#!/bin/bash
pecl install swoole
Mac users
# get a list of avaiable packages
brew install swoole
#!/bin/bash
brew install homebrew/php/php71-swoole
To use autocomplete in IDE it is suggested to use
ide-helperMinimal Websocket Server Template: <?php $server = new swoole_websocket_server("127.0.0.1", 9502); $server->on('open', function($server, $req) { echo "connection open: {$req->fd}\n"; }); $server->on('message', function($server, $frame) { echo "received message: {$frame->data}\n"; $server->push($frame->fd, json_encode(["hello", "world"])); }); $server->on('close', function($server, $fd) { echo "connection close: {$fd}\n"; }); $server->start();
$ fd is the connection identifier.
Get current connections:
$server->connections;
Inside the $ frame contains all the data sent. Here is an example of the incoming object in the onMessage function:
Swoole\WebSocket\Frame Object ( [fd] => 20 [data] => {"type":"login","username":"new user"} [opcode] => 1 [finish] => 1 )
Data is sent to the client using the function
Server::push($fd, $data, $opcode=null, $finish=null)
Learn more about frames and opcodes in Russian -
learn.javascript . Section "data format"
The most detailed about the protocol Websocket -
RFCHow to save data that came to the server?Swoole provides functionality for asynchronous work with
MySQL ,
Redis ,
file I / O.And also
swoole_buffer ,
swoole_channel and
swoole_tableI think the differences are not difficult to understand the documentation. For storing usernames, I chose swoole_table. The messages themselves are stored in MySQL.
So, initialization of the table of user names:
$users_table = new swoole_table(131072); $users_table->column('id', swoole_table::TYPE_INT, 5); $users_table->column('username', swoole_table::TYPE_STRING, 64); $users_table->create();
Filling data is as follows:
$count = count($messages_table); $dateTime = time(); $row = ['username' => $username, 'message' => $data->message, 'date_time' => $dateTime]; $messages_table->set($count, $row);
To work with MySQL, I decided not to use the asynchronous model, but to use the standard way, from the web socket server, via PDO
Appeal to the base public function getAll() { $stmt = $this->pdo->query('SELECT * from messages'); $messages = []; foreach ($stmt->fetchAll() as $row) { $messages[] = new Message( $row['username'], $row['message'], new \DateTime($row['date_time']) ); } return $messages; }
Websocket server, it was decided to issue in the form of a class, and start it in the constructor:
Constructor public function __construct() { $this->ws = new swoole_websocket_server('0.0.0.0', 9502); $this->ws->on('open', function ($ws, $request) { $this->onConnection($request); }); $this->ws->on('message', function ($ws, $frame) { $this->onMessage($frame); }); $this->ws->on('close', function ($ws, $id) { $this->onClose($id); }); $this->ws->on('workerStart', function (swoole_websocket_server $ws) { $this->onWorkerStart($ws); }); $this->ws->start(); }
Problems encountered:- The user connected to the chat breaks the connection after 60 seconds if there is no packet exchange (that is, the user did not send or receive anything)
- The web server loses connection with MySQL if no interaction takes place for a long time.
Decision:
In both cases, the implementation of the ping function is needed, which will constantly ping the client in the first case every n seconds, and the MySQL database in the second.
Since both functions must work asynchronously, they must be called in the child processes of the server.
To do this, they can be initialized at the workerStart event. We have already defined it in the constructor, and this event already calls the $ this-> onWorkerStart method:
Websocket protocol supports
ping-pong out of the box. Below you can see the implementation on Swoole.
onWorkerStart private function onWorkerStart(swoole_websocket_server $ws) { $this->messagesRepository = new MessagesRepository(); $ws->tick(self::PING_DELAY_MS, function () use ($ws) { foreach ($ws->connections as $id) { $ws->push($id, 'ping', WEBSOCKET_OPCODE_PING); } }); }
Next, I implemented a simple function for pinging a MySQL server every N seconds using swoole \ Timer:
DatabasehelperThe timer itself starts in initPdo if not already enabled:
private static function initPdo() { if (self::$timerId === null || (!Timer::exists(self::$timerId))) { self::$timerId = Timer::tick(self::MySQL_PING_INTERVAL, function () { self::ping(); }); } self::$pdo = new PDO(self::DSN, DBConfig::USER, DBConfig::PASSWORD, self::OPT); } private static function ping() { try { self::$pdo->query('SELECT 1'); } catch (PDOException $e) { self::initPdo(); } }
The main part of the work was to write logic for adding, saving, sending messages (no more difficult than the usual CRUD), and then a huge scope for improvements.
So far, I've brought my code to a more or less readable form and object-oriented style, implemented a bit of functionality:
- Login by name;
- Verify that the name is not taken private function isUsernameCurrentlyTaken(string $username) { foreach ($this->usersRepository->getByIds($this->ws->connection_list()) as $user) { if ($user->getUsername() == $username) { return true; } } return false; }
- Request spammer for spam protection <?php namespace App\Helpers; use Swoole\Channel; class RequestLimiter { private $userIds; const MAX_RECORDS_COUNT = 10; const MAX_REQUESTS_BY_USER = 4; public function __construct() { $this->userIds = new Channel(1024 * 64); } public function checkIsRequestAllowed(int $userId) { $requestsCount = $this->getRequestsCountByUser($userId); $this->addRecord($userId); if ($requestsCount >= self::MAX_REQUESTS_BY_USER) return false; return true; } private function getRequestsCountByUser(int $userId) { $channelRecordsCount = $this->userIds->stats()['queue_num']; $requestsCount = 0; for ($i = 0; $i < $channelRecordsCount; $i++) { $userIdFromChannel = $this->userIds->pop(); $this->userIds->push($userIdFromChannel); if ($userIdFromChannel === $userId) { $requestsCount++; } } return $requestsCount; } private function addRecord(int $userId) { $recordsCount = $this->userIds->stats()['queue_num']; if ($recordsCount >= self::MAX_RECORDS_COUNT) { $this->userIds->pop(); } $this->userIds->push($userId); } }
PS: Yes, the check is on connection id. Perhaps it makes sense to replace it in this case, for example, with the user's IP address.
I'm also not sure that swoole_channel was best suited in this situation. I think later to reconsider this moment.
-
Simple XSS protection using
ezyang / htmlpurifier- simple spam filterWith the ability to further add additional checks.
<?php namespace App\Helpers; class SpamFilter { private $errors = []; public function checkIsMessageTextCorrect(string $text) { $isCorrect = true; if (empty(trim($text))) { $this->errors[] = 'Empty message text'; $isCorrect = false; } return $isCorrect; } public function getErrors(): array { return $this->errors; } }
Frontend at the chat is still very raw, because I am more attracted to the backend, but when there is more time I will try to make it more pleasant.
Where to get information, get news about the framework?
- English official site - useful links, relevant documentation, some comments from users
- Twitter - current news, useful links, interesting articles
- Issue tracker (Github) - bugs, questions, communication with the creators of the framework. They answer very smartly (they answered my issue with a question in a couple of hours, helped with the implementation of pingloop).
- Closed issues - also advise. A large database of questions from users and answers from the creators of the framework.
- Tests written by developers - for almost every module of the documentation there are tests written in PHP, showing use cases.
- Chinese wiki framework - all the information is in English, but much more comments from users (google translator to help).
API documentation - the description of some classes and functions of the framework in a rather convenient form.
Summary
It seems to me that Swoole was very actively developing over the past year, out of the stage when it could be called “raw”, and now it is in competition with the use of node.js / go from the point of view of asynchronous programming and implementation of network protocols.
I will be glad to hear different opinions on the topic and feedback from those who already have experience using Swoole
You can chat in the described chatik by
reference.Sources are available on
Github .