简介:webman-manual 是一份面向 PHP 后端开发者的 Webman 框架中文手册源码包,尤其适合从传统 PHP-FPM 架构转向高性能常驻内存框架的团队和个人。Webman 基于 Workerman 构建,主打高稳定性、超高性能与高复用,可用于开发网站、HTTP 接口、微服务,也可借助自定义进程处理 WebSocket、物联网、游戏、TCP/UDP 等实时通信场景。手册从安装配置、路由、控制器、中间件、请求响应、验证码、视图模板开始,逐步深入到数据库模型、迁移、Redis、Mongo、队列、定时任务、缓存、日志,以及支付、微信、Excel 等集成模块,并配有性能压测对比图,便于读者理解框架各项特性。压缩包共 57 个文件、约 404KB,以 47 个 Markdown 文档为主,辅以 PNG/JPG 图片和 docsify 文档站运行所需的 CSS/JS/HTML 文件,离线即可按目录浏览,轻量方便。目前已有 748 人学习下载,可作为从入门到进阶的案头资料,也可作为团队开发 Webman 项目时的速查手册,帮助快速定位每个常见功能点的具体用法。
1. 别把Webman当又一个PHP框架:webman-manual要解决的是常驻内存下的正确用法
webman-manual 这份手册的名字很朴素,但它解决的不是“怎么安装一个框架”,而是常驻内存模型下“怎样写代码才不会在流量一起来就翻车”。webman 基于 Workerman 的事件驱动,进程启动后一直驻留,路由、中间件、控制器都比传统 PHP-FPM 轻量,但也因此带出了一整套不同的编码纪律。我最初接触时,按传统 PHP 习惯写全局变量和长连接,结果线上内存上涨、数据库连接数爆掉,后来照着手册逐条核对,才意识到框架的手册本质上是把底层行为翻译成了人话。它适合刚接手 webman 项目的后端,也适合优化过一段性能但始终靠猜的人。这份手册不是文档堆砌,而是一份事故检查单,接下来的内容,我会把它拆成可操作的落地步骤。
2. 跑通手册环境:安装、启动与第一个进程模型
2.1 安装webman并理解composer项目骨架
要先看懂手册,得先有一个能跑起来的 webman 项目。最常见的方式是直接使用 Composer 创建官方骨架,而不是从零手写入口文件。原因有两个:骨架已经把start.php、config目录、app目录的约定建好,手册里所有路径都以这个骨架为基准;同时骨架的依赖版本是经过组合验证的,避免你装的框架本体和辅助组件相互不兼容。
# 用 Composer 创建 webman 项目骨架到 webman-demo 目录 composer create-project workerman/webman webman-demo # 进入项目目录 cd webman-demo # 如果 create-project 没有自动执行依赖安装,则手动安装 composer install # 将手册项目中的示例代码目录(假设名为 manual-demo)复制进当前项目 cp -r ../webman-manual/manual-demo/* .create-project相当于把它背后配置好的依赖组合拉下来,再执行一次依赖解析,最终写入composer.lock。复制示例代码这一步不是必须的,但它能让后续章节的命令直接有真实文件可查。手册项目中通常会有一批带编号的 markdown 文件和配套 demo 目录,复制时注意不要把manual-demo里的vendor覆盖到主项目,否则依赖版本可能错乱。
安装过程中,Composer 可能会提示缺少扩展。webman 依赖pcntl、posix两个扩展,它们是实现进程管理和信号控制的基础。Linux 环境下可以用包管理器安装,常见命令是apt install php-pcntl php-posix(具体名称依据系统发行版会不同)。Windows 下 Workerman 无法直接运行多进程,手册也明确不建议在生产使用。建议用 Linux 虚拟机或容器做手册实验,避免后面被权限和信号问题干扰。
装好后,看composer.json。里面真正代表框架核心的是workerman/webman-framework,而workerman/workerman是底层网络引擎。理解这层关系很有用:手册如果提到“框架版本”,通常指webman-framework;如果提到“Workerman 行为”,通常指底层的事件循环、进程信号。两者独立升级,但互相有兼容要求。手动查版本时用composer show workerman/webman-framework,不要只看根包版本。
2.2 启动命令与debug模式:怎么确认手册里的配置生效
webman 的入口是根目录start.php,所有命令都通过它设置进程参数。手册里把这几个命令称为“日常逃生按钮”:启动、守护启动、重启、平滑重载。
# 前台启动:日志直接打印到终端,用于开发调试 php start.php start # 后台守护进程启动:进程脱离终端,日志写入文件 php start.php start -d # 重启:杀掉所有进程并重新拉起(会短暂断服) php start.php restart # 平滑重载:仅重载业务代码,不中断端口监听 php start.php reloadstart命令输出启动横幅,包含监听地址和 worker 进程数量。如果端口被占用,会直接抛异常。此时修改config/server.php中的listen项,例如改成http://0.0.0.0:8788。要注意修改listen后必须restart,因为reload不会重新初始化监听 socket。
-d是 daemon 模式,进程启动后标准输出被重定向到日志。Workerman 默认会把框架日志写到runtime/workerman.log,var_export之类的调试输出不一定出现在这里,需要手动监控业务日志。开发时建议不要加-d,这样能看到请求期间的所有异常。
reload是常驻内存框架最重要的命令。它向 master 进程发送 SIGUSR1 信号,worker 进程处理完当前请求后退出,再重新拉取文件。这意味着业务代码修改后无需重启,就能让新控制器、中间件生效。但reload不会重启 config 里注册的自定义进程,也不会重新加载config/server.php。只有restart才会整个进程模型重建。手册中反复强调:安装新 Composer 包、改listen、改process.php后,别省这一次restart,省了就是白等十分钟。
webman 的 debug 模式通过config/app.php控制。开发时设置debug => true,异常会以完整堆栈形式出现在 HTTP 响应中,方便定位是哪一行出的问题。生产环境务必设为false,避免暴露文件路径和变量内容。切换后同样要reload生效。验证配置是否生效,可以写一个临时路由返回phpinfo或框架版本号,然后curl访问,确认响应里能看到预期的修改。注意:临时调试路由用完要删,手册里也把它列为上线前扫查项。
注意:修改
config/server.php和config/process.php后,必须执行restart,不要依赖reload。
2.3 目录结构与手册索引:先读哪几个文件
一个全新 webman 项目的目录结构,是手册所有路径的参照系。第一次打开时,先不要急着看业务代码,按下面的顺序浏览。
webman-demo/ ├── app/ │ ├── controller/ # 控制器目录,HTTP 业务入口 │ ├── middleware/ # 中间件目录,请求过滤器 │ └── view/ # 视图目录(模板引擎,可选) ├── config/ │ ├── app.php # 应用开关、debug、时区 │ ├── middleware.php # 中间件注册表 │ ├── process.php # 自定义进程注册表 │ └── server.php # 监听地址、进程数、运行用户 ├── runtime/ # 运行时文件:pid、日志、临时缓存 ├── start.php # 命令行启动入口 └── vendor/ # Composer 依赖我先看config/server.php,因为它决定了进程的“身体构造”:监听地址、transport 协议、worker 进程数、运行用户和组。再读config/middleware.php,了解请求默认要过几道过滤;接着看config/process.php,那里是所有定时任务、自定义服务的窝点。最后才打开app/controller下某个真实控制器,验证前面的理解。
手册目录通常自带编号和标签。比如01-快速开始、02-进程模型、03-中间件、04-连接管理、05-常见故障,每个章节顶部会标注“适用 webman-framework 版本”和“风险等级”。我习惯用grep扫描手册中的风险字段,把高风险章节抄到团队的周会文档里。这份手册不是给人从头到尾读的,而是按“当前痛点”去检索。比如线上连接数暴涨,直接翻“连接管理”章节,对照它给的连接池示例,比自己猜要快得多。
另外,runtime目录里的webman.pid保存了 master 进程 PID,这是restart和reload命令判断运行状态用的。如果误删了这个文件,命令会报“not running”,但进程实际还在。解决办法是kill掉残留进程后重新start。手册里也把这个现象列为常见误操作。
3. 手册核心章节拆解:请求生命周期、中间件与控制器复用
3.1 请求从进入到响应的三条关键路径
手册里有一张简化的请求流程图,第一眼看去只有几行字,但每条路径背后都是进程内的状态流转。我把它归纳成三条路径:静态文件直出、路由分发、中间件链。静态文件请求往往在 Web 层就被处理了,不一定进控制器;路由分发是 URL 到控制器方法的映射;中间件链则是在进入控制器前和响应返回后都会执行的过滤器。
webman 的事件驱动模型下,每个 worker 进程在一个进程内顺序处理多个连接的事件。当一个新 HTTP 请求到达时,Workerman 解析请求报文,构造出Webman\Http\Request对象,然后交给App类的run方法。run会执行中间件、路由匹配、控制器实例化,最终生成Response对象返回给客户端。整个过程中,进程不会退出,所以Request对象在请求结束后可以被回收,但任何通过静态变量保存的数据仍残留在进程里。
三条路径中最容易出问题的是中间件链。手册特别强调:中间件可以嵌套,全局中间件先执行,然后是按目录或路由注册的中间件。执行顺序不是简单的从上到下,而是“入栈顺序”和“出栈顺序”相反。下面通过一个配置和一个小控制器验证。
3.2 中间件的执行顺序与参数约定
先看配置文件config/middleware.php:
<?php // 全局中间件配置:所有请求都会经过 return [ // '' 表示所有模块的全局中间件,按数组顺序执行 '' => [ app\middleware\AccessLog::class, app\middleware\AuthCheck::class, ], // 'api' 表示只对路径前缀为 /api 的请求生效 'api' => [ app\middleware\CorsMiddleware::class, ], ];这里约定''下的中间件是全局的,'api'下的中间件只作用于api模块。执行时,先运行全局中间件列表中的第一个AccessLog,再运行第二个AuthCheck,如果请求路径匹配api前缀,再运行CorsMiddleware。响应返回时,顺序完全反过来。也就是说,最晚进入的中间件最早拿到响应。这个行为和 Laravel 等框架一致,但 webman 的手册里用了一个更直观的说法:中间件像一层层洋葱,你从外往里剥,留下核心控制器;响应再从内往外穿。
下面的AccessLog中间件演示怎么写:
<?php namespace app\middleware; use Webman\Http\Request; use Webman\Http\Response; class AccessLog { public function process(Request $request, callable $next): Response { // 请求进入时记录开始时间 $start = microtime(true); // 调用下一个中间件或控制器 $response = $next($request); // 响应返回后记录耗时 $duration = microtime(true) - $start; // 写入日志(这里简单打印,生产环境请用日志组件) file_put_contents(runtime_path() . '/access.log', sprintf("%s %.3fms\n", $request->method(), $duration), FILE_APPEND); return $response; } }process方法的签名是固定的:接收Request和callable $next,返回Response。如果不调用$next($request),请求链就会在这里中断,可以用来做权限拦截。注意:返回值必须是Response对象,所以在分支判断里要显式返回response()或json()。很多新手在拦截场景下只写了return没返回 Response,导致框架报类型错误。这类错误在常驻内存下会表现为 worker 进程重启,日志里却不一定有堆栈,需要打开 debug 才能看到具体信息。
3.3 控制器里不要做重活:常驻内存下的资源释放
控制器是业务代码最密集的地方,也是踩坑最重的地带。传统 PHP-FPM 下,一个请求结束后整个进程销毁,全局变量、静态变量、数据库连接统统释放。webman 的 worker 进程是长命的,这些变量不会自动清空。手册里关于控制器有两条铁律:不要在控制器里创建超长生命周期的大对象,不要用静态属性保存请求相关数据。
反例代码:
<?php namespace app\controller; use Webman\Http\Request; use Webman\Http\Response; class UserController { // 错误示范:用静态属性缓存请求数据 public static array $tempItems = []; public function index(Request $request): Response { $items = $request->all(); self::$tempItems[] = $items; // 每个请求都往静态变量里塞,内存永远不释放 return json(['count' => count(self::$tempItems)]); } }这个接口一调用,$tempItems就会在进程里持续累积。流量一大,几个 worker 进程各自持有越来越多的数据,内存以肉眼可见的速度上涨。正确做法是把请求级数据存到响应完成后立即消失的局部变量中,或者通过框架提供的request()辅助函数访问。如果确实需要缓存,应该用支持过期时间的缓存组件,而不是静态数组。
另一个“重活”是指耗时的 I/O。比如在控制器里直接发邮件、调用外部 API、处理图片。这些操作会阻塞当前 worker 事件循环,导致后续请求排队。手册里的常见方案是:把任务丢给异步队列或自定义进程。webman 本身不内置队列,但可以结合 Workerman 的AsyncTcpConnection或第三方消息队列客户端,让控制器只做“接收请求 + 投递任务”这两件轻活,然后立即返回。这样 worker 进程能迅速进入下一个事件,TPS 才能上去。
4. Webman手册里的性能参数:进程数、连接复用与压测验证
4.1 进程数的计算公式与上限
config/server.php中的count字段控制 worker 进程数量,这是 webman 手册第一个高性能参数。
// config/server.php return [ // 监听地址,0.0.0.0 表示所有网卡 'listen' => 'http://0.0.0.0:8787', // 传输层协议,tcp 即可 'transport' => 'tcp', // worker 进程数:建议 CPU 核心数的 2~4 倍 'count' => 8, // 运行用户,留空表示当前用户 'user' => '', // 运行组 'group' => '', ];count并非越大越好。每个 worker 进程是单线程的事件循环,当进程数超过 CPU 核心数过多时,操作系统会频繁切换上下文,反而降低吞吐。一般公式是N = CPU核心数 * 2,如果程序里有大量 IO 等待(比如外部 API),可以提高到 4 倍;如果主要是 CPU 计算,控制在核心数左右。手册里给出的经验值是 8~32,但最终要靠压测决定。修改count后必须restart,因为reload不会重建进程池。
还有一个经常被忽略的参数是max_request。传统 PHP-FPM 设置它是为了让进程定期退出,避免内存泄漏;webman 不建议设置太小,否则常驻内存的优势被抵消。手册里的建议是:如果代码质量还达不到零泄漏,可以设一个较大的值比如 5000,作为兜底;但要清楚,这会让 worker 定期重启,长连接会被重置。
下面是手册里常用的参数参考:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| count | CPU 核心数 x 2 | IO 密集可到 x4,CPU 密集约 x1 |
| max_request | 5000 或 0 | 0 表示不自动退出,大值用于兜底清理 |
| timeout | 30 | 连接超时,单位秒,默认 30 |
| listen | http://0.0.0.0:8787 | 生产环境可用内网 IP 限制来源 |
4.2 数据库长连接的正确姿势:手册里推荐的连接池
常驻内存下,最省资源的数据库访问方式是进程内长连接。每个 worker 进程持有自己的 PDO 实例,请求期间复用,请求结束后不断开。
<?php namespace support; class Db { private static ?\PDO $pdo = null; public static function pdo(): \PDO { // 首次调用时创建,后续请求直接复用 if (self::$pdo === null) { self::$pdo = new \PDO('mysql:host=127.0.0.1;dbname=test', 'user', 'pass'); self::$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); } return self::$pdo; } }使用这个Db类的控制器:
public function index(Request $request): Response { try { $stmt = \support\Db::pdo()->query('SELECT id FROM users LIMIT 1'); $row = $stmt->fetch(); return json(['ok' => true]); } catch (\PDOException $e) { // 连接断开时捕获异常,并清除静态 pdo,让下次请求重建 \support\Db::reset(); return json(['error' => $e->getMessage()], 500); } }这里的关键在reset()——当 MySQL 因为超时或重启断开连接后,静态 PDO 已经不可用。如果不重置,后续每个请求都会拿到一个死连接,直到进程被重启。手册里把这种方法叫做“惰性重连”:每次使用前不影响,捕获异常时清除实例。注意:长连接只解决连接建立开销,不解决事务污染。如果在一次请求里开启了事务,但请求结束前没有提交或回滚,下一次请求复用同一个 PDO 时,就可能读到上一次未提交事务的数据。所以每次请求开始或结束时,都应当检查inTransaction(),必要时回滚。更稳妥的做法是在框架提供的onWorkerStart回调里初始化连接,而不是在静态方法中首次调用时初始化,因为后者无法控制初始化时机。
4.3 用wrk压测并对照手册指标:TPS和响应时间怎么看
手册里给出的性能数据必须在自己机器上复现才可信。压测工具我常用wrk,它轻量、适合 HTTP 短连接场景。
# 10 个线程、100 个并发连接、持续压测 10 秒,记录延迟分布 wrk -t10 -c100 -d10s --latency http://127.0.0.1:8787/输出结果主要看三列:Requests/sec是 QPS,Latency Distribution是延迟分布,Transfer/sec是吞吐量。手册里如果写了“某配置下 QPS 到达 2 万”,那是在特定硬件、特定路由、特定并发下的结果。你的业务路由可能只有几十微秒,也可能包含数据库查询,所以不要只看 QPS 绝对值,要看同样的路由在调整配置前后的相对变化。
压测时还要注意wrk本身可能成为瓶颈。如果你用一台低配机器压测压力很大的接口,CPU 可能跑满,导致 QPS 上不去。建议用两台机器,压测端和业务端分开。修改count从 4 到 8 再做一轮,记录 QPS 曲线,找到拐点。手册里的“参考配置”只是起点,最终参数必须落在你自己的压测报告上。
5. Webman手册避坑指南:常驻内存下最容易翻车的5个细节
5.1 定时器重复执行:每个进程都在跑
现象:在手册里照搬了定时器代码,结果数据重复处理了 N 次,而且 N 刚好等于 worker 进程数。
原因:在onWorkerStart里直接调用了Timer::add,每个 worker 进程都会执行这个回调,于是定时任务被每个进程同时执行。
解决:定时任务应该放进config/process.php定义的自定义进程中,并确保这个自定义进程的count设为 1,或者把定时器判断条件改成只允许一个进程执行。进程模型的细节决定了这类问题只会在生产环境爆发,本地单个 worker 时一切正常。
5.2 修改代码后不生效:忘了 reload
现象:改完控制器,用浏览器刷新,返回的还是旧结果。于是反复检查缓存,甚至重启 PHP-FPM,但毫无作用。
原因:webman 常驻内存,php 文件在 worker 启动时已加载到内存,不会随文件修改而自动更新。
解决:执行php start.php reload,让 worker 退出重启并重新加载业务代码。如果改的是config/server.php或config/process.php,必须php start.php restart。注意reload只对注册过reloadable的进程生效,自定义进程默认可能不可重载,这时也得 restart。
5.3 数据库连接数打满
现象:并发 200 左右,数据库报Too many connections。查看进程状态,发现每个连接都处于sleep状态。
原因:在每个请求里通过new PDO创建连接,请求结束后 PHP 的垃圾回收没有立即释放连接,而 MySQL 的wait_timeout又较长,连接数持续累积。
解决:用进程内长连接,或者可靠的连接池。至少也要在请求结束前显式释放 PDO 引用,让垃圾回收有机会关闭连接。但更推荐自定义一个进程内单例,让每个 worker 只持有一个连接。连接数上限最多等于 worker 进程数,而不是并发请求数。
5.4 内存持续增长
现象:运行一周后,从ps看到某个 worker 进程内存占用达到 1.5GB,且只会涨不会降。
原因:代码里用了静态属性或全局变量保存请求数据,或者有循环引用未清理。常驻进程不会在请求结束时销毁变量,累积的数据永远留在内存里。
解决:用php start.php status查看 worker 内存占用,定位异常进程。在代码中规范静态变量的用途:只保存不随时间变化的环境类数据,请求数据一律放入局部变量。如果使用了第三方类库,留意它内部是否有长期缓存。定期reload可以暂时缓解,但不能根治。
5.5 日志文件权限混乱
现象:多 worker 同时写同一个日志文件,出现日志丢失、文件损坏或权限报错。
原因:操作系统对同一个文件的并发追加不是原子操作,多个进程同时写入时会交错,且日志轮转时权限冲突。
解决:使用 Workerman 自带的日志组件或 Monolog,按进程名和日期分文件。日志文件路径要包含pid或worker_id,例如runtime/log/2025-01-01_worker_1.log。这样即使多个进程同时写,也不会互相干扰。如果非要共享一个文件,可以给写操作加锁,但性能损耗不值得。
6. 把手册变成自己的项目:从阅读到输出一份可执行的手册
6.1 用Markdown组织手册:目录结构、示例代码和故障记录
手册不应只是存着官方文档,而应沉淀你项目的实际配置和踩坑记录。我的目录结构是:
webman-manual/ ├── README.md # 索引与适用范围 ├── 00-快速开始/ # 安装、启动命令 ├── 01-核心概念/ # 进程模型、生命周期 ├── 02-配置项/ # server、process、middleware 配置 ├── 03-实战案例/ # 连接池、定时任务、队列 ├── 04-排错手册/ # 按现象排错,每条包含现象/原因/解决 └── scripts/ # 自动验证脚本04-排错手册里的每条记录必须包含三要素:现象、原因、解决。这样后续出问题时可以直接按关键词检索,而不是重新翻聊天记录。每条记录的顶部注明适用版本和发生日期。比如“2025-03 某项目出现内存上涨,最终定位到静态属性缓存”。版本信息能帮你在升级框架后回滚参考。
6.2 把命令和配置参数变成自动验证脚本
手册里的命令如果只靠人肉执行,很容易在岁月中失准。我把高频验证写成 shell 脚本,放在scripts/下。
#!/usr/bin/env bash # 验证手册环境是否健康 # 1. 检查 PHP 语法 php -l start.php || exit 1 # 2. 启动服务(如果未运行) if ! curl -s http://127.0.0.1:8787/health > /dev/null; then php start.php start -d sleep 2 fi # 3. 检查健康接口 curl -s http://127.0.0.1:8787/health | grep -q 'OK' || exit 1 # 4. 检查中间件响应头 curl -sI http://127.0.0.1:8787/api | grep -q 'X-Request-Id' || exit 1 echo "HANDLE OK"这个脚本能告诉你手册里的命令是否真的有效。每个新同学接手项目时先跑一遍,如果失败,说明环境与手册不一致,需要更新手册而不是绕过脚本。脚本还可以接入 CI,在每次改完配置后自动验证。
6.3 怎样让手册持续更新:版本注释和变更记录
任何手册都会过时。webman 框架迭代快,配置项可能改名,进程模型也可能调整。我的习惯是:每次升级框架或修改线上配置,先在手册对应章节的头部加上一段“版本影响”,写明从哪个版本开始该配置生效、旧版本应该如何处理。同时维护一个CHANGELOG.md,把每次变更的动机和实际操作记下来。这不仅方便后来者,也让我在半年后回看时,能回忆起当时为什么选 16 个进程而不是 8 个。
我吃过最大的亏,是手册里没写“这个配置只在 debug 模式生效”,结果团队把配置抄到生产环境,线上报错时因 debug 关闭而看不到堆栈,白白排查了两天。从那以后,我要求手册里所有配置必须附带“环境标签”:dev、prod、all。这样从源头避免跨环境误用。
如果你现在在维护 webman 项目,不妨把官方手册当参考书,把自己项目的排错记录写成自己的手册。这份手册是活的,它记录的是你和这个框架之间的真实边界。希望帮到你。
本文还有配套的精品资源,点击获取