我们使用Swoole在Websockets上进行在线聊天



Websockets的主题已经在Habré上反复提及,特别是考虑了在PHP中实现的选项。 但是,自从上一篇文章介绍各种技术以来已经过去了一年多的时间,随着时间的流逝,PHP世界也有了一些值得夸耀的东西

在本文中,我想向Swoole讲俄语社区-PHP的异步开放源代码框架,用C编写,并作为pecl扩展提供。

来源github

为什么要抽烟?


肯定会有一些人在原则上反对将PHP用于此类目的,但是,他们经常可以支持PHP:

  • 不愿在该项目中繁殖各种语言的动物园
  • 能够使用已经开发的代码库(如果项目使用PHP)。

但是,即使与node.js / go / erlang和其他本机提供异步模型的语言进行比较,Swoole(用C编写并结合了较低的入门阈值和强大功能的框架)也可能是不错的选择。

框架的特点:

  • 事件,异步编程模型
  • 异步TCP / UDP / HTTP / Websocket / HTTP2客户端/服务器API
  • 支持IPv4 / IPv6 / Unixsocket / TCP / UDP和SSL / TLS
  • 快速数据序列化/反序列化
  • 高性能,可扩展性,支持多达一百万个并发连接
  • 毫秒任务计划程序
  • 开源的
  • 协程支持

可能的用例:

  • 微服务
  • 游戏服务器
  • 物联网
  • 实时通讯系统
  • WEB API
  • 其他需要即时响应/高速/异步执行的服务

在网站的主页上可以找到代码示例。 在文档部分中,有关框架的所有功能的更多详细信息。

让我们开始吧


下面,我将描述编写用于在线聊天的简单Websocket服务器的过程以及此过程可能遇到的困难。

开始之前:有关swoole_websocket_serverswoole_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-helper

最小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; 

$ Frame包含所有发送的数据。 这是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引入了与MySQLRedis文件I / O异步工作的功能

以及swoole_bufferswoole_channelswoole_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,我决定不再使用异步模型,而是通过Web套接字服务器通过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. 如果长时间没有交互,Webserver将失去与MySQL的连接

解决方案:

在这两种情况下,我们都需要实现ping函数,在第一种情况下,它将每隔n秒不断对客户端进行ping操作,在第二种情况下,将持续对MySQL数据库进行ping操作。

由于这两个函数必须异步工作,因此必须在服务器的子进程中调用它们。

为此,可以使用“ 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); } }); } 


接下来,我实现了一个简单的功能,使用swoole \ Timer每N秒对MySQL服务器执行一次ping操作:

数据库助手
如果尚未启用计时器,计时器本身将在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); } } 

PS:是的,验证在连接ID上。 在这种情况下,例如用用户的IP地址替换它也许很有意义。

我也不确定在这种情况下最适合swoole_channel。 我想稍后再修改这一刻。

-使用ezyang / htmlpurifier的简单 XSS保护

-简单的垃圾邮件过滤器
将来可以添加其他支票。

 <?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; } } 


前端聊天仍然很原始,因为 我对后端更感兴趣,但是如果有更多时间,我将尝试使其更加愉悦。

在哪里获取信息,获取有关框架的新闻?


  • 英文官方网站 -有用的链接,最新文档,用户的几点评论
  • Twitter-当前新闻,有用的链接,有趣的文章
  • 问题跟踪器(Github) -错误,问题以及与框架创建者的沟通。 他们非常聪明地回答(他们在几个小时内用一个问题回答了我的问题,并帮助执行了pingloop)。
  • 已解决的问题 -我也建议。 来自用户的问题和框架创建者的答案的大型数据库。
  • 开发人员编写的测试 -文档中几乎每个模块都有用PHP编写的测试,其中显示了用例。
  • 中文Wiki框架 -所有信息均以英文提供,但来自用户的评论更多(由Google翻译帮助)。

API文档 -以相当方便的形式描述框架的某些类和功能。

总结


在我看来,Swoole去年一直在非常积极地发展,它已经脱离了可以称之为“原始”的阶段,现在它在异步编程和网络协议实现方面与使用node.js / go处于完全竞争中。

我很高兴听到关于该主题的不同意见,以及已经有使用Swoole经验的人的反馈

您可以通过链接在描述的聊天室聊天
来源可从Github上获得

Source: https://habr.com/ru/post/zh-CN427589/


All Articles