
Le thème des
Websockets a déjà été abordé à plusieurs reprises sur Habré, en particulier des options d'implémentation en PHP ont été envisagées. Cependant, plus d'un an s'est écoulé depuis le
dernier article avec un aperçu des différentes technologies, et le monde PHP
a de quoi se vanter au fil du temps.
Dans cet article, je veux présenter
Swoole à la communauté russophone - Framework Open Source asynchrone pour PHP, écrit en C, et livré en tant qu'extension pecl.
Sources sur github .
Pourquoi swoole?
Il y aura sûrement des gens qui seront en principe contre l'utilisation de PHP à de telles fins, cependant, ils peuvent souvent jouer en faveur de PHP:
- Réticence à élever un zoo de différentes langues sur le projet
- Possibilité d'utiliser une base de code déjà développée (si le projet est en PHP).
Néanmoins, même en comparant avec node.js / go / erlang et d'autres langages qui proposent nativement un modèle asynchrone, Swoole - un framework écrit en C et combinant un seuil d'entrée bas et des fonctionnalités puissantes peut être un bon candidat.
Caractéristiques du cadre:- Événement, modèle de programmation asynchrone
- API client / serveur TCP / UDP / HTTP / Websocket / HTTP2 asynchrones
- Prise en charge IPv4 / IPv6 / Unixsocket / TCP / UDP et SSL / TLS
- Sérialisation / désérialisation rapide des données
- Haute performance, extensibilité, prise en charge de jusqu'à 1 million de connexions simultanées
- Planificateur de tâches en millisecondes
- Open source
- Assistance: Coroutines
Cas d'utilisation possibles:- Microservices
- Serveurs de jeux
- Internet des objets
- Systèmes de communication en direct
- API WEB
- Tout autre service nécessitant une réponse instantanée / haute vitesse / exécution asynchrone
Des exemples de code peuvent être vus sur la
page principale du site . Dans la section documentation, des informations plus détaillées sur toutes les fonctionnalités du framework.
Commençons
Ci-dessous, je décrirai le processus d'écriture d'un simple serveur Websocket pour le chat en ligne et les difficultés possibles avec cela.
Avant de commencer: Plus d'informations sur les
classes swoole_websocket_server et
swoole_server (la deuxième classe hérite de la première).
Sources du chat lui-même.Installer le 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
Pour utiliser la saisie semi-automatique dans l'IDE, il est proposé d'utiliser
ide-helperModèle de serveur Websocket minimum: <?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 est l'identifiant de connexion.
Obtenez les connexions actuelles:
$server->connections;
$ Frame contient toutes les données envoyées. Voici un exemple d'un objet entré dans la fonction onMessage:
Swoole\WebSocket\Frame Object ( [fd] => 20 [data] => {"type":"login","username":"new user"} [opcode] => 1 [finish] => 1 )
Les données sont envoyées au client à l'aide de la fonction
Server::push($fd, $data, $opcode=null, $finish=null)
En savoir plus sur les cadres et les opcodes en russe sur
learn.javascript . Section "format des données"
Autant que possible sur le protocole Websocket -
RFCEt comment enregistrer les données qui sont arrivées sur le serveur?Swoole introduit des fonctionnalités pour travailler de manière asynchrone avec
MySQL ,
Redis ,
les E / S de fichiersAinsi que
swoole_buffer ,
swoole_channel et
swoole_tableJe pense que les différences ne sont pas difficiles à comprendre à partir de la documentation. Pour stocker les noms d'utilisateurs, j'ai sélectionné swoole_table. Les messages eux-mêmes sont stockés dans MySQL.
Donc, initialisation de la table des noms d'utilisateurs:
$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();
Le remplissage des données est le suivant:
$count = count($messages_table); $dateTime = time(); $row = ['username' => $username, 'message' => $data->message, 'date_time' => $dateTime]; $messages_table->set($count, $row);
Pour travailler avec MySQL, j'ai décidé de ne pas encore utiliser le modèle asynchrone, mais d'y accéder de manière standard, depuis le serveur de socket web, via PDO
Appel à la 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; }
Serveur Websocket, il a été décidé d'émettre sous forme de classe, et de le démarrer dans le constructeur:
Constructeur 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(); }
Problèmes rencontrés:- Un utilisateur connecté au chat se déconnecte après 60 secondes s'il n'y a pas d'échange de paquets (c'est-à-dire que l'utilisateur n'a rien envoyé ni reçu)
- Le serveur Web perd la connexion avec MySQL si aucune interaction ne se produit pendant longtemps
Solution:
Dans les deux cas, nous avons besoin d'une implémentation de la fonction ping, qui testera en permanence le client toutes les n secondes dans le premier cas, et la base de données MySQL dans le second.
Étant donné que les deux fonctions doivent fonctionner de manière asynchrone, elles doivent être appelées dans les processus enfants du serveur.
Pour ce faire, ils peuvent être initialisés avec l'événement "workerStart". Nous l'avons déjà défini dans le constructeur, et avec cet événement, la méthode $ this-> onWorkerStart est déjà appelée:
Le protocole Websocket prend en charge le
ping-pong hors de la boîte. Ci-dessous, vous pouvez voir l'implémentation sur 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); } }); }
Ensuite, j'ai implémenté une fonction simple pour envoyer un ping à un serveur MySQL toutes les N secondes en utilisant swoole \ Timer:
DatabaseHelperLe temporisateur lui-même démarre dans initPdo s'il n'est pas déjà activé:
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(); } }
La partie principale du travail consistait à écrire une logique pour ajouter, enregistrer, envoyer des messages (pas plus compliqué que le CRUD habituel), puis une énorme marge d'amélioration.
Jusqu'à présent, j'ai apporté mon code sous une forme plus ou moins lisible et un style orienté objet, j'ai implémenté un peu de fonctionnalité:
- Connectez-vous par nom;
- Vérifiez que le nom n'est pas occupé private function isUsernameCurrentlyTaken(string $username) { foreach ($this->usersRepository->getByIds($this->ws->connection_list()) as $user) { if ($user->getUsername() == $username) { return true; } } return false; }
- Limiteur de spam <?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: Oui, la vérification est sur l'identifiant de connexion. Il est peut-être judicieux de le remplacer dans ce cas, par exemple, par l'adresse IP de l'utilisateur.
Je ne suis pas sûr non plus que dans cette situation, ce soit swoole_channel qui soit le mieux adapté. Je pense plus tard à revoir ce moment.
- Protection XSS
simple en utilisant
ezyang / htmlpurifier- Filtre anti-spam simpleAvec la possibilité d'ajouter des contrôles supplémentaires à l'avenir.
<?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; } }
Le chat frontal est encore très brut, car Je suis plus attiré par le backend, mais quand il y aura plus de temps, j'essaierai de le rendre plus agréable.
Où obtenir des informations, obtenir des nouvelles sur le cadre?
- Site officiel anglais - liens utiles, documentation à jour, quelques commentaires des utilisateurs
- Twitter - actualités, liens utiles, articles intéressants
- Issue tracker (Github) - bugs, questions, communication avec les créateurs du framework. Ils répondent très intelligemment (ils ont répondu à ma question avec une question dans quelques heures, aidé à la mise en œuvre de pingloop).
- Problèmes résolus - je conseille également. Une grande base de données de questions d'utilisateurs et de réponses des créateurs du framework.
- Tests écrits par les développeurs - presque tous les modules de la documentation ont des tests écrits en PHP, montrant des cas d'utilisation.
- Framework wiki chinois - toutes les informations comme en anglais, mais beaucoup plus de commentaires des utilisateurs (traducteur google pour vous aider).
Documentation API - une description de certaines classes et fonctions du framework sous une forme assez pratique.
Résumé
Il me semble que Swoole s'est développé très activement l'année dernière, il est sorti de l'étape où il pouvait être appelé "brut", et maintenant il est en pleine concurrence avec l'utilisation de node.js / go en termes de programmation asynchrone et de mise en œuvre de protocoles réseau.
Je serai heureux d'entendre différentes opinions sur le sujet et les commentaires de ceux qui ont déjà de l'expérience avec Swoole
Vous pouvez discuter dans la salle de chat décrite par le
lienLes sources sont disponibles sur
Github .