☰
Hyperf WebSocket Server 实战指南:从零搭建高性能 WebSocket 服务
2026/10/8 7:47:32 网站建设 项目流程
  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

导读

Hyperf 对 Swoole 的 WebSocket Server 进行了完整封装,基于 hyperf/websocket-server 组件即可快速构建 WebSocket 应用,并与 Hyperf 的依赖注入、路由、中间件、协程上下文等能力无缝集成。本文将以官方文档 WebSocket server 为核心脉络,完整讲解从安装、服务配置、路由与中间件绑定,到控制器编写、连接上下文、跨 Worker 主动推送以及"WebSocket 中处理 HTTP 请求"等进阶玩法的全过程,并结合仓库源码剖析握手校验、消息分发、fd 收集器等底层实现,帮助你写出可直接上线的 WebSocket 服务。

安装组件

WebSocket Server 是 Hyperf 的独立组件,通过 Composer 安装:

composer require hyperf/websocket-server

从组件 composer.json 可以看到,该组件要求 PHP>= 8.2,依赖hyperf/contract、hyperf/http-server、hyperf/context、hyperf/exception-handler、hyperf/coordinator等 Hyperf 核心组件,并遵循 PSR-4 规范将Hyperf\WebSocketServer\命名空间映射到src/目录。组件通过 ConfigProvider.php 自动注册了两个监听器:Listener\InitSenderListener(初始化 Sender 的 WorkerId)与Listener\OnPipeMessageListener(处理跨 Worker 的管道消息),安装后无需额外手动注册即可生效。

配置 WebSocket Server

在config/autoload/server.php中新增一个servers配置项,即可声明一个 WebSocket 服务:

<?php return [ 'servers' => [ [ 'name' => 'ws', 'type' => Server::SERVER_WEBSOCKET, 'host' => '0.0.0.0', 'port' => 9502, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'], Event::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'], Event::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'], ], ], ], ];

各配置项含义如下:

配置项说明
name服务名称,路由与中间件配置都会以它为 key 关联到对应服务
type服务类型,WebSocket 使用Server::SERVER_WEBSOCKET
host/port监听地址与端口,示例中 WebSocket 服务监听9502
sock_typeSocket 类型,SWOOLE_SOCK_TCP表示 TCP 协议
callbacks三个关键事件的回调:onHandShake(握手)、onMessage(消息)、onClose(关闭)

这三个回调全部指向Hyperf\WebSocketServer\Server类的对应方法。从源码 Server.php 可见,Server实现了OnHandShakeInterface、OnCloseInterface、OnMessageInterface,并通过initCoreMiddleware()读取middlewares.{serverName}与exceptions.handler.{serverName}配置,默认异常处理器为WebSocketExceptionHandler。

配置路由

目前 WebSocket 服务仅支持配置文件方式定义路由,注解方式即将支持。

在config/routes.php中,使用Router::addServer()将路由注册到名为ws的服务上(ws即config/autoload/server.php中 WebSocket Server 的name):

<?php Router::addServer('ws', function () { Router::get('/', 'App\Controller\WebSocketController'); });

与 HTTP 路由不同,WebSocket 路由最终指向的是一个控制器类而非具体方法。底层 CoreMiddleware.php 在handleFound()中会通过prepareHandler()解析出控制器类,将其作为class属性写入响应对象;如果路由不存在或容器中无对应控制器,会抛出WebSocketHandShakeException。握手成功后,该控制器类会被记录进 fd 收集器(见下文),后续的onMessage、onClose事件都会实例化同一个控制器来分发。

配置中间件

WebSocket 服务同样支持中间件,在config/autoload/middlewares.php中以服务名ws为 key 配置:

<?php return [ 'ws' => [ yourMiddleware::class ], ];

这些中间件会在握手阶段(onHandShake)被执行。Server.php 的握手流程中,coreMiddleware->dispatch()完成路由分发后,会将全局中间件($this->middlewares)与通过MiddlewareManager::get()获取的路由级中间件合并,再交给HttpDispatcher统一调度——因此 WebSocket 的鉴权、限流等中间件逻辑与 HTTP 服务共用同一套中间件机制,用法完全一致。

创建控制器

创建一个同时实现OnMessageInterface、OnOpenInterface、OnCloseInterface的控制器:

<?php declare(strict_types=1); namespace App\Controller; use Hyperf\Contract\OnCloseInterface; use Hyperf\Contract\OnMessageInterface; use Hyperf\Contract\OnOpenInterface; use Swoole\Http\Request; use Swoole\Server; use Swoole\Websocket\Frame; use Swoole\WebSocket\Server as WebSocketServer; class WebSocketController implements OnMessageInterface, OnOpenInterface, OnCloseInterface { public function onMessage($server, Frame $frame): void { $server->push($frame->fd, 'Recv: ' . $frame->data); } public function onClose($server, int $fd, int $reactorId): void { var_dump('closed'); } public function onOpen($server, Request $request): void { $server->push($request->fd, 'Opened'); } }

三个回调接口分别对应 WebSocket 生命周期的三个阶段:

  • onOpen:握手成功、连接建立后触发,通常在此做初始化(如记录在线用户);
  • onMessage:收到客户端消息时触发,$frame->data为消息内容,$frame->fd为连接句柄;
  • onClose:连接关闭时触发。

在非协程风格(异步风格)的 Swoole Server 下,Server.php 会在握手成功后通过defer()延迟执行onOpen;在协程风格服务(CoroutineServer、SwowServer)下则通过wait()包裹,并在子协程中预先设置好Context::FD,保证onOpen也能正确读取连接上下文。另外,onMessage 与 onClose 都会先从FdCollector中取出握手阶段记录的控制器类再实例化调用,若 fd 不存在会直接返回并记录 warning 日志。

启动服务

执行启动命令,即可看到 WebSocket Server 成功监听 9502 端口:

$ php bin/hyperf.php start [INFO] Worker#0 started. [INFO] WebSocket Server listening at 0.0.0.0:9502 [INFO] HTTP Server listening at 0.0.0.0:9501

!> 当 HTTP Server(9501)与 WebSocket Server(9502)同时监听时,WebSocket 客户端通过两个端口都可以连接,即连接ws://0.0.0.0:9501与ws://0.0.0.0:9502均有效。

原因是Swoole\WebSocket\Server继承自Swoole\Http\Server,天然兼容 HTTP 协议,因此可以用 HTTP 方式完成所有 WebSocket 推送。如果希望 HTTP 服务不再受理 WebSocket 协议升级,可以在config/autoload/server.php中给http服务加上open_websocket_protocol配置并设为false:

<?php return [ // 无关配置已省略 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9501, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], 'settings' => [ 'open_websocket_protocol' => false, ] ], ] ];

源码视角:握手是如何完成的

关于握手,Security.php 封装了完整的协议校验逻辑:isInvalidSecurityKey()用正则#^[+/0-9A-Za-z]{21}[AQgw]==$#校验客户端传来的sec-websocket-key并验证其 base64 解码后长度为 16 字节;handshakeHeaders()则会生成Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Accept(通过sha1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11)签名)等响应头。CoreMiddleware 将响应状态设置为101,即完成了标准的 WebSocket 协议升级。

连接上下文(Connected Context)

WebSocket 的onOpen、onMessage、onClose回调不会在同一个协程中触发,因此它们之间无法直接使用协程上下文存储的数据。为此,WebSocket Server 组件提供了Connected Context(连接上下文),其 API 与协程上下文一致,但数据以fd为维度隔离,天然解决"每个连接各存一份数据"的需求。

<?php declare(strict_types=1); namespace App\Controller; use Hyperf\Contract\OnMessageInterface; use Hyperf\Contract\OnOpenInterface; use Hyperf\WebSocketServer\Context; use Swoole\Http\Request; use Swoole\Websocket\Frame; use Swoole\WebSocket\Server as WebSocketServer; class WebSocketController implements OnMessageInterface, OnOpenInterface { public function onMessage($server, Frame $frame): void { $server->push($frame->fd, 'Username: ' . Context::get('username')); } public function onOpen($server, Request $request): void { Context::set('username', $request->cookie['username']); } }

从源码 Context.php 可以看到其实现原理:set()内部通过CoContext::get(Context::FD)取得当前连接的 fd,再将数据存入"{fd}.{key}"形式的键中,get()/has()也按相同规则读取,从而实现按连接隔离的存储。该上下文还提供destroy()、release()(连接关闭时清理)、copy()(将某个连接的上下文复制到当前连接)、override()、getOrSet()等能力。在 Server.php 的onClose中,通过defer()延迟执行FdCollector::del($fd)与Context::release($fd),确保连接关闭后相关数据被及时回收。

多 WebSocket Server 配置(Nginx 负载均衡)

当需要部署多个 WebSocket Server 实例时,可以通过 Nginx 的upstream做反向代理与负载均衡。官方示例配置如下:

# /etc/nginx/conf.d/ng_socketio.conf # multiple ws server upstream io_nodes { server ws1:9502; server ws2:9502; } server { listen 9502; # server_name your.socket.io; location / { proxy_set_header Upgrade "websocket"; proxy_set_header Connection "upgrade"; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header Host $host; # proxy_http_version 1.1; # Forward to multiple ws server proxy_pass http://io_nodes; } }

注意 Nginx 代理 WebSocket 的关键在于必须设置Upgrade与Connection两个请求头(如上所示),否则协议升级无法完成。多个ws节点通过upstream统一对外,客户端只需连接 Nginx 的 9502 端口。

Sender:跨 Worker 主动推送与断开

WebSocket 连接归属于具体的 Worker 进程,当你想在HTTP 服务中主动推送消息或断开连接时,直接调用 Swoole 原生的push()是行不通的(目标 fd 可能不在当前 Worker 内)。此时应使用组件提供的Hyperf\WebSocketServer\Sender。

Sender的工作机制:先检查fd是否归属于当前 Worker,若属于则直接发送;否则通过PipeMessage将消息广播给其他所有 Worker,由持有该 fd 的 Worker 完成实际发送。Sender.php 的__call()正是这一逻辑的入口——proxy()直接发送失败后,会调用sendPipeMessage()向其余每个 Worker 发送SenderPipeMessage;另一端由 OnPipeMessageListener 监听OnPipeMessage事件并再次调用proxy()完成投递。

Sender支持两个方法:push(推送数据)与disconnect(断开连接)。典型用法如下:

<?php declare(strict_types=1); namespace App\Controller; use Hyperf\Di\Annotation\Inject; use Hyperf\HttpServer\Annotation\AutoController; use Hyperf\WebSocketServer\Sender; use function Hyperf\Coroutine\go; #[AutoController] class ServerController { #[Inject] protected Sender $sender; public function close(int $fd) { go(function () use ($fd) { sleep(1); $this->sender->disconnect($fd); }); return ''; } public function send(int $fd) { $this->sender->push($fd, 'Hello World.'); return ''; } }

除此之外,Sender还提供pushFrame()用于推送FrameInterface数据帧(可指定 opcode 与 finish 标志),以及check($fd)方法(通过connection_info()判断该连接是否处于WEBSOCKET_STATUS_ACTIVE状态)判断连接是否仍然活跃。需要留意的是,在协程风格服务(server.type为CoroutineServer或SwowServer)下,Sender直接基于保存在responses中的连接对象发送,无需跨进程通信。InitSenderListener会在 Worker 启动时调用setWorkerId()记录当前 WorkerId,这是判断"fd 是否归属当前 Worker"的基础。

在 WebSocket Server 中处理 HTTP 请求

除了通过端口分离 HTTP 与 WebSocket 服务外,还可以让 WebSocket 服务同时处理 HTTP 请求。由于server.servers.*.callbacks中的配置项都是单例,需要先在config/autoload/dependencies.php中声明一个新的单例:

<?php return [ 'HttpServer' => Hyperf\HttpServer\Server::class, ];

然后修改 WebSocket 服务的callbacks配置,在原有三个回调基础上追加Event::ON_REQUEST(以下省略无关配置):

<?php declare(strict_types=1); use Hyperf\Server\Event; use Hyperf\Server\Server; return [ 'mode' => SWOOLE_BASE, 'servers' => [ [ 'name' => 'ws', 'type' => Server::SERVER_WEBSOCKET, 'host' => '0.0.0.0', 'port' => 9502, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => ['HttpServer', 'onRequest'], Event::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'], Event::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'], Event::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'], ], ], ], ];

配置完成后,即可在ws服务中直接添加 HTTP 路由。这种模式适用于"同一端口同时提供 REST API 与 WebSocket 长连接"的场景,例如聊天室的鉴权接口与实时消息通道共用 9502 端口。由于Swoole\WebSocket\Server本身继承自Swoole\Http\Server,这在协议层面是完全支持的。

总结

至此,你已经完整掌握了 Hyperf 中 WebSocket Server 的核心用法:

  • 服务声明:在config/autoload/server.php中配置SERVER_WEBSOCKET类型服务并绑定onHandShake/onMessage/onClose回调;
  • 路由与中间件:通过Router::addServer('ws', ...)注册控制器路由,通过middlewares.php的ws键绑定中间件,握手阶段即完成路由分发与中间件调度;
  • 生命周期控制:控制器实现OnOpenInterface/OnMessageInterface/OnCloseInterface三接口,配合Connected Context按连接隔离数据;
  • 主动推送:利用Sender在 HTTP 层跨 Worker 完成push/disconnect,底层通过 PipeMessage 实现进程间协作;
  • 扩展玩法:通过open_websocket_protocol关闭 HTTP 端的 WebSocket 升级,通过追加ON_REQUEST回调让 WebSocket 服务同时承载 HTTP 请求。

如需进一步验证各环节行为,可以查阅组件的单元测试(ServerTest.php、ContextTest.php、SenderTest.php),并结合 websocket-client 文档 编写客户端完成端到端联调。

  • 后端
  • 微服务

【免费下载链接】hyperf

🚀 A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.

项目地址:https://gitcode.com/gh_mirrors/hy/hyperf
点击查看免费下载

相关推荐

上一篇:OpenChamber 隔离空间 Dispatcher 识别机制:为何选择客户端前缀寻址而非服务端嗅探
下一篇:Chronos-2-Synth vs 传统模型:为什么合成数据训练的时间序列模型更强大?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询