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_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-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引入了与
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,我决定不再使用异步模型,而是通过Web套接字服务器通过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秒后断开连接
- 如果长时间没有交互,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中启动:
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); } }
PS:是的,验证在连接ID上。 在这种情况下,例如用用户的IP地址替换它也许很有意义。
我也不确定在这种情况下最适合swoole_channel。 我想稍后再修改这一刻。
-使用
ezyang / htmlpurifier的简单 XSS保护
-简单的垃圾邮件过滤器将来可以添加其他支票。
<?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; } }
前端聊天仍然很原始,因为 我对后端更感兴趣,但是如果有更多时间,我将尝试使其更加愉悦。
在哪里获取信息,获取有关框架的新闻?
- 英文官方网站 -有用的链接,最新文档,用户的几点评论
- Twitter-当前新闻,有用的链接,有趣的文章
- 问题跟踪器(Github) -错误,问题以及与框架创建者的沟通。 他们非常聪明地回答(他们在几个小时内用一个问题回答了我的问题,并帮助执行了pingloop)。
- 已解决的问题 -我也建议。 来自用户的问题和框架创建者的答案的大型数据库。
- 开发人员编写的测试 -文档中几乎每个模块都有用PHP编写的测试,其中显示了用例。
- 中文Wiki框架 -所有信息均以英文提供,但来自用户的评论更多(由Google翻译帮助)。
API文档 -以相当方便的形式描述框架的某些类和功能。
总结
在我看来,Swoole去年一直在非常积极地发展,它已经脱离了可以称之为“原始”的阶段,现在它在异步编程和网络协议实现方面与使用node.js / go处于完全竞争中。
我很高兴听到关于该主题的不同意见,以及已经有使用Swoole经验的人的反馈
您可以通过
链接在描述的聊天室聊天
来源可从
Github上获得 。