
Das Thema
Websockets wurde bei Habré bereits wiederholt angesprochen, insbesondere wurden Optionen für die Implementierung in PHP in Betracht gezogen. Seit dem
letzten Artikel mit einem Überblick über verschiedene Technologien ist jedoch mehr als ein Jahr vergangen, und die PHP-Welt
hat im Laufe der Zeit
etwas zu bieten.
In diesem Artikel möchte ich
Swoole der russischsprachigen Community
vorstellen - Asynchrones Open Source-Framework für PHP, geschrieben in C und geliefert als Pecl-Erweiterung.
Quellen zu Github .
Warum stürzen?
Sicherlich wird es Leute geben, die grundsätzlich gegen die Verwendung von PHP für solche Zwecke sind, aber sie können oft zugunsten von PHP spielen:
- Widerwillen, einen Zoo in verschiedenen Sprachen im Projekt zu züchten
- Möglichkeit, eine bereits entwickelte Codebasis zu verwenden (wenn das Projekt in PHP ausgeführt wird).
Selbst im Vergleich zu node.js / go / erlang und anderen Sprachen, die nativ ein asynchrones Modell bieten, kann Swoole - ein in C geschriebenes Framework, das eine niedrige Einstiegsschwelle und leistungsstarke Funktionen kombiniert - ein guter Kandidat sein.
Merkmale des Frameworks:- Ereignis, asynchrones Programmiermodell
- Asynchrone TCP / UDP / HTTP / Websocket / HTTP2-Client / Server-APIs
- Unterstützt IPv4 / IPv6 / Unixsocket / TCP / UDP und SSL / TLS
- Schnelle Serialisierung / Deserialisierung von Daten
- Hohe Leistung, Erweiterbarkeit und Unterstützung für bis zu 1 Million gleichzeitige Verbindungen
- Millisekunden-Taskplaner
- Open Source
- Coroutines-Unterstützung
Mögliche Anwendungsfälle:- Microservices
- Spieleserver
- Internet der Dinge
- Live-Kommunikationssysteme
- WEB API
- Alle anderen Dienste, die eine sofortige Antwort / hohe Geschwindigkeit / asynchrone Ausführung erfordern
Beispiele für Code finden Sie auf der
Hauptseite der Website . Im Dokumentationsabschnitt finden Sie detailliertere Informationen zu allen Funktionen des Frameworks.
Fangen wir an
Im Folgenden werde ich den Prozess des Schreibens eines einfachen Websocket-Servers für den Online-Chat und die möglichen Schwierigkeiten damit beschreiben.
Bevor Sie beginnen: Weitere Informationen zu den
Klassen swoole_websocket_server und
swoole_server (Die zweite Klasse erbt von der ersten).
Quellen des Chats selbst.Framework installierenLinux 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
Um die automatische Vervollständigung in der IDE zu verwenden, wird vorgeschlagen,
ide-helper zu verwendenMinimale Websocket-Server-Vorlage: <?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 ist die Verbindungskennung.
Aktuelle Verbindungen abrufen:
$server->connections;
$ Frame enthält alle gesendeten Daten. Hier ist ein Beispiel für ein Objekt, das in die Funktion onMessage aufgenommen wurde:
Swoole\WebSocket\Frame Object ( [fd] => 20 [data] => {"type":"login","username":"new user"} [opcode] => 1 [finish] => 1 )
Daten werden über die Funktion an den Client gesendet
Server::push($fd, $data, $opcode=null, $finish=null)
Weitere
Informationen zu Frames und Opcodes in russischer Sprache finden Sie unter
learn.javascript . Abschnitt "Datenformat"
So viel wie möglich über das Websocket-Protokoll -
RFCUnd wie speichere ich die Daten, die auf den Server kamen?Swoole bietet Funktionen für die asynchrone Arbeit mit
MySQL ,
Redis und
Datei-E / A.
Sowie
swoole_buffer ,
swoole_channel und
swoole_tableIch denke, die Unterschiede sind aus der Dokumentation nicht schwer zu verstehen. Um Benutzernamen zu speichern, habe ich swoole_table ausgewählt. Die Nachrichten selbst werden in MySQL gespeichert.
Also, Initialisierung der Benutzernamen-Tabelle:
$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();
Das Füllen mit Daten ist wie folgt:
$count = count($messages_table); $dateTime = time(); $row = ['username' => $username, 'message' => $data->message, 'date_time' => $dateTime]; $messages_table->set($count, $row);
Um mit MySQL zu arbeiten, habe ich mich entschieden, das asynchrone Modell noch nicht zu verwenden, sondern auf standardmäßige Weise vom Web-Socket-Server über PDO darauf zuzugreifen
Appell an die Basis 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, es wurde beschlossen, in Form einer Klasse auszugeben und es im Konstruktor zu starten:
Konstruktor 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(); }
Probleme aufgetreten:- Ein mit dem Chat verbundener Benutzer trennt die Verbindung nach 60 Sekunden, wenn kein Paketaustausch stattfindet (d. H. Der Benutzer hat nichts gesendet oder empfangen).
- Der Webserver verliert die Verbindung zu MySQL, wenn längere Zeit keine Interaktion stattfindet
Lösung:
In beiden Fällen benötigen wir eine Implementierung der Ping-Funktion, die im ersten Fall den Client alle n Sekunden und im zweiten die MySQL-Datenbank ständig pingt.
Da beide Funktionen asynchron arbeiten müssen, müssen sie in den untergeordneten Prozessen des Servers aufgerufen werden.
Dazu können sie mit dem Ereignis "workerStart" initialisiert werden. Wir haben es bereits im Konstruktor definiert, und mit diesem Ereignis wird die Methode $ this-> onWorkerStart bereits aufgerufen:
Das Websocket-Protokoll unterstützt sofort
Ping-Pong . Unten sehen Sie die Implementierung auf 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); } }); }
Als nächstes implementierte ich eine einfache Funktion zum Pingen eines MySQL-Servers alle N Sekunden mit swoole \ Timer:
DatabaseHelperDer Timer selbst startet in initPdo, falls er noch nicht aktiviert ist:
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(); } }
Der Hauptteil der Arbeit bestand darin, Logik zum Hinzufügen, Speichern, Senden von Nachrichten (nicht komplizierter als die übliche CRUD) zu schreiben und dann einen großen Spielraum für Verbesserungen zu schaffen.
Bisher habe ich meinen Code in eine mehr oder weniger lesbare Form und einen objektorientierten Stil gebracht. Ich habe ein paar Funktionen implementiert:
- Login mit Namen;
- Stellen Sie sicher, dass der Name nicht belegt ist private function isUsernameCurrentlyTaken(string $username) { foreach ($this->usersRepository->getByIds($this->ws->connection_list()) as $user) { if ($user->getUsername() == $username) { return true; } } return false; }
- Spam-Begrenzer <?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: Ja, die Überprüfung erfolgt über die Verbindungs-ID. Vielleicht ist es sinnvoll, es in diesem Fall beispielsweise durch die IP-Adresse des Benutzers zu ersetzen.
Ich bin mir auch nicht sicher, ob in dieser Situation swoole_channel am besten geeignet war. Ich denke später, um diesen Moment zu überarbeiten.
-
Einfacher XSS-Schutz mit
ezyang / htmlpurifier- Einfacher SpamfilterMit der Möglichkeit, in Zukunft zusätzliche Prüfungen hinzuzufügen.
<?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-Chat ist immer noch sehr roh, weil Das Backend zieht mich mehr an, aber wenn mehr Zeit bleibt, werde ich versuchen, es angenehmer zu gestalten.
Wo kann man Informationen erhalten und Neuigkeiten über das Framework erhalten?
- Englische offizielle Seite - nützliche Links, aktuelle Dokumentation, wenige Kommentare von Benutzern
- Twitter - aktuelle Nachrichten, nützliche Links, interessante Artikel
- Issue Tracker (Github) - Fehler, Fragen, Kommunikation mit den Erstellern des Frameworks. Sie antworten sehr klug (sie beantworteten meine Frage in ein paar Stunden mit einer Frage, die bei der Implementierung von Pingloop half).
- Geschlossene Ausgaben - ich rate auch. Eine große Datenbank mit Fragen von Benutzern und Antworten der Ersteller des Frameworks.
- Von Entwicklern geschriebene Tests - Fast jedes Modul aus der Dokumentation enthält in PHP geschriebene Tests, die Anwendungsfälle zeigen.
- Chinesisches Wiki-Framework - alle Informationen wie auf Englisch, aber viel mehr Kommentare von Nutzern (Google Übersetzer hilft).
API-Dokumentation - eine Beschreibung einiger Klassen und Funktionen des Frameworks in einer recht praktischen Form.
Zusammenfassung
Es scheint mir, dass Swoole sich im letzten Jahr sehr aktiv entwickelt hat, es ist aus dem Stadium herausgekommen, in dem es als "roh" bezeichnet werden könnte, und jetzt steht es in vollem Wettbewerb mit der Verwendung von node.js / go hinsichtlich der asynchronen Programmierung und Implementierung von Netzwerkprotokollen.
Ich freue mich über unterschiedliche Meinungen zu diesem Thema und Feedback von denen, die bereits Erfahrung mit Swoole haben
Sie können im beschriebenen Chatraum über den
Link chatten
Quellen sind auf
Github verfügbar.