
Tema
Websockets sudah berulang kali disinggung tentang Habré, khususnya opsi untuk implementasi dalam PHP dipertimbangkan. Namun, lebih dari satu tahun telah berlalu sejak
artikel terakhir dengan tinjauan berbagai teknologi, dan dunia PHP
memiliki sesuatu untuk dibanggakan dari waktu ke waktu.
Pada artikel ini saya ingin memperkenalkan
Swoole ke komunitas berbahasa Rusia - Kerangka kerja Open Source Asynchronous untuk PHP, ditulis dalam C, dan dikirim sebagai ekstensi pecl.
Sumber di github .
Mengapa swoole?
Tentunya akan ada orang-orang yang pada prinsipnya akan menentang penggunaan PHP untuk tujuan tersebut, namun, mereka sering dapat bermain mendukung PHP:
- Keengganan untuk membiakkan kebun binatang dari berbagai bahasa di proyek
- Kemampuan untuk menggunakan basis kode yang sudah dikembangkan (jika proyek dalam PHP).
Namun demikian, bahkan membandingkan dengan node.js / go / erlang dan bahasa lain yang secara asli menawarkan model asinkron, Swoole - kerangka kerja yang ditulis dalam C dan menggabungkan ambang entri yang rendah dan fungsi yang kuat dapat menjadi kandidat yang baik.
Fitur kerangka kerja:- Acara, model pemrograman asinkron
- API Asynchronous TCP / UDP / HTTP / Websocket / HTTP2 Client / Server
- Mendukung IPv4 / IPv6 / Unixsocket / TCP / UDP dan SSL / TLS
- Serialisasi / deserialisasi data yang cepat
- Kinerja tinggi, ekstensibilitas, mendukung hingga 1 juta koneksi simultan
- Penjadwal tugas milidetik
- Sumber terbuka
- Dukungan Coroutines
Kasing yang mungkin digunakan:- Layanan microser
- Server game
- Internet hal
- Sistem Komunikasi Langsung
- API WEB
- Layanan lain apa pun yang memerlukan respons instan / kecepatan tinggi / eksekusi asinkron
Contoh kode dapat dilihat di
halaman utama situs . Di bagian dokumentasi, informasi lebih detail tentang semua fungsionalitas kerangka kerja.
Mari kita mulai
Di bawah ini saya akan menjelaskan proses penulisan server Websocket sederhana untuk obrolan online dan kemungkinan kesulitan dengan ini.
Sebelum Anda mulai: Informasi lebih lanjut tentang kelas
swoole_websocket_server dan
swoole_server (
Kelas kedua mewarisi dari yang pertama).
Sumber obrolan itu sendiri.Menginstal kerangka kerjaLinux 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
Untuk menggunakan autocomplete dalam IDE, disarankan untuk menggunakan
ide-helperTemplat Server 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 adalah pengenal koneksi.
Dapatkan koneksi saat ini:
$server->connections;
$ Frame berisi semua data yang dikirim. Berikut adalah contoh objek yang masuk ke fungsi onMessage:
Swoole\WebSocket\Frame Object ( [fd] => 20 [data] => {"type":"login","username":"new user"} [opcode] => 1 [finish] => 1 )
Data dikirim ke klien menggunakan fungsi
Server::push($fd, $data, $opcode=null, $finish=null)
Baca lebih lanjut tentang bingkai dan opcode dalam bahasa Rusia di
learn.javascript . Bagian "format data"
Sebisa mungkin tentang protokol Websocket -
RFCDan bagaimana cara menyimpan data yang datang ke server?Swoole memperkenalkan fungsionalitas untuk bekerja secara asinkron dengan
MySQL ,
Redis ,
file I / OSeperti halnya
swoole_buffer ,
swoole_channel dan
swoole_tableSaya pikir perbedaannya tidak sulit untuk dipahami dari dokumentasi. Untuk menyimpan nama pengguna, saya memilih swoole_table. Pesan-pesan itu sendiri disimpan di MySQL.
Jadi, inisialisasi tabel nama pengguna:
$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();
Mengisi data adalah sebagai berikut:
$count = count($messages_table); $dateTime = time(); $row = ['username' => $username, 'message' => $data->message, 'date_time' => $dateTime]; $messages_table->set($count, $row);
Untuk bekerja dengan MySQL, saya memutuskan untuk tidak menggunakan model asinkron, tetapi untuk mengaksesnya dengan cara standar, dari server soket web, melalui PDO
Banding ke pangkalan 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; }
Server Websocket, diputuskan untuk mengeluarkan dalam bentuk kelas, dan memulainya di konstruktor:
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(); }
Masalah yang ditemui:- Pengguna yang terhubung ke obrolan terputus setelah 60 detik jika tidak ada pertukaran paket (mis., Pengguna tidak mengirim atau menerima apa pun)
- Server web kehilangan koneksi dengan MySQL jika tidak ada interaksi yang terjadi untuk waktu yang lama
Solusi:
Dalam kedua kasus tersebut, kita membutuhkan implementasi fungsi ping, yang akan terus-menerus melakukan ping klien setiap n detik dalam kasus pertama, dan database MySQL di yang kedua.
Karena kedua fungsi harus bekerja secara tidak sinkron, mereka harus dipanggil dalam proses turunan server.
Untuk melakukan ini, mereka dapat diinisialisasi dengan acara "workerStart". Kami sudah mendefinisikannya di konstruktor, dan dengan peristiwa ini metode $ this-> onWorkerStart sudah dipanggil:
Protokol Websocket mendukung
ping-pong di luar kotak. Di bawah ini Anda dapat melihat implementasinya di 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); } }); }
Selanjutnya, saya menerapkan fungsi sederhana untuk melakukan ping ke server MySQL setiap N detik menggunakan swoole \ Timer:
DatabaseHelperTimer itu sendiri dimulai di initPdo jika belum diaktifkan:
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(); } }
Bagian utama dari karya ini terdiri dari penulisan logika untuk menambah, menyimpan, mengirim pesan (tidak lebih rumit dari CRUD biasa), dan kemudian ruang lingkup besar untuk perbaikan.
Sejauh ini saya telah membawa kode saya ke bentuk yang lebih mudah dibaca dan gaya berorientasi objek, saya menerapkan sedikit fungsi:
- Login dengan nama;
- Pastikan nama itu tidak sibuk private function isUsernameCurrentlyTaken(string $username) { foreach ($this->usersRepository->getByIds($this->ws->connection_list()) as $user) { if ($user->getUsername() == $username) { return true; } } return false; }
- Pembatas 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: Ya, verifikasi ada pada id koneksi. Mungkin masuk akal untuk menggantinya dalam hal ini, misalnya, dengan alamat IP pengguna.
Saya juga tidak yakin bahwa dalam situasi ini adalah swoole_channel yang paling cocok. Saya pikir nanti untuk merevisi momen ini.
- Perlindungan XSS
sederhana menggunakan
ezyang / htmlpurifier- Filter spam sederhanaDengan kemampuan untuk menambahkan cek tambahan di masa depan.
<?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 masih sangat mentah, karena Saya lebih tertarik pada backend, tetapi ketika ada lebih banyak waktu, saya akan mencoba membuatnya lebih menyenangkan.
Di mana mendapatkan informasi, dapatkan berita tentang kerangka kerja?
- Situs resmi bahasa Inggris - tautan yang bermanfaat, dokumentasi terbaru, beberapa komentar dari pengguna
- Twitter - berita terkini, tautan bermanfaat, artikel menarik
- Pelacak isu (Github) - bug, pertanyaan, komunikasi dengan pembuat kerangka. Mereka menjawab dengan sangat cerdas (mereka menjawab pertanyaan saya dengan pertanyaan dalam beberapa jam, membantu pelaksanaan pingloop).
- Masalah tertutup - Saya juga menyarankan. Database besar pertanyaan dari pengguna dan jawaban dari pencipta kerangka kerja.
- Tes yang ditulis oleh pengembang - hampir setiap modul dari dokumentasi memiliki tes yang ditulis dalam PHP, menunjukkan kasus penggunaan.
- Kerangka kerja wiki Cina - semua informasi dalam bahasa Inggris, tetapi lebih banyak komentar dari pengguna (penerjemah google untuk membantu).
Dokumentasi API - deskripsi beberapa kelas dan fungsi kerangka kerja dalam bentuk yang cukup nyaman.
Ringkasan
Tampaknya bagi saya bahwa Swoole telah berkembang sangat aktif tahun lalu, telah keluar dari tahap ketika itu bisa disebut "mentah", dan sekarang ia bersaing penuh dengan penggunaan node.js / go dalam hal pemrograman asinkron dan implementasi protokol jaringan.
Saya akan senang mendengar pendapat berbeda tentang topik dan umpan balik dari mereka yang sudah memiliki pengalaman menggunakan Swoole
Anda dapat mengobrol di ruang obrolan yang dijelaskan oleh
tautanSumber tersedia di
Github .