【基于 Swoole+Hyperf 的微服务实战】第一周·周六:综合实战与知识串联

📅 2026/8/23 8:49:15
【基于 Swoole+Hyperf 的微服务实战】第一周·周六:综合实战与知识串联
为了巩固前五天所学的协程、Channel、上下文与 Hyperf 基础我为你设计了一个可选的周六综合实战日。今天不引入全新理论而是通过一个完整的项目——“协程 TCP 聊天室 HTTP 监控面板”把本周所有知识点串联起来。这种“做中学”的方式能让你对协程的威力和框架的便利性产生肌肉记忆。第一周 周六综合实战与知识串联可选今日目标使用 Swoole 原生构建一个多人在线 TCP 聊天室支持用户上线、广播消息和私聊。利用Channel实现用户间消息广播利用WaitGroup安全关闭服务器。利用协程上下文Context传递用户昵称和房间信息。额外添加一个 HTTP 监控接口可查看在线人数和最近消息记录。将所有代码组织清晰并能在 Docker 环境中运行、测试。注虽然我们才刚在周四接触 Hyperf今天为了聚焦协程本身仍先用纯 Swoole 实现核心聊天逻辑。你会惊讶地发现用下周即将学习的依赖注入和注解可以把这个项目进一步改造成一个更优雅的微服务。一、环境准备约 15 分钟在之前使用的swoole-course目录下新建一个周六项目文件夹cdswoole-coursemkdirday6-chatcdday6-chat进入容器docker-composeexecswoolebashcd/var/www/day6-chat二、核心知识点回顾约 30 分钟开始前我们快速在脑海中过一遍今天会用到的工具TCP 服务器$server new Swoole\Server(0.0.0.0, 9501)回调onConnect,onReceive,onClose。Channel用作广播通道所有连接都可以向同一个 Channel 推送消息专门的广播协程负责消费。协程上下文Context::set(nickname, $name)在接收消息时可以拿到发送者的信息。WaitGroup用于在服务器关闭时等待所有广播协程处理完剩余消息后再退出。HTTP 服务器我们将在同一个进程中开启一个 HTTP 服务监听 9502提供监控接口注意协程化。由于 Swoole Server 是单进程多协程的我们可以直接在onWorkerStart中启动 HTTP 服务和广播协程。三、实战构建聊天室与监控面板约 3 小时我们将分步编写chat_server.php最终效果如下用户通过telnet连接后输入/nick 昵称设置昵称然后发言。消息会被广播给所有在线用户包括自己。支持/msg 昵称 内容发送私聊。通过浏览器访问http://localhost:9502/stats查看在线人数和最近 20 条消息。步骤 1创建基本 TCP 服务器初始化广播通道?php// chat_server.phpuseSwoole\Server;useSwoole\Coroutine\Channel;useSwoole\Coroutine\Context;useSwoole\Coroutine\WaitGroup;useSwoole\Http\Response;useSwoole\Http\Request;$servernewServer(0.0.0.0,9501,SWOOLE_PROCESS);$server-set([worker_num1,// 单 Worker 方便演示enable_coroutinetrue,]);// 全局广播通道容量 256$broadcastChannewChannel(256);// 在线用户表[ fd [nick xxx, ctx Context] ]$onlineUsers[];// 历史消息队列保留最近 20 条$historyMessages[];// WaitGroup 用于关闭时的优雅退出$shutdownWgnewWaitGroup();$server-on(WorkerStart,function(Server$server,int$workerId)use($broadcastChan,$onlineUsers,$historyMessages,$shutdownWg){// 启动广播协程不断从 Channel 消费消息并分发给所有连接go(function()use($server,$broadcastChan,$onlineUsers,$historyMessages,$shutdownWg){while($msg$broadcastChan-pop()){if($msgSHUTDOWN)break;// 关闭信号// $msg 结构: [type broadcast|private, from fd, content , to fd|0]switch($msg[type]){casebroadcast:$payloadjson_encode([typebroadcast,from$msg[from],nick$onlineUsers[$msg[from]][nick]??Unknown,content$msg[content],timedate(H:i:s),]);// 存储历史$historyMessages[]$payload;if(count($historyMessages)20){array_shift($historyMessages);}// 广播给所有人foreach($onlineUsersas$fd$info){$server-send($fd,$payload.\n);}break;caseprivate:$toFd$msg[to];if(isset($onlineUsers[$toFd])){$payloadjson_encode([typeprivate,from$msg[from],nick$onlineUsers[$msg[from]][nick]??Unknown,content$msg[content],timedate(H:i:s),]);$server-send($toFd,$payload.\n);// 也发给发送者一份回显$server-send($msg[from],$payload.\n);}else{$server-send($msg[from],系统用户不在线\n);}break;}}echo广播协程退出\n;$shutdownWg-done();});$shutdownWg-add();// 广播协程计入等待组// 启动一个协程 HTTP 服务器监听 9502go(function()use($onlineUsers,$historyMessages){$httpnewSwoole\Http\Server(0.0.0.0,9502);$http-on(Request,function(Request$req,Response$res)use($onlineUsers,$historyMessages){$path$req-server[request_uri];if($path/stats){$res-header(Content-Type,application/json);$res-end(json_encode([online_countcount($onlineUsers),usersarray_map(fn($u)$u[nick],$onlineUsers),recent_messages$historyMessages,],JSON_UNESCAPED_UNICODE));}else{$res-status(404);$res-end(Not Found);}});$http-start();});});$server-on(Connect,function(Server$server,int$fd)use($onlineUsers){$onlineUsers[$fd][nickUser{$fd},ctxContext::get()];echo客户端连接: fd{$fd}\n;});$server-on(Receive,function(Server$server,int$fd,int$reactorId,string$data)use($broadcastChan,$onlineUsers){$datatrim($data);if(empty($data))return;// 命令解析if(str_starts_with($data,/nick )){$nicktrim(substr($data,6));if($nick){$onlineUsers[$fd][nick]$nick;Context::set(nick,$nick);// 存入当前协程上下文这里上下文作用域有限仅演示$server-send($fd,系统昵称已设置为{$nick}\n);}}elseif(str_starts_with($data,/msg )){// 格式: /msg 昵称 内容$partsexplode( ,$data,3);if(count($parts)3){$server-send($fd,系统格式错误应为 /msg 昵称 内容\n);return;}$targetNick$parts[1];$content$parts[2];$targetFdnull;foreach($onlineUsersas$fdId$info){if($info[nick]$targetNick){$targetFd$fdId;break;}}if($targetFd){$broadcastChan-push([typeprivate,from$fd,to$targetFd,content$content,]);}else{$server-send($fd,系统用户{$targetNick}不在线\n);}}else{// 普通消息广播$broadcastChan-push([typebroadcast,from$fd,content$data,]);}});$server-on(Close,function(Server$server,int$fd)use($onlineUsers,$broadcastChan){// 通知其他用户该人下线$nick$onlineUsers[$fd][nick]??User{$fd};$broadcastChan-push([typebroadcast,from0,// 系统content{$nick}离开了聊天室,]);unset($onlineUsers[$fd]);echo客户端断开: fd{$fd}\n;});// 关闭服务器时的清理$server-on(WorkerStop,function()use($broadcastChan,$shutdownWg){$broadcastChan-push(SHUTDOWN);$shutdownWg-wait();// 等待广播协程结束echoWorker 安全退出\n;});$server-start();步骤 2启动并测试聊天功能在容器内运行php chat_server.php然后打开三个终端窗口均使用telnet localhost 9501连接终端 1 输入/nick Alice然后输入Hello everyone!终端 2 输入/nick Bob应该能收到 Alice 的消息。终端 3 输入/nick Charlie然后 Bob 发一条/msg Alice 你好私聊只有 Alice 和 Bob 能看到。观察服务器终端日志并同时浏览器访问http://localhost:9502/stats查看在线用户列表和历史消息。四、成果测试与观察约 1 小时1. 功能测试清单昵称设置/nick 新昵称服务器返回确认。广播消息任意一个客户端发言所有在线客户端都能收到并且历史消息被记录。私聊消息/msg 目标昵称 内容只有目标和自己收到若目标不存在则提示。上线/下线通知新用户连接时虽然没有主动通知但可改进下线时系统广播离开消息。监控面板浏览器访问http://localhost:9502/stats在线人数随连接数变化历史消息实时更新。2. 协程安全性验证并发连接快速打开 10 个telnet连接并同时发送消息所有消息都能正常广播没有协程冲突导致的 PHP 警告。通道背压由于broadcastChan容量为 256如果广播协程处理速度跟不上生产者push会自动挂起等待不会丢失消息。可以试着用脚本快速发送数百条消息看看服务器是否卡顿或报错。3. 进阶思考如何将聊天服务集成到 Hyperf 中下周学完依赖注入后你可以把广播通道定义为一个单例 Bean把用户管理抽象成一个UserManager服务通过注解注入到 WebSocket 或 TCP 控制器中立刻变得高内聚低耦合。如何用 WebSocket 替代 TelnetSwoole 支持 WebSocket 服务器可以在 HTTP 基础上升级协议Hyperf 也提供了hyperf/websocket-server组件届时聊天室将变成网页版体验更好。4. 自我挑战任务增加房间功能实现/join 房间名消息只发给同房间的人。提示可以用二维数组或 Channel 分组。敏感词过滤在广播消息入库前用协程调用一个外部过滤 API可用file_get_contents协程化将敏感词替换为***。持久化将历史消息异步写入文件或 MySQL使用go加defer保证文件句柄关闭。五、今日作业与产出提交代码将chat_server.php和相关注释提交到 Git。学习笔记画出聊天室的消息流向图标出 Channel、Context、WaitGroup 各自扮演的角色。总结思考写一段话总结本周从协程基础到完整聊天室的收获重点反思“为什么单进程能支撑成百上千的并发连接”。预习准备周末简单浏览 Hyperf 官方文档的“依赖注入”章节为周一正式学习 DI 和 AOP 做准备。这个周六的综合项目让你真正感受到了 Swoole 协程在生产环境下的巨大潜力。下周开始我们将用 Hyperf 框架的优雅封装来重新审视这些底层能力写出更健壮、更可维护的微服务代码。