尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FrankenPHP 扩展工作者(Extension Workers)完全指南:为 PHP 扩展注册后台 Worker 池、发送消息与 HTTP 请求

FrankenPHP 扩展工作者(Extension Workers)完全指南:为 PHP 扩展注册后台 Worker 池、发送消息与 HTTP 请求 FrankenPHP 扩展工作者Extension Workers完全指南为 PHP 扩展注册后台 Worker 池、发送消息与 HTTP 请求【免费下载链接】frankenphp The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp扩展工作者Extension Workers是 FrankenPHP 为 Go 语言编写的 PHP 扩展提供的一套实验性EXPERIMENTAL基础设施它允许你的扩展维护一个专用的 PHP 线程池用于执行后台任务、处理异步事件或实现自定义协议。本指南以 docs/tr/extension-workers.md 为骨架结合 workerextension.go、caddy/app.go、worker.go 等源码完整讲解扩展工作者的注册方式、SendMessage/SendRequest两种交互模式、PHP 工作者脚本的写法以及生命周期钩子读完即可在自己的 FrankenPHP 扩展中落地队列系统、事件监听器或定时器。什么是扩展工作者FrankenPHP 允许你用 Go 编写 PHP 扩展详见 docs/extensions.md而扩展工作者则进一步让扩展能够管理一组专用的 PHP 线程池。这些线程持续运行一个 PHP 脚本从而让扩展具备后台任务执行把耗时操作从请求主路径中剥离异步事件处理消费 MQTT、Kafka 等消息源的事件自定义协议实现在 PHP 层实现非 HTTP 的业务协议队列系统、事件监听器、定时器等基础设施。从源码看核心抽象是 workerextension.go 中定义的frankenphp.Workers接口// EXPERIMENTAL: Workers allows you to register a worker. type Workers interface { // SendRequest calls the closure passed to frankenphp_handle_request() and updates the PHP context . SendRequest(rw http.ResponseWriter, r *http.Request) error // SendMessage calls the closure passed to frankenphp_handle_request(), passes message as a parameter, and returns the value produced by the closure. SendMessage(ctx context.Context, message any, rw http.ResponseWriter) (any, error) // NumThreads returns the number of available threads. NumThreads() int }SendMessage对应无头消息模式SendRequest对应HTTP 模拟模式NumThreads用于查询当前可用线程数。值得注意接口与注册 API 目前都标记为EXPERIMENTAL意味着其签名在未来版本中仍可能调整。注册扩展工作者FrankenPHP 提供三种注册方式按使用场景区分静态注册、Caddy 模块注册、纯 Go 嵌入注册。静态注册固定脚本与固定线程数如果工作者无需用户配置脚本路径、线程数固定可以直接在扩展的init()函数中注册。注册入口是 Caddy 包暴露的 RegisterWorkerspackage myextension import ( github.com/dunglas/frankenphp github.com/dunglas/frankenphp/caddy ) // 用于与工作者池通信的公共句柄 var worker frankenphp.Workers func init() { // 模块加载时注册工作者 worker caddy.RegisterWorkers( my-internal-worker, // 唯一名称 worker.php, // 脚本路径相对于运行目录或绝对路径 2, // 固定线程数 // 可选的生命周期钩子 frankenphp.WithWorkerOnServerStartup(func() { // 全局初始化逻辑... }), ) }RegisterWorkers的参数依次为唯一名称、PHP 脚本路径、线程数量以及可选的WorkerOption。其实现内部会调用frankenphp.WithExtensionWorkers(...)把注册信息追加到 Caddy 模块的全局选项列表中待 Caddy 启动frankenphp应用时统一初始化。在 Caddy 模块中注册用户可配置如果你打算把扩展分享出去例如一个通用的队列或事件监听器扩展推荐把它包装成 Caddy 模块让用户通过Caddyfile配置脚本路径和线程数。这要求实现caddy.Provisioner接口并自行解析 CaddyfileFrankenPHP 官方提供了 dunglas/frankenphp-queue 作为参考实现。从 Caddy 侧源码看注册进frankenphp全局配置的 worker 会在Start()阶段被统一装载caddy/app.go 遍历f.Workers为每个 worker 追加环境变量、监听模式、最大失败次数、最大线程数等WorkerOption再调用frankenphp.WithWorkers(...)完成注册。这也解释了为什么基于 Caddy 的注册能够直接融入 Caddyfile 的frankenphp { worker ... }配置体系。在纯 Go 应用中注册嵌入模式如果不使用 Caddy而是把 FrankenPHP 作为标准 Go 库嵌入自己的应用则在初始化选项时使用frankenphp.WithExtensionWorkers即可frankenphp.WithExtensionWorkers(name, fileName string, numThreads int, options ...WorkerOption) (Workers, Option)该方法返回两个值一个Workers句柄用于后续通信以及一个Option需要传给frankenphp.Init(...)。WithExtensionWorkers的定义位于 options.go它会通过withExtensionWorkers把扩展工作者句柄与底层 worker 对象关联起来见 worker.go 中o.extensionWorkers.internalWorker w的绑定逻辑。与工作者交互两种通信模式工作者池启动后就可以向它派发任务了。触发点可以是导出给 PHP 的原生函数//export_php:function或手工 C 桥接也可以是任意 Go 逻辑——cron 定时器、事件监听器MQTT、Kafka或任何其他 goroutine。无头模式SendMessageSendMessage直接把原始数据传给工作者脚本适合队列投递或简单命令。其底层实现见 workerextension.go它新建一个 FrankenPHP 上下文把消息放入handlerParameters然后通过internalWorker.handleRequest把请求派发给空闲线程并等待 PHP 闭包返回结果。示例一个异步队列扩展的导出函数。// #include Zend/zend_types.h import C import ( context unsafe github.com/dunglas/frankenphp ) //export_php:function my_queue_push(mixed $data): bool func my_queue_push(data *C.zval) bool { // 1. 确保工作者已就绪 if worker nil { return false } // 2. 发送给后台工作者 _, err : worker.SendMessage( context.Background(), // 标准 Go context unsafe.Pointer(data), // 传递给工作者的数据 nil, // 可选的 http.ResponseWriter ) return err nil }注意SendMessage是同步等待的它会一直阻塞到 PHP 闭包处理完该消息并返回结果。rw参数可传nil消息模式不需要 HTTP 响应。HTTP 模拟SendRequest如果扩展需要调用一个期望标准 Web 环境的 PHP 脚本会填充$_SERVER、$_GET等超全局变量应使用SendRequest。其实现见 workerextension.go内部构造一个带WithOriginalRequest和WithWorkerName选项的请求然后走与普通 HTTP 请求相同的ServeHTTP路径派发给工作者。// #include Zend/zend_types.h import C import ( net/http net/http/httptest unsafe github.com/dunglas/frankenphp ) //export_php:function my_worker_http_request(string $path): string func my_worker_http_request(path *C.zend_string) unsafe.Pointer { // 1. 准备请求与响应记录器 url : frankenphp.GoString(unsafe.Pointer(path)) req, _ : http.NewRequest(GET, url, http.NoBody) rr : httptest.NewRecorder() // 2. 发送给工作者 if err : worker.SendRequest(rr, req); err ! nil { return nil } // 3. 返回捕获的响应 return frankenphp.PHPString(rr.Body.String(), false) }SendRequest适合扩展内部发起子请求的场景工作者脚本会以完整 HTTP 语义执行包含请求行、请求头、请求体响应通过传入的http.ResponseWriter写出。示例中用httptest.NewRecorder()捕获响应体再通过frankenphp.PHPString转回 PHP 字符串返回给调用方。编写 PHP 工作者脚本工作者脚本在一个循环中运行同一个脚本可以同时处理裸消息与 HTTP 请求。两者通过回调参数区分SendMessage场景下闭包会收到消息负载$payloadSendRequest场景下$payload为null但超全局变量已填充。?php // 在同一循环中同时处理裸消息与 HTTP 请求 $handler function ($payload null) { // 情况 1消息模式 if ($payload ! null) { return Received payload: . $payload; } // 情况 2HTTP 模式标准 PHP 超全局变量已填充 echo Hello from page: . $_SERVER[REQUEST_URI]; }; while (frankenphp_handle_request($handler)) { gc_collect_cycles(); }关键点frankenphp_handle_request($handler)是阻塞调用直到有新的消息或 HTTP 请求到达返回false时如进程被要求关闭应退出循环每次循环迭代后调用gc_collect_cycles()主动回收垃圾避免长驻脚本内存持续增长脚本在每个线程中独立执行因此共享状态需要依赖持久化存储Redis、数据库或线程内静态变量不能假设跨线程共享内存。从 threadworker.go 可以看到工作者线程的调度细节线程在waitForWorkerRequest()中通过select同时监听thread.requestChan直接派发的请求与worker.requestChan排队请求拿到请求后把handlerParameters即SendMessage的消息作为参数返回给 PHP 层。生命周期钩子FrankenPHP 提供了四类钩子让 Go 代码在生命周期的特定节点执行。钩子的注册函数定义于 options.go对应表格如下钩子类型选项名称签名时机与典型用途服务器级WithWorkerOnServerStartupfunc()全局初始化只执行一次。例如连接 NATS/Redis。服务器级WithWorkerOnServerShutdownfunc()全局清理只执行一次。例如关闭共享连接。线程级WithWorkerOnReadyfunc(threadID int)每个线程的初始化线程启动时调用携带线程 ID。线程级WithWorkerOnShutdownfunc(threadID int)每个线程的清理携带线程 ID。从源码验证各钩子的触发点服务器级启动/关闭钩子在 frankenphp.go 中注册Init()成功启动所有工作者后依次调用每个 worker 的onServerStartup()Shutdown()时见 frankenphp.go逆序调用收集到的onServerShutdown函数线程级钩子在 threadworker.go 的beforeScriptExecution()中触发onThreadReady(threadIndex)在线程进入Ready/TransitionComplete状态、执行脚本前调用onThreadShutdown(threadIndex)在线程被转换或关闭时调用随后线程从 worker 中分离detachThread。完整示例package myextension import ( fmt github.com/dunglas/frankenphp frankenphpCaddy github.com/dunglas/frankenphp/caddy ) func init() { workerHandle frankenphpCaddy.RegisterWorkers( my-worker, worker.php, 2, // 服务器启动全局 frankenphp.WithWorkerOnServerStartup(func() { fmt.Println(扩展服务器启动中...) }), // 线程就绪每个线程一次 // 注意函数接收表示线程 ID 的整数参数 frankenphp.WithWorkerOnReady(func(id int) { fmt.Printf(扩展工作线程 #%d 就绪。\n, id) }), ) }源码级原理与注意事项底层 worker 结构每个扩展工作者在运行时对应一个 worker.go 中的worker对象它维护名称、脚本绝对路径、线程数、requestChan请求队列、线程列表以及最大连续失败次数等字段。newWorkerworker.go会执行若干关键步骤通过filepath.EvalSymlinks与fastabs.FastAbs解析脚本的绝对路径并校验文件存在名称以m#开头的 worker 属于模块 worker只能按名称匹配、不能按路径匹配自动为 worker 注入FRANKENPHP_WORKER环境变量并把脚本所在目录设为请求文档根目录同一文件名或同一名称的 worker 不能重复注册。请求派发与排队handleRequestworker.go采用先到先得策略优先尝试把请求直接投递给空闲线程若所有线程繁忙则进入排队状态并触发自动扩缩容受max_threads限制同时受全局max_wait_time约束超时后返回ErrMaxWaitTimeExceeded。这意味着扩展工作者天然继承了 FrankenPHP 的线程缩放能力——scaling.go 中的自动扩缩逻辑同样作用于扩展工作者池。测试佐证仓库中的 workerextension_test.go 同时使用了全部四个生命周期钩子对扩展工作者进行集成测试此外 caddy/caddy_test.go 中大量测试覆盖了 worker 模式下frankenphp_handle_request的指标采集与状态转换可作为编写自己扩展测试时的参考模板。适用前提与限制扩展工作者属于EXPERIMENTAL功能Workers接口与注册 API 的签名可能变化工作者脚本必须主动调用frankenphp_handle_request()才能接收消息/请求每个扩展只能注册一个具有唯一名称的工作者池线程数在注册时固定除非在 Caddy 模块中暴露配置若 worker 脚本连续启动失败会按maxConsecutiveFailures策略含指数退避重试处理达到阈值后终止启动流程threadworker.go。小结扩展工作者把 FrankenPHP 的常驻 PHP 进程能力开放给了 Go 编写的 PHP 扩展通过caddy.RegisterWorkersCaddy 环境或frankenphp.WithExtensionWorkers纯 Go 嵌入注册线程池用SendMessage投递裸数据、用SendRequest模拟 HTTP 请求配合服务器级与线程级两套生命周期钩子完成资源管理与清理。对于队列、事件驱动、定时任务等异步场景这是在 FrankenPHP 扩展中构建后台处理能力的标准路径。更多扩展编写细节类型转换、导出函数与类、常量与命名空间可继续阅读 docs/extensions.md 与 docs/tr/extensions.md。【免费下载链接】frankenphp The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表