【基于 Swoole+Hyperf 的微服务实战】第一周·周六:综合实战与知识串联
2026/8/23 8:49:11 网站建设 项目流程

为了巩固前五天所学的协程、Channel、上下文与 Hyperf 基础,我为你设计了一个可选的周六综合实战日。今天不引入全新理论,而是通过一个完整的项目——“协程 TCP 聊天室 + HTTP 监控面板”,把本周所有知识点串联起来。这种“做中学”的方式,能让你对协程的威力和框架的便利性产生肌肉记忆。


第一周 周六:综合实战与知识串联(可选)

今日目标
  1. 使用 Swoole 原生构建一个多人在线 TCP 聊天室,支持用户上线、广播消息和私聊。
  2. 利用Channel实现用户间消息广播,利用WaitGroup安全关闭服务器。
  3. 利用协程上下文Context传递用户昵称和房间信息。
  4. 额外添加一个 HTTP 监控接口,可查看在线人数和最近消息记录。
  5. 将所有代码组织清晰,并能在 Docker 环境中运行、测试。

注:虽然我们才刚在周四接触 Hyperf,今天为了聚焦协程本身,仍先用纯 Swoole 实现核心聊天逻辑。你会惊讶地发现,用下周即将学习的依赖注入和注解,可以把这个项目进一步改造成一个更优雅的微服务。


一、环境准备(约 15 分钟)

在之前使用的swoole-course目录下新建一个周六项目文件夹:

cdswoole-coursemkdirday6-chat&&cdday6-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;$server=newServer('0.0.0.0',9501,SWOOLE_PROCESS);$server->set(['worker_num'=>1,// 单 Worker 方便演示'enable_coroutine'=>true,]);// 全局广播通道(容量 256)$broadcastChan=newChannel(256);// 在线用户表:[ fd => ['nick' => 'xxx', 'ctx' => Context] ]$onlineUsers=[];// 历史消息队列(保留最近 20 条)$historyMessages=[];// WaitGroup 用于关闭时的优雅退出$shutdownWg=newWaitGroup();$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($msg==='SHUTDOWN')break;// 关闭信号// $msg 结构: ['type' => 'broadcast'|'private', 'from' => fd, 'content' => '', 'to' => fd|0]switch($msg['type']){case'broadcast':$payload=json_encode(['type'=>'broadcast','from'=>$msg['from'],'nick'=>$onlineUsers[$msg['from']]['nick']??'Unknown','content'=>$msg['content'],'time'=>date('H:i:s'),]);// 存储历史$historyMessages[]=$payload;if(count($historyMessages)>20){array_shift($historyMessages);}// 广播给所有人foreach($onlineUsersas$fd=>$info){$server->send($fd,$payload."\n");}break;case'private':$toFd=$msg['to'];if(isset($onlineUsers[$toFd])){$payload=json_encode(['type'=>'private','from'=>$msg['from'],'nick'=>$onlineUsers[$msg['from']]['nick']??'Unknown','content'=>$msg['content'],'time'=>date('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){$http=newSwoole\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_count'=>count($onlineUsers),'users'=>array_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]=['nick'=>"User{$fd}",'ctx'=>Context::get()];echo"客户端连接: fd={$fd}\n";});$server->on('Receive',function(Server$server,int$fd,int$reactorId,string$data)use($broadcastChan,&$onlineUsers){$data=trim($data);if(empty($data))return;// 命令解析if(str_starts_with($data,'/nick ')){$nick=trim(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 昵称 内容$parts=explode(' ',$data,3);if(count($parts)<3){$server->send($fd,"系统:格式错误,应为 /msg 昵称 内容\n");return;}$targetNick=$parts[1];$content=$parts[2];$targetFd=null;foreach($onlineUsersas$fdId=>$info){if($info['nick']===$targetNick){$targetFd=$fdId;break;}}if($targetFd){$broadcastChan->push(['type'=>'private','from'=>$fd,'to'=>$targetFd,'content'=>$content,]);}else{$server->send($fd,"系统:用户{$targetNick}不在线\n");}}else{// 普通消息,广播$broadcastChan->push(['type'=>'broadcast','from'=>$fd,'content'=>$data,]);}});$server->on('Close',function(Server$server,int$fd)use(&$onlineUsers,$broadcastChan){// 通知其他用户该人下线$nick=$onlineUsers[$fd]['nick']??"User{$fd}";$broadcastChan->push(['type'=>'broadcast','from'=>0,// 系统'content'=>"{$nick}离开了聊天室",]);unset($onlineUsers[$fd]);echo"客户端断开: fd={$fd}\n";});// 关闭服务器时的清理$server->on('WorkerStop',function()use($broadcastChan,$shutdownWg){$broadcastChan->push('SHUTDOWN');$shutdownWg->wait();// 等待广播协程结束echo"Worker 安全退出\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 替代 Telnet?Swoole 支持 WebSocket 服务器,可以在 HTTP 基础上升级协议,Hyperf 也提供了hyperf/websocket-server组件,届时聊天室将变成网页版,体验更好。
4. 自我挑战任务
  • 增加房间功能:实现/join 房间名,消息只发给同房间的人。提示:可以用二维数组或 Channel 分组。
  • 敏感词过滤:在广播消息入库前,用协程调用一个外部过滤 API(可用file_get_contents协程化),将敏感词替换为***
  • 持久化:将历史消息异步写入文件或 MySQL,使用godefer保证文件句柄关闭。

五、今日作业与产出

  1. 提交代码:将chat_server.php和相关注释提交到 Git。
  2. 学习笔记:画出聊天室的消息流向图,标出 Channel、Context、WaitGroup 各自扮演的角色。
  3. 总结思考:写一段话总结本周从协程基础到完整聊天室的收获,重点反思“为什么单进程能支撑成百上千的并发连接”。
  4. 预习准备:周末简单浏览 Hyperf 官方文档的“依赖注入”章节,为周一正式学习 DI 和 AOP 做准备。

这个周六的综合项目,让你真正感受到了 Swoole 协程在生产环境下的巨大潜力。下周开始,我们将用 Hyperf 框架的优雅封装来重新审视这些底层能力,写出更健壮、更可维护的微服务代码。

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

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

立即咨询