
在实际的 Web 项目中消息推送是一个高频且核心的需求无论是电商订单状态变更、社交互动提醒还是系统告警通知都需要一个稳定、高效、可扩展的推送系统来支撑。PHP 作为后端开发的主流语言之一其生态中提供了多种实现推送的方案但很多开发者在构建时容易陷入“能用就行”的误区忽略了连接管理、性能瓶颈、异常处理和水平扩展等工程细节。一个健壮的推送系统不仅要能发得出消息更要保证消息不丢失、连接不断开、服务能扛压。本文将围绕 PHP 实现一个可投入生产环境使用的 WebSocket 推送系统展开。我们将从最基础的 Socket 编程概念讲起逐步过渡到使用成熟的 Workerman 框架来构建长连接服务。文章会详细解释单机模式下如何管理连接与广播消息并探讨当单机性能达到瓶颈时如何借助 Redis 的发布订阅Pub/Sub机制实现多进程或多服务器间的消息协同最终构建一个支持水平扩展的分布式推送架构。整个过程会包含环境准备、核心代码实现、关键配置参数解析、服务部署验证以及生产环境中必然会遇到的连接闪断、内存泄漏、消息堆积等问题的排查路径与解决方案。1. 理解推送系统的核心从短轮询到 WebSocket在动手写代码之前必须理清推送技术的演进脉络和选型依据。这决定了我们系统的底层通信模型和资源消耗模式。1.1 传统方案的局限短轮询与长轮询最原始的“推送”实际上是客户端不断向服务器发起请求询问是否有新消息这被称为短轮询Short Polling。它的实现简单但缺点极其明显无论服务器是否有新数据客户端都会频繁发起请求造成大量无效的 HTTP 连接开销和服务器资源浪费实时性也取决于轮询间隔。为了改进出现了长轮询Long Polling。客户端发起请求后服务器会保持连接直到有数据更新或超时才返回响应客户端收到响应后立即发起下一次请求。这减少了无效请求但每个连接在等待期间仍然占用服务器资源如 Apache/NGINX 的工作进程或线程并发能力受限于服务器的工作进程数。并且连接建立和断开的开销依然存在。这两种基于 HTTP 的方案其本质都是“客户端拉取Pull”并非真正的“服务器推送Push”。1.2 WebSocket真正的全双工通信协议WebSocket 协议在 HTTP 握手之后将连接升级为一个全双工Full-Duplex的 TCP 长连接。这意味着一旦连接建立服务器和客户端可以在任意时刻主动向对方发送数据而不需要反复建立连接。这对于需要高实时性、低延迟的推送场景是理想选择。优点真正的双向通信低延迟低开销一个连接持续复用高实时性。挑战需要服务器端有能维持大量 TCP 长连接的能力这对传统 PHP 运行模式每个请求结束后释放所有资源是颠覆性的。因此我们需要一个能常驻内存的 PHP 程序来处理连接。在 PHP 生态中直接操作 Socket 进行编程是可行的但复杂度高。更普遍的做法是使用现成的常驻内存框架例如Swoole或Workerman。它们封装了底层的 Socket、事件循环和进程管理让开发者能更专注于业务逻辑。本文选择Workerman进行演示因为它纯 PHP 实现不依赖扩展部署和调试相对更简单适合大多数环境。2. 环境准备与 Workerman 基础在开始构建推送服务前需要确保你的开发或生产环境满足基本要求并理解 Workerman 的运行模型。2.1 环境与依赖要求首先你的 PHP 环境需要支持 CLI命令行接口模式运行并且建议禁用pcntl_fork和posix_setsid等函数限制因为 Workerman 会使用它们来管理进程。可以通过以下命令快速检查环境php -v | grep -i cli # 确认是 CLI 版本 php -m | grep -E pcntl|posix # 检查相关扩展非必须但推荐 php --ri sockets # 检查 sockets 扩展Workerman 需要接下来使用 Composer 初始化项目并安装 Workermanmkdir php-push-system cd php-push-system composer init --no-interaction composer require workerman/workerman这会在项目根目录生成vendor文件夹和composer.json文件。Workerman 的核心就是一个 PHP 库通过 Composer 引入后即可在代码中直接使用。2.2 理解 Workerman 的进程模型Workerman 以多进程模式运行。默认情况下它会启动一个主进程Master和多个子进程Worker。主进程负责监控子进程子进程才是真正处理客户端连接和业务逻辑的单位。每个子进程都是一个独立的 Reactor 事件循环实例可以处理成千上万的连接。这种模型带来了几个关键特性进程隔离一个 Worker 进程崩溃不会影响其他 Worker主进程会重新拉起它。多核利用多个 Worker 进程可以绑定到不同的 CPU 核心充分利用多核性能。共享数据困难默认情况下Worker 进程间的内存是隔离的。这意味着在一个 Worker 中设置的变量其他 Worker 无法直接访问。这是设计分布式推送系统时必须解决的核心问题。3. 构建单机版 WebSocket 推送服务我们先从最简单的单机场景开始实现一个能接受连接并向所有在线客户端广播消息的服务。3.1 项目结构与入口文件创建以下目录结构php-push-system/ ├── composer.json ├── vendor/ ├── start.php # 服务启动入口 ├── Applications/ # 业务应用目录 │ └── Push/ │ ├── Events.php # 事件处理类 │ └── start_websocket.php # WebSocket 服务启动脚本 └── logs/ # 日志目录手动创建首先创建服务启动入口start.php它负责加载 Composer 的自动加载文件并启动我们的推送应用?php // start.php require_once __DIR__ . /vendor/autoload.php; // 运行 Applications/Push/ 下的服务 require_once __DIR__ . /Applications/Push/start_websocket.php;3.2 实现 WebSocket 服务与事件处理核心逻辑在Applications/Push/目录下。我们先创建事件处理类Events.php?php // Applications/Push/Events.php namespace Applications\Push; class Events { /** * 当客户端连接时触发 * param \Workerman\Connection\TcpConnection $connection */ public static function onConnect($connection) { echo New connection established, ID: {$connection-id}\n; // 可以将连接ID与用户信息绑定这里简单记录 $connection-last_heartbeat_time time(); } /** * 当客户端发送消息时触发 * param \Workerman\Connection\TcpConnection $connection * param mixed $data 客户端发送的数据 */ public static function onMessage($connection, $data) { // 更新心跳时间 $connection-last_heartbeat_time time(); // 假设客户端发送 JSON 格式消息: {type: ping, content: hello} $message json_decode($data, true); if (!$message) { $connection-send(json_encode([error Invalid JSON format])); return; } switch ($message[type] ?? ) { case ping: // 心跳回应 $connection-send(json_encode([type pong, time time()])); break; case broadcast: // 模拟管理员广播消息这里直接广播给所有连接 // 注意单机模式下只能广播给当前 Worker 进程内的连接 $broadcastMsg json_encode([ type broadcast, from system, content $message[content] ?? , time date(Y-m-d H:i:s) ]); foreach ($connection-worker-connections as $clientConn) { $clientConn-send($broadcastMsg); } break; default: $connection-send(json_encode([type echo, received $message])); } } /** * 当客户端连接关闭时触发 * param \Workerman\Connection\TcpConnection $connection */ public static function onClose($connection) { echo Connection closed, ID: {$connection-id}\n; } /** * 当客户端连接发生错误时触发 * param \Workerman\Connection\TcpConnection $connection * param int $code 错误码 * param string $msg 错误信息 */ public static function onError($connection, $code, $msg) { echo Error [{$code}] on connection {$connection-id}: {$msg}\n; } }接下来创建 WebSocket 服务启动脚本start_websocket.php?php // Applications/Push/start_websocket.php use Workerman\Worker; use Workerman\Connection\TcpConnection; require_once __DIR__ . /Events.php; // 创建一个 WebSocket 服务器监听 2346 端口 $ws_worker new Worker(websocket://0.0.0.0:2346); // 设置进程数根据 CPU 核心数调整单机测试可设为1 $ws_worker-count 4; // 设置连接回调函数 $ws_worker-onConnect [Applications\Push\Events, onConnect]; $ws_worker-onMessage [Applications\Push\Events, onMessage]; $ws_worker-onClose [Applications\Push\Events, onClose]; $ws_worker-onError [Applications\Push\Events, onError]; // 设置心跳检测每 30 秒检查一次55 秒无响应则断开 $ws_worker-onWorkerStart function($worker) { // 每 30 秒遍历一次所有连接 Timer::add(30, function() use ($worker) { $time_now time(); foreach ($worker-connections as $connection) { // 如果连接最后活跃时间在 55 秒前则认为连接已死 if (empty($connection-last_heartbeat_time) || $time_now - $connection-last_heartbeat_time 55) { echo Connection {$connection-id} timeout, closing.\n; $connection-close(); } } }); }; // 如果不是在根目录启动则运行 Worker if (!defined(GLOBAL_START)) { Worker::runAll(); }3.3 关键配置与参数解析在上面的代码中有几个关键点需要理解Worker(websocket://0.0.0.0:2346)创建一个 WebSocket 协议的工作进程绑定在所有网络接口0.0.0.0的 2346 端口。websocket://协议头告诉 Workerman 自动处理 WebSocket 握手协议。$ws_worker-count 4设置启动 4 个 Worker 子进程。这通常设置为服务器 CPU 核心数或稍多一点。每个进程独立监听同一个端口由内核负载均衡但连接和内存数据不共享。心跳检测 (Timer::add)由于网络不稳定或客户端异常退出服务器可能残留“死连接”。定时器定期检查每个连接的最后活跃时间通过onMessage或自定义心跳包更新超时则主动关闭释放资源。广播的局限性在onMessage的broadcast分支中我们遍历$connection-worker-connections。这只能广播给当前 Worker 进程内维护的连接。如果count4一个连接连到了 Worker 2那么 Worker 1、3、4 中的客户端将收不到这条广播消息。这是单机多进程架构下推送系统要解决的首要问题。3.4 启动服务与基础测试在项目根目录下运行以下命令以调试模式启动服务php start.php start你会看到类似输出Workerman[php-push-system] start in DEBUG mode ----------------------------------------------- WORKERMAN ----------------------------------------------- Workerman version:4.1.15 PHP version:8.1.2 Event-Loop:\Workerman\Events\Select ----------------------------------------------- WORKERS -------------------------------------------------- proto user worker listen processes status tcp nobody none websocket://0.0.0.0:2346 4 [OK] --------------------------------------------------------------------------------------------------------- Press CtrlC to stop. Start success.现在你可以使用任何 WebSocket 客户端进行测试。例如在浏览器控制台确保页面协议为 https 或 http且域名与服务器一致中// 前端测试代码 const ws new WebSocket(ws://你的服务器IP:2346); ws.onopen function() { console.log(Connected); // 发送一个 ping 消息 ws.send(JSON.stringify({type: ping})); }; ws.onmessage function(event) { const data JSON.parse(event.data); console.log(Received:, data); if (data.type pong) { console.log(Heartbeat received at, data.time); } }; ws.onerror function(error) { console.error(WebSocket Error:, error); }; ws.onclose function(event) { console.log(Connection closed, event.code, event.reason); };同时在服务器终端你会看到New connection established的日志。至此一个最基础的单进程内广播的 WebSocket 服务就完成了。4. 引入 Redis 实现跨进程/跨服务器消息广播单 Worker 进程内的广播无法满足实际需求。我们需要一个“中间人”来协调所有 Worker 进程甚至所有服务器节点。Redis 的发布订阅Pub/Sub模式是解决此问题的经典方案。4.1 架构设计发布-订阅模式核心思想是每个 Worker 进程在启动时都订阅Subscribe一个共同的 Redis 频道例如push_channel。当某个 Worker 需要广播消息时它不直接发送给它的连接而是将消息发布Publish到push_channel。Redis 会将这条消息推送给所有订阅了该频道的 Worker 进程。每个 Worker 进程收到 Redis 推送的消息后再遍历自己进程内的连接将消息发送出去。这样无论消息源自哪个 Worker 或哪台服务器所有在线的客户端都能收到。4.2 安装依赖与修改代码首先确保服务器安装了 Redis并且 PHP 有 Redis 扩展推荐使用phpredis或predis客户端库。我们使用 Composer 安装predis因为它更轻量且纯 PHP 实现。composer require predis/predis修改Events.php增加 Redis 客户端属性和初始化逻辑。我们创建一个新的PushServer类来整合?php // Applications/Push/PushServer.php namespace Applications\Push; use Workerman\Worker; use Workerman\Timer; use Predis\Client as RedisClient; class PushServer { /** * var RedisClient Redis 客户端实例 */ protected static $redis null; /** * Redis 配置 */ const REDIS_CONFIG [ scheme tcp, host 127.0.0.1, // Redis 服务器地址 port 6379, database 0, // password your_password, // 如果有密码 ]; /** * 广播频道名称 */ const BROADCAST_CHANNEL push_system_broadcast; /** * 初始化 Redis 连接 * return RedisClient */ public static function getRedis() { if (self::$redis null) { self::$redis new RedisClient(self::REDIS_CONFIG); } return self::$redis; } /** * 启动 Worker 时的回调 * param Worker $worker */ public static function onWorkerStart($worker) { echo Worker {$worker-id} starting...\n; // 1. 初始化 Redis 并订阅广播频道 $redis self::getRedis(); // 创建一个新的 Redis 连接用于订阅订阅会阻塞必须用独立连接 $subscriber new RedisClient(self::REDIS_CONFIG); // 在独立协程/进程中处理订阅这里简化实际生产环境需考虑连接管理 // 使用定时器模拟一个简单的订阅循环注意这不是标准做法仅作演示。生产环境应用异步客户端或 workerman/redis Timer::add(1, function() use ($subscriber, $worker) { try { // 监听频道 $pubsub $subscriber-pubSubLoop(); $pubsub-subscribe(self::BROADCAST_CHANNEL); foreach ($pubsub as $message) { if ($message-kind message) { // 收到来自其他进程/服务器的广播消息 $data json_decode($message-payload, true); if ($data $data[type] broadcast) { self::broadcastToLocalConnections($worker, $message-payload); } } } } catch (\Exception $e) { echo Redis subscribe error in Worker {$worker-id}: . $e-getMessage() . \n; } }); // 2. 启动心跳检测定时器 Timer::add(30, function() use ($worker) { $time_now time(); foreach ($worker-connections as $connection) { if (empty($connection-last_heartbeat_time) || $time_now - $connection-last_heartbeat_time 55) { echo Worker {$worker-id}: Connection {$connection-id} timeout, closing.\n; $connection-close(); } } }); } /** * 向本 Worker 进程内的所有连接广播消息 * param Worker $worker * param string $message JSON 字符串 */ public static function broadcastToLocalConnections($worker, $message) { foreach ($worker-connections as $conn) { $conn-send($message); } } /** * 向全局所有 Worker、所有服务器广播消息 * param string $message JSON 字符串 */ public static function broadcastToGlobal($message) { $redis self::getRedis(); $redis-publish(self::BROADCAST_CHANNEL, $message); } // ... 保留原有的 onConnect, onMessage, onClose, onError 方法但需修改 onMessage ... public static function onMessage($connection, $data) { $connection-last_heartbeat_time time(); $message json_decode($data, true); if (!$message) { $connection-send(json_encode([error Invalid JSON])); return; } switch ($message[type] ?? ) { case ping: $connection-send(json_encode([type pong, time time()])); break; case broadcast: // 关键修改不再本地广播而是发布到 Redis $broadcastMsg json_encode([ type broadcast, from $message[from] ?? unknown, content $message[content] ?? , time date(Y-m-d H:i:s) ]); // 发布到 Redis 频道 self::broadcastToGlobal($broadcastMsg); // 注意消息会通过 Redis 订阅循环回来再由各个 Worker 发送给其连接 break; default: $connection-send(json_encode([type echo, received $message])); } } // ... onConnect, onClose, onError 方法保持不变 ... }然后修改start_websocket.php使用新的PushServer类?php // Applications/Push/start_websocket.php use Workerman\Worker; require_once __DIR__ . /PushServer.php; $ws_worker new Worker(websocket://0.0.0.0:2346); $ws_worker-count 4; // 使用 PushServer 类中的静态方法 $ws_worker-onWorkerStart [Applications\Push\PushServer, onWorkerStart]; $ws_worker-onConnect [Applications\Push\PushServer, onConnect]; $ws_worker-onMessage [Applications\Push\PushServer, onMessage]; $ws_worker-onClose [Applications\Push\PushServer, onClose]; $ws_worker-onError [Applications\Push\PushServer, onError]; if (!defined(GLOBAL_START)) { Worker::runAll(); }4.3 验证分布式广播启动服务php start.php start。你会看到 4 个 Worker 进程启动每个都会打印Worker X starting...。连接多个客户端打开两个以上的浏览器标签页分别运行之前的前端测试代码连接到 WebSocket 服务器。测试广播在其中一个客户端发送广播消息ws.send(JSON.stringify({type: broadcast, from: user1, content: Hello, everyone!}));观察结果所有连接的客户端无论它们被分配到哪个 Worker 进程都应该收到这条广播消息。同时在服务器终端你会看到消息被发布到 Redis然后各个 Worker 收到并转发。至此一个支持跨进程广播的单机推送系统就完成了。如果要扩展到多台服务器架构几乎不变只需确保所有服务器上的 Worker 进程都连接到同一个 Redis 实例或集群并订阅相同的频道即可。5. 生产环境部署、监控与问题排查将上述代码直接用于生产环境是远远不够的。下面从部署、监控、排错和优化几个维度阐述需要关注的要点。5.1 部署与进程管理在开发环境我们使用php start.php start在前台运行。生产环境必须使用守护进程daemon模式并且需要进程管理器来保证服务异常退出后能自动重启。1. 以守护进程模式启动php start.php start -d使用-d参数后Workerman 会转入后台运行所有日志默认输出到标准输出stdout。建议重定向到日志文件php start.php start -d /path/to/your/logs/workerman.log 212. 使用进程管理器推荐 systemd 或 supervisor以 systemd 为例创建服务文件/etc/systemd/system/php-push.service[Unit] DescriptionPHP Push System (Workerman) Afternetwork.target redis.service [Service] Typesimple Userwww-data # 根据你的运行用户修改 Groupwww-data WorkingDirectory/path/to/your/php-push-system ExecStart/usr/bin/php /path/to/your/php-push-system/start.php start -d Restartalways RestartSec3 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target然后启用并启动服务sudo systemctl daemon-reload sudo systemctl enable php-push sudo systemctl start php-push sudo systemctl status php-push # 查看状态5.2 关键配置参数与调优Workerman 和系统层面有一些关键参数需要调整以支撑高并发。1. Worker 配置 (start_websocket.php中)$worker-count设置为 CPU 核心数。太多会增加进程切换开销太少无法利用多核。$worker-reloadable默认true表示收到SIGUSR1信号php start.php reload时平滑重启。生产环境建议保持开启用于代码更新。$worker-name给 Worker 起个名字方便在ps aux中识别。2. 系统层面调优文件描述符限制一个 TCP 连接占用一个文件描述符。使用ulimit -n查看当前限制。生产环境建议设置为 65535 或更高。# 临时生效 ulimit -n 65535 # 永久生效修改 /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535Linux 内核参数调整 TCP 连接相关参数例如net.core.somaxconn监听队列长度、net.ipv4.tcp_tw_reuseTIME_WAIT 端口重用等需要根据实际压力测试调整。3. Redis 连接池与异步客户端上面的示例中每个 Worker 使用独立的 Redis 连接进行订阅和发布。在高并发下这可能会成为瓶颈。生产环境应考虑使用连接池管理 Redis 连接。使用 Workerman 官方推荐的异步 Redis 客户端如workerman/redis避免阻塞 Worker 进程的事件循环。对于超大规模部署考虑使用 Redis Cluster 替代单点 Redis。5.3 常见问题排查清单当推送系统出现问题时可以按照以下清单逐项排查。问题现象可能原因检查方式解决方案客户端无法连接 WebSocket1. 防火墙/安全组未开放端口2. Workerman 服务未启动3. PHP 监听地址错误1.netstat -tlnp | grep :23462.ps aux | grep workerman3. 检查start_websocket.php中监听 IP1. 开放端口2. 启动服务3. 将0.0.0.0改为服务器内网IP或公网IP谨慎连接建立后立即断开1. 心跳检测时间设置过短2. 客户端未及时发送心跳包3. Nginx 等代理超时1. 检查onWorkerStart中的定时器间隔和超时值2. 检查客户端心跳发送逻辑3. 检查代理配置如proxy_read_timeout1. 调整心跳参数如 60秒检查120秒超时2. 确保客户端定时发送 ping3. 将代理超时时间设长广播消息部分客户端收不到1. 消息未通过 Redis 广播单 Worker 广播2. Redis 订阅连接断开3. 客户端连接到了不同的服务器但 Redis 未共用1. 检查onMessage中广播是否调用broadcastToGlobal2. 查看 Redis 日志和 Workerman 错误日志3. 确认所有服务器连接同一 Redis1. 修改代码确保广播走 Redis2. 增加 Redis 连接断线重连机制3. 统一 Redis 配置服务器内存持续增长1. 连接未正常关闭导致内存泄漏2. 消息队列堆积如果使用了队列3. PHP 变量未及时释放1. 使用memory_get_usage()监控内存2. 检查心跳检测和onClose是否正常执行3. 检查是否有全局数组无限增长1. 强化心跳和连接管理2. 定期重启 Worker利用max_request类似机制3. 审查代码避免在全局作用域缓存大量数据高并发时大量连接失败1. 系统文件描述符限制2. Worker 进程数不足3. 服务器资源CPU/内存耗尽1.ulimit -n2. 监控服务器资源使用率3. 查看 Workerman 日志是否有错误1. 提高系统文件描述符限制2. 适当增加$worker-count不超过 CPU 核数*23. 扩容服务器或优化代码/数据结构5.4 日志与监控没有日志的系统如同盲人摸象。除了 Workerman 自带的输出应该将关键事件记录到文件或日志系统。1. 集成 Monolog推荐composer require monolog/monolog在PushServer.php的onWorkerStart中初始化日志use Monolog\Logger; use Monolog\Handler\StreamHandler; $log new Logger(push_system); $log-pushHandler(new StreamHandler(/path/to/logs/push.log, Logger::INFO)); // 然后使用 $log-info(), $log-error() 记录日志2. 关键日志点连接建立/关闭记录连接ID和来源IP。收到/发送特定类型的消息如广播。Redis 发布/订阅操作。心跳检测触发的连接清理。任何异常和错误。3. 系统监控进程存活通过 systemd 或 supervisor 监控。连接数可以通过 Workerman 的$worker-connections数量粗略估算或通过netstat命令。服务器资源CPU、内存、网络 IO。Redis 状态内存使用、连接数、命令延迟。6. 扩展方向与最佳实践基础推送系统搭建完成后可以根据业务需求向以下几个方向深化。6.1 用户-连接映射与私信推送目前系统只有广播实际业务需要点对点推送。这需要在服务端维护一个“用户ID”到“连接对象”的映射关系。由于连接对象无法跨进程序列化这个映射关系必须存储在共享存储中如 Redis。思路客户端连接后发送一个认证消息包含其用户唯一标识如user_id。服务端在onMessage中处理认证将user_id与当前连接的$connection-id关联起来存储到 Redis 的 Hash 或 Sorted Set 中Key 可以设计为user_conn:{user_id}Value 为worker_id:connection_id。当需要向特定用户推送时从 Redis 中查出该用户所在的 Worker 和 Connection ID然后通过 Redis 发布一个“私信”频道消息目标 Worker 收到后找到对应的连接并发送。在onClose中需要清理 Redis 中的映射关系。这是一个典型的有状态连接管理问题设计时需要仔细考虑并发更新和过期清理。6.2 消息持久化与可靠性保证当前的系统是“发后即忘”的。如果客户端临时断线重连会错过离线期间的消息。对于订单状态等关键消息需要引入消息持久化。方案存储离线消息当发布一条针对特定用户的消息时如果检测到该用户不在线Redis 中无映射则将消息存入持久化队列如 Redis List 或 MySQL。用户上线后拉取用户重连并认证后服务端从持久化队列中取出该用户的未读消息逐一推送。消息确认机制客户端收到消息后发送一个 ACK 回执服务端才从队列中删除该消息防止消息丢失。6.3 协议优化与安全加固协议压缩对于频繁的聊天或实时数据可以考虑对 WebSocket 传输的数据进行压缩如permessage-deflate扩展。WSS (WebSocket Secure)生产环境务必使用wss://即 WebSocket over TLS。这可以通过在 Workerman 前配置 Nginx 反向代理并启用 SSL或者使用 Workerman 的 SSL 上下文配置来实现。连接认证不应允许任意客户端连接。可以在onConnect或首次onMessage时进行 Token 验证无效则立即断开。频率限制防止恶意客户端发送大量消息耗尽资源。可以在onMessage中针对连接 ID 或用户 ID 进行限流。6.4 从 Workerman 到 Swoole如果你追求极致的性能并且环境允许安装 PHP 扩展可以考虑将底层框架从 Workerman 迁移到Swoole。Swoole 作为 C 扩展在性能上有显著优势特别是其协程特性可以更高效地处理大量并发 I/O。但 Swoole 的学习曲线和调试复杂度也更高。迁移并非简单替换类名涉及到底层事件循环、进程模型和协程编程范式的转变需要充分评估和测试。构建一个生产级的 PHP 推送系统技术选型只是起点真正的挑战在于对连接生命周期、状态同步、资源管理和异常处理的精细把控。从单机 Worker 内广播到基于 Redis 的跨进程协同这套架构提供了一个清晰可扩展的基线。后续无论是引入用户会话管理、增加消息持久化还是整合更复杂的微服务核心思想都是将“连接”与“业务逻辑”解耦通过中间件如 Redis进行状态同步和消息路由。在落地时务必结合业务体量从小规模验证开始逐步完善监控、告警和容灾机制让推送服务成为业务中可靠的基础设施而非脆弱的短板。