peachtree / websocket
基于Ratchet构建的一个简单、强大且易于使用的WebSocket服务
Requires
- php: ^7.4
- ext-json: *
- cboden/ratchet: ^0.4.1
- respect/validation: ^1.1.31
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.16.1
- phpstan/phpstan: ^0.12.1
- phpunit/phpunit: ^8.3
README
这是Peachtree LLC使用的WebSocket库。这个组件最初是从我们的主应用程序中分离出来的,但由于它对我们帮助很大,我们选择将其开源!
动机
这个库并不是从头开始重新构建WebSocket服务,这只是一个在cboden/ratchet之上的抽象层。如果您需要更少的开销,它是一个构建在之上的绝佳库!
这个包的想法是提供一个非常低开销、强大且易于使用的框架,用于构建WebSocket应用程序。
安装
使用Composer安装应该相当简单。
composer require peachtree/websocket
您如何实现项目的目录结构由您自己决定,但如果您需要一个起点,我推荐以下结构。
.
├── src # Your projects source directory
│ ├── Handlers # Toss all of your message handlers in a folder
│ │ ├── BarHandler.php
│ │ └── FooHandler.php
│ └── server.php # The entry point for the server
└── vendor # For 3rd party stuff (like this package)
项目结构
在我们走得太远之前,重要的是要记下库的各个部分负责的内容。
服务器
\Peachtree\Websocket\Server
类是应用程序的主要入口点。这个类充当工厂,帮助设置WebSocket服务器。
消息
\Peachtree\Websocket\Message
对象是客户端和服务器通信的方式。一个消息由三个主要元素组成,动作、有效载荷和引用。\Peachtree\Websocket\MessageFactory
工厂可以帮助传递常用消息,如确认或验证错误。
动作是一个字符串,用于描述消息的分类。有效载荷是消息的主体,是一个PHP数组,转换为JSON对象。引用是一个字符串,设计用来帮助在客户端和服务器之间保持状态。
连接处理器
\Peachtree\Websocket\Connection\Handler
类负责管理连接到服务器的各种客户端。如果您不想使用我们的默认路由器,您可以提供自己的消息路由器。
连接状态
当客户端连接时,它们会被分配一个\Peachtree\Websocket\Connection\State
。它包含有关客户端的一些基本信息,包括其唯一标识符、用户提供的元数据、最后发送消息的时间以及它订阅的频道信息。
提供一个默认的元数据,那就是remoteAddress
。如果我们能够解析客户端的远程地址,则该值将被填充,否则将为null。这可以通过$state->getMeta()->remoteAddress
访问。
消息处理器
消息处理器(扩展\Peachtree\Websocket\Handler\MessageHandler
的类)负责处理消息。每个传入的消息都会将其动作传递到这个类的一个方法中,以指示该处理器是否负责该消息。如果是,它将传递完整的消息和客户端的连接状态到其“handle”方法。
您可以在内置的\Peachtree\Websocket\Handler\PingPong
类中看到一个超级简单的示例。
可以为任何给定的消息负责多个处理器。每个处理器将产生一个或多个响应(见下一节)。
响应
\Peachtree\Websocket\IO\Response
是一个类,可以从消息处理器中产生。它将被立即发送到发送消息的客户端。
广播
广播(\Peachtree\Websocket\IO\Broadcast
)是一种响应类型,它发送到特定频道的所有客户端,而不仅仅是发送最后一条消息的客户端。
有时您的应用程序需要进行一些耗时处理来发送广播,如果没有人正在收听频道,您可能不希望这样做。对于这些情况,您可以改用 \Peachtree\Websocket\IO\DeferredBroadcast
,其中实际的消息内容将在服务器可以确保要广播的频道有成员时生成。
路由器
提供的 \Peachtree\Websocket\Routing\Router
路由器负责将消息从客户端路由到正确的消息处理器。
使用方法
服务器类型区分
您会发现很快就会注意到有两种服务器“模式”。本例中提供的是普通套接字,但也可使用 HTTP 套接字。
区别在于,如果您想使用类似 telnet 这样的应用程序来消费您的应用程序,您将想要这样做
$server = (new \Peachtree\Websocket\Server('0.0.0.0', 8080))->socket();
如果您打算从 JavaScript 或 web 前端消费您的 websocket API,您将想要使用‘http’方法。
$server = (new \Peachtree\Websocket\Server('0.0.0.0', 8080))->http();
如果您使用 Windows,则本机 Telnet 客户端相当糟糕,因此我建议使用 PuTTY。如果您使用 Mac 或 Linux,则本机 Telnet 客户端将正常工作。
最小化服务器
非常简单,开箱即用,您可以设置一个新的服务器,如下所示
<?php
require_once 'vendor/autoload.php';
// Listen on 0.0.0.0 on port 8080
$server = (new \Peachtree\Websocket\Server('0.0.0.0', 8080))->socket();
$server->run();
如果您运行它,您可以通过 telnet 连接到该服务器。
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> test
< {"ref":null,"action":"error","payload":{"message":"Syntax error"}}
如果您输入某些内容并按回车键,例如 test
,它应该会给出一个响应,表明您的消息中有语法错误,因为它没有正确格式化。
{"ref":null,"action":"error","payload":{"message":"Syntax error"}}
如果您收到了这个,那么它正在工作!不过,在不进行进一步设置的情况下,您将无法做很多事情。
Ping Pong
假设您想向服务器发送一条消息并获得响应。在这种情况下,您将需要注册一个 消息处理器。 幸运的是,这些很容易构建,我们有一些内置的!
<?php
require_once 'vendor/autoload.php';
// Register the "PingPong" message handler with the message router
\Peachtree\Websocket\Routing\Router::addMessageHandler(
new \Peachtree\Websocket\Handler\PingPong()
);
// Listen on 0.0.0.0 on port 8080
$server = (new \Peachtree\Websocket\Server('0.0.0.0', 8080))->socket();
$server->run();
现在使用您的本地 telnet 客户端进行连接。如果您发送“ping”消息,您将获得“pong”响应。
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> {"action":"ping","payload":{},"ref":null}
< {"ref":null,"action":"pong","payload":[]}
聊天服务器
假设我们想要创建一个简单的聊天服务,用户可以向他们所在的频道发送消息。
这样做需要构建我们自己的消息处理器。让我们写一个!
<?php
declare(strict_types=1);
require_once 'vendor/autoload.php';
use Peachtree\Websocket\Connection\State;
use Peachtree\Websocket\Handler\MessageHandler;
use Peachtree\Websocket\IO\Broadcast;
use Peachtree\Websocket\IO\Response;
use Peachtree\Websocket\Message;
use Peachtree\Websocket\MessageFactory;
use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Rules\Key;
use Respect\Validation\Rules\StringType;
use Respect\Validation\Validator;
final class ChatManager extends MessageHandler
{
/**
* @inheritDoc
*/
protected function handle(Message $message, State &$state): Generator
{
// First things first, lets validate our payload.
// We need to know what the message is, and what channel to send to.
// This library uses the Respect/Validation package
try {
(new Validator())
->addRule(new Key('channel', new StringType(), true))
->addRule(new Key('message', new StringType(), true))
->assert($message->getPayload());
} catch (NestedValidationException $e) {
// This message was invalid. Lets send them a response letting them know why.
yield new Response(
MessageFactory::make($message->getRef())->validationException(
$e->getMainMessage(),
...$e->getMessage()
)
);
return;
}
// Now that we know the message passes validation, lets broadcast the message to the channel!
yield new Broadcast($message, $message->getPayload()['channel']);
// Lets tell the sender that the message has been sent
yield new Response(
MessageFactory::make($message->getRef())->acknowledge('Your message has been sent!')
);
}
/**
* @inheritDoc
*/
public function shouldHandle(string $messageAction): bool
{
// We only care if someone is trying to "say" something
return $messageAction === 'say';
}
}
现在我们已经编写了消息处理器,让我们启动一个新的服务器并注册该处理器!
<?php
require_once 'vendor/autoload.php';
// Register both the default channel manager and our own chat manager
\Peachtree\Websocket\Routing\Router::addMessageHandler(
new \Peachtree\Websocket\Handler\ChannelManager(),
new ChatManager()
);
// Listen on 0.0.0.0 on port 8080
$server = (new \Peachtree\Websocket\Server('0.0.0.0', 8080))->socket();
$server->run();
现在让我们打开两个 telnet 窗口。在第一个窗口中,我们想要订阅一个频道。
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> {"action":"channel","payload":{"subscribe":["My Super Channel"],"unsubscribe":[]},"ref":"My Reference"}
< {"ref":"My Reference","action":"ack","payload":{"message":"Subscribed to 1 and unsubscribed from 0 channels."}}
在下一个窗口中,让我们向该频道发送一条消息。
$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
> {"action":"say","payload":{"channel":"My Super Channel","message":"Hello World!"},"ref":"foo reference"}
< {"ref":"foo reference","action":"ack","payload":{"message":"Your message has been sent!"}}
回到第一个窗口,您可以看到消息出现了!
< {"ref":"foo reference","action":"say","payload":{"channel":"My Super Channel","message":"Hello World!"}}
多消息编码
有时您的应用程序可能需要一次性发送多条消息。您可以逐条发送它们,但您也可以通过将它们打包成一个数组作为单条消息来帮助减少服务器的消息处理开销。
例如,而不是这个
> {"action":"foo","payload":{},"ref":"foo ref"}
< {"action":"ack","payload":{},"ref":"foo ref"}
> {"action":"bar","payload":{},"ref":"bar ref"}
< {"action":"ack","payload":{},"ref":"bar ref"}
您可以这样做
> [{"action":"foo","payload":{},"ref":"foo ref"},{"action":"bar","payload":{},"ref":"bar ref"}]
< {"action":"ack","payload":{},"ref":"foo ref"}
< {"action":"ack","payload":{},"ref":"bar ref"}
应用程序中间件
中间件是一个方便的工具,可以在同一上下文中交互消息输入和消息输出。对于每个消息输入,都会运行中间件,无论是否需要响应。
要创建一个中间件类,只需在您的类中实现 \Peachtree\Websocket\Middleware\Middleware
接口,并将其注册到应用程序路由器中,如下所示
/** @var \Peachtree\Websocket\Middleware\Middleware $myMiddleware */
Peachtree\Websocket\Routing\Router::addMiddleware($myMiddleware);
中间件需要一个 __invoke()
方法。如果您只想在响应发送给客户端之前或之后运行代码,可以使用此方法
use Peachtree\Websocket\Connection\State;
use Peachtree\Websocket\Message;
use Peachtree\Websocket\Middleware\Middleware;
class MyMiddleware implements Middleware
{
public function __invoke(Generator $responses, Message $input, State &$state): Generator {
// Before $input is parsed
foo();
// parse the $input and send $responses back to the client
yield from $responses;
// after the messages have been sent
bar();
}
}
有关更高级的使用示例,请参阅 \Peachtree\Websocket\Middleware\Debugger
文件。这是一个内置的中间件,可以实时打印出发送和接收的消息。
错误处理
默认情况下,消息处理器会抛出异常,连接将被关闭,异常将被忽略。如果您想添加自定义记录/报告功能,您可以将回调添加到服务器路由器中。
use Exception;
use Peachtree\Websocket\Connection\State;
use Peachtree\Websocket\Routing\Router;
Router::addErrorHandler(
function (Exception $e, State $state): void {
// However you want to log/report the exception.
// Also passing the connection state to help troubleshoot.
}
);
频道管理脚注
为了限制任何给定连接可以订阅的频道,您可以提供自己的可选回调函数。这个回调函数接受两个参数,频道名称和连接状态对象。
示例
use Peachtree\Websocket\Connection\State;
use Peachtree\Websocket\Handler\ChannelManager;
use Peachtree\Websocket\Routing\Router;
Router::addMessageHandler(
new ChannelManager(
static function (string $channel, State $state): bool {
return
in_array($channel, ['channel 1', 'channel 2']) &&
$state->getMeta()->whatever;
}
)
);