نكتب محادثة عبر الإنترنت على Websockets باستخدام Swoole



لقد تم بالفعل التطرق إلى موضوع 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

مناشدة القاعدة
 /** * @return Message[] */ 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(); } 


المشاكل التي تمت مواجهتها:

  1. ينقطع اتصال المستخدم المتصل بالدردشة بعد 60 ثانية إذا لم يكن هناك تبادل للحزم (أي أن المستخدم لم يرسل أو يستقبل أي شيء)
  2. يفقد خادم الويب الاتصال بـ 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 إذا لم يكن ممكّنًا بالفعل:

  /** * Init new Connection, and ping DB timer function */ 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); } /** * Ping database to maintain the connection */ private static function ping() { try { self::$pdo->query('SELECT 1'); } catch (PDOException $e) { self::initPdo(); } } 


يتألف الجزء الرئيسي من العمل من كتابة المنطق لإضافة الرسائل وحفظها وإرسالها (ليست أكثر تعقيدًا من CRUD المعتاد) ، ثم نطاق كبير للتحسينات.

لقد أحضرت حتى الآن الرمز الخاص بي إلى شكل قابل للقراءة إلى حد ما وأسلوب موجه للكائنات ، قمت بتطبيق بعض الوظائف:

- تسجيل الدخول بالاسم.

- تأكد من أن الاسم ليس مشغولاً
 /** * @param string $username * @return bool */ 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 { /** * @var Channel */ private $userIds; const MAX_RECORDS_COUNT = 10; const MAX_REQUESTS_BY_USER = 4; public function __construct() { $this->userIds = new Channel(1024 * 64); } /** * Check if there are too many requests from user * and make a record of request from that user * * @param int $userId * @return bool */ public function checkIsRequestAllowed(int $userId) { $requestsCount = $this->getRequestsCountByUser($userId); $this->addRecord($userId); if ($requestsCount >= self::MAX_REQUESTS_BY_USER) return false; return true; } /** * @param int $userId * @return int */ 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; } /** * @param int $userId */ 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 { /** * @var string[] errors */ private $errors = []; /** * @param string $text * @return bool */ public function checkIsMessageTextCorrect(string $text) { $isCorrect = true; if (empty(trim($text))) { $this->errors[] = 'Empty message text'; $isCorrect = false; } return $isCorrect; } /** * @return string[] errors */ public function getErrors(): array { return $this->errors; } } 


دردشة الواجهة الأمامية لا تزال خامًا جدًا ، لأن أنا أكثر انجذابًا إلى الواجهة الخلفية ، ولكن عندما يتوفر المزيد من الوقت ، سأحاول جعلها أكثر متعة.

من أين تحصل على المعلومات ، والحصول على الأخبار حول الإطار؟


  • الموقع الرسمي باللغة الإنجليزية - روابط مفيدة ووثائق حديثة وقليل من تعليقات المستخدمين
  • تويتر - الأخبار الحالية ، روابط مفيدة ، مقالات مثيرة للاهتمام
  • تعقب المشكلات (Github) - الأخطاء والأسئلة والتواصل مع منشئي الإطار. يجيبون بذكاء شديد (أجابوا على سؤالي بسؤال في بضع ساعات ، وساعدوا في تنفيذ pingloop).
  • القضايا المغلقة - أنصح أيضًا. قاعدة بيانات كبيرة من الأسئلة من المستخدمين والإجابات من منشئي الإطار.
  • الاختبارات التي يكتبها المطورون - تحتوي كل وحدة من الوثائق تقريبًا على اختبارات مكتوبة بلغة PHP ، تُظهر حالات الاستخدام.
  • إطار عمل الويكي الصيني - جميع المعلومات كما في اللغة الإنجليزية ، ولكن الكثير من التعليقات من المستخدمين (مترجم جوجل للمساعدة).

وثائق API - وصف لبعض فئات ووظائف الإطار في شكل مناسب إلى حد ما.

الملخص


يبدو لي أن Swoole تطورت بنشاط كبير في العام الماضي ، وقد خرجت من المرحلة عندما يمكن تسميتها "الخام" ، وهي الآن في منافسة كاملة مع استخدام node.js / go من حيث البرمجة غير المتزامنة وتنفيذ بروتوكولات الشبكة.

سأكون سعيدًا لسماع آراء مختلفة حول الموضوع وردود الفعل من أولئك الذين لديهم خبرة بالفعل في استخدام Swoole

يمكنك الدردشة في غرفة الدردشة الموضحة عن طريق الرابط
المصادر متوفرة على جيثب .

Source: https://habr.com/ru/post/ar427589/


All Articles