
لقد
تم بالفعل التطرق إلى موضوع
Websockets مرارًا وتكرارًا على حبري ، ولا سيما خيارات التنفيذ في PHP. ومع ذلك ، لقد مر أكثر من عام منذ
المقالة الأخيرة مع نظرة عامة على التقنيات المختلفة ، ولدى عالم PHP
شيء يتباهى به بمرور الوقت.
في هذه المقالة ، أود أن أعرض
Swoole على المجتمع الناطق بالروسية - إطار مفتوح المصدر غير متزامن لـ PHP ، مكتوب في C ، ويتم تسليمه كملحق pecl.
مصادر في جيثب .
لماذا ابتسم؟
بالتأكيد سيكون هناك أشخاص من حيث المبدأ سيعارضون استخدام PHP لهذه الأغراض ، ومع ذلك ، يمكنهم غالبًا اللعب لصالح PHP:
- عدم الرغبة في تربية حديقة حيوانات بمختلف اللغات في المشروع
- القدرة على استخدام قاعدة التعليمات البرمجية المطورة بالفعل (إذا كان المشروع في PHP).
ومع ذلك ، حتى بالمقارنة مع node.js / go / erlang واللغات الأخرى التي تقدم في الأصل نموذجًا غير متزامن ، فإن Swoole - إطار مكتوب في C ويجمع بين عتبة دخول منخفضة ووظائف قوية يمكن أن يكون مرشحًا جيدًا.
ميزات الإطار:- حدث ، نموذج برمجة غير متزامن
- واجهات برمجة تطبيقات TCP / UDP / HTTP / Websocket / HTTP2 Client / Server غير متزامنة
- دعم IPv4 / IPv6 / Unixsocket / TCP / UDP و SSL / TLS
- تسلسل / إلغاء تسلسل البيانات بسرعة
- أداء فائق وقابلية توسعة ودعم لما يصل إلى مليون اتصال متزامن
- ميلي ثانية مهمة جدولة
- المصدر المفتوح
- دعم Coroutines
حالات الاستخدام الممكنة:- الخدمات الدقيقة
- خوادم الألعاب
- إنترنت الأشياء
- أنظمة الاتصالات الحية
- WEB API
- أي خدمات أخرى تتطلب استجابة فورية / سرعة عالية / تنفيذ غير متزامن
يمكن رؤية أمثلة من التعليمات البرمجية على
الصفحة الرئيسية للموقع . في قسم التوثيق ، معلومات أكثر تفصيلاً حول جميع وظائف إطار العمل.
دعنا نبدأ
فيما يلي سأصف عملية كتابة خادم Websocket بسيط للدردشة عبر الإنترنت والصعوبات المحتملة في ذلك.
قبل البدء: مزيد من المعلومات حول
فئتي swoole_websocket_server و
swoole_server (ترث الفئة الثانية من الأولى).
مصادر الدردشة نفسها.تثبيت الإطارLinux 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
لاستخدام الإكمال التلقائي في IDE ، يقترح استخدام
المساعد ideقالب خادم Websocket الحد الأدنى: <?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 هو معرف الاتصال.
احصل على الاتصالات الحالية:
$server->connections;
يحتوي الإطار على جميع البيانات المرسلة. فيما يلي مثال لكائن دخل في وظيفة onMessage:
Swoole\WebSocket\Frame Object ( [fd] => 20 [data] => {"type":"login","username":"new user"} [opcode] => 1 [finish] => 1 )
يتم إرسال البيانات إلى العميل باستخدام الوظيفة
Server::push($fd, $data, $opcode=null, $finish=null)
اقرأ المزيد عن الإطارات
والرموز التشفيرية باللغة الروسية على
learn.javascript . قسم "تنسيق البيانات"
قدر الإمكان حول بروتوكول Websocket -
RFCوكيفية حفظ البيانات التي وصلت إلى الخادم؟يقدم Swoole وظائف للعمل بشكل غير متزامن مع
MySQL و
Redis وملف I / Oبالإضافة إلى
swoole_buffer و
swoole_channel و
swoole_tableأعتقد أن الاختلافات ليست صعبة الفهم من التوثيق. لتخزين أسماء المستخدمين ، اخترت swoole_table. يتم تخزين الرسائل نفسها في MySQL.
لذا ، تهيئة جدول اسم المستخدم:
$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();
تعبئة البيانات على النحو التالي:
$count = count($messages_table); $dateTime = time(); $row = ['username' => $username, 'message' => $data->message, 'date_time' => $dateTime]; $messages_table->set($count, $row);
للعمل مع MySQL ، قررت عدم استخدام النموذج غير المتزامن بعد ، ولكن الوصول إليه بالطريقة القياسية ، من خادم مقبس الويب ، من خلال PDO
مناشدة القاعدة 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 ، تقرر إصداره في شكل فئة ، وبدء تشغيله في المنشئ:
مُنشئ 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(); }
المشاكل التي تمت مواجهتها:- ينقطع اتصال المستخدم المتصل بالدردشة بعد 60 ثانية إذا لم يكن هناك تبادل للحزم (أي أن المستخدم لم يرسل أو يستقبل أي شيء)
- يفقد خادم الويب الاتصال بـ MySQL إذا لم يحدث تفاعل لفترة طويلة
الحل:
في كلتا الحالتين ، نحتاج إلى تنفيذ وظيفة ping ، والتي ستقوم باستمرار باختبار العميل كل n ثانية في الحالة الأولى ، وقاعدة بيانات MySQL في الثانية.
نظرًا لأن كلا الوظيفتين يجب أن تعمل بشكل غير متزامن ، يجب استدعاؤهما في العمليات الفرعية للخادم.
للقيام بذلك ، يمكن تهيئة مع الحدث "workerStart". لقد قمنا بالفعل بتعريفه في المُنشئ ، ومع هذا الحدث ، فإن طريقة $ this-> onWorkerStart تسمى بالفعل:
بروتوكول Websocket يدعم
كرة الطاولة خارج الصندوق. أدناه يمكنك رؤية التنفيذ على 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); } }); }
بعد ذلك ، قمت بتنفيذ وظيفة بسيطة لإجراء اختبار ping لخادم MySQL كل ثانية N باستخدام swoole \ Timer:
قاعدة البياناتيبدأ المؤقت نفسه في initPdo إذا لم يكن ممكّنًا بالفعل:
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(); } }
يتألف الجزء الرئيسي من العمل من كتابة المنطق لإضافة الرسائل وحفظها وإرسالها (ليست أكثر تعقيدًا من CRUD المعتاد) ، ثم نطاق كبير للتحسينات.
لقد أحضرت حتى الآن الرمز الخاص بي إلى شكل قابل للقراءة إلى حد ما وأسلوب موجه للكائنات ، قمت بتطبيق بعض الوظائف:
- تسجيل الدخول بالاسم.
- تأكد من أن الاسم ليس مشغولاً private function isUsernameCurrentlyTaken(string $username) { foreach ($this->usersRepository->getByIds($this->ws->connection_list()) as $user) { if ($user->getUsername() == $username) { return true; } } return false; }
- محدد البريد المزعج <?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); } }
ملاحظة: نعم ، التحقق موجود على معرف الاتصال. ربما يكون من المنطقي استبداله في هذه الحالة ، على سبيل المثال ، بعنوان IP الخاص بالمستخدم.
أنا لست متأكدًا أيضًا من أنه في هذه الحالة كانت قناة swoole_channel هي الأنسب. أعتقد في وقت لاحق لمراجعة هذه اللحظة.
- حماية XSS
بسيطة باستخدام
ezyang / htmlpurifier- عامل تصفية بسيط للبريد العشوائيمع إمكانية إضافة شيكات إضافية في المستقبل.
<?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; } }
دردشة الواجهة الأمامية لا تزال خامًا جدًا ، لأن أنا أكثر انجذابًا إلى الواجهة الخلفية ، ولكن عندما يتوفر المزيد من الوقت ، سأحاول جعلها أكثر متعة.
من أين تحصل على المعلومات ، والحصول على الأخبار حول الإطار؟
- الموقع الرسمي باللغة الإنجليزية - روابط مفيدة ووثائق حديثة وقليل من تعليقات المستخدمين
- تويتر - الأخبار الحالية ، روابط مفيدة ، مقالات مثيرة للاهتمام
- تعقب المشكلات (Github) - الأخطاء والأسئلة والتواصل مع منشئي الإطار. يجيبون بذكاء شديد (أجابوا على سؤالي بسؤال في بضع ساعات ، وساعدوا في تنفيذ pingloop).
- القضايا المغلقة - أنصح أيضًا. قاعدة بيانات كبيرة من الأسئلة من المستخدمين والإجابات من منشئي الإطار.
- الاختبارات التي يكتبها المطورون - تحتوي كل وحدة من الوثائق تقريبًا على اختبارات مكتوبة بلغة PHP ، تُظهر حالات الاستخدام.
- إطار عمل الويكي الصيني - جميع المعلومات كما في اللغة الإنجليزية ، ولكن الكثير من التعليقات من المستخدمين (مترجم جوجل للمساعدة).
وثائق API - وصف لبعض فئات ووظائف الإطار في شكل مناسب إلى حد ما.
الملخص
يبدو لي أن Swoole تطورت بنشاط كبير في العام الماضي ، وقد خرجت من المرحلة عندما يمكن تسميتها "الخام" ، وهي الآن في منافسة كاملة مع استخدام node.js / go من حيث البرمجة غير المتزامنة وتنفيذ بروتوكولات الشبكة.
سأكون سعيدًا لسماع آراء مختلفة حول الموضوع وردود الفعل من أولئك الذين لديهم خبرة بالفعل في استخدام Swoole
يمكنك الدردشة في غرفة الدردشة الموضحة عن طريق
الرابطالمصادر متوفرة على
جيثب .