TrueAsync Server 为 PHP 带来了原生的高性能 HTTP 服务器
一切都在一个线程中首先也是最重要的一点TrueAsync Server 是一个一切都在一个线程中的服务器。从解析请求到发送响应的整个处理过程都在单一线程上完成。在这一点上TrueAsync Server 在以非 PHP 语言实现的 PHP 生态项目中几乎是独一无二的尽管 Swoole 在基础模式下也运行单个工作进程。AMPHP 服务器采用了类似的单线程事件循环模型——区别在于 AMPHP 是用 PHP 实现的而 TrueAsync Server 作为原生扩展嵌入到 PHP 进程中。每个线程一个事件循环的模型本身并不罕见这正是 NGINX、Envoy、Node.js 以及 Rust 技术栈 Tokio hyper 的构建方式。核心思想是一个线程从始至终同时持有连接和请求没有接受线程与工作线程之间的交接没有锁没有上下文切换。优势与代价这种架构有一个明显的缺点。如果 PHP 虚拟机与 TrueAsync Server 位于同一线程上而 PHP 虚拟机崩溃——服务器工作进程也会随之崩溃。客户端可能会突然失去连接。如果反应器与 PHP 虚拟机运行在不同的线程上甚至不同的进程中架构看起来会更健壮客户端至少能收到一个错误响应。缺点到此为止——剩下的都是优势无需线程间通信。线程间通信需要复杂的算法而这些算法永远无法在所有场景下都达到最优某些类型的网络负载表现良好另一些则不尽如人意。简单、可预测的扩展方式。启动第二个工作进程——性能大致翻倍。工作进程通过setWorkers(N)启动内核通过SO_REUSEPORT在它们之间分配连接。每个工作进程都是一个独立的事件循环没有共享状态没有全局锁。对服务器的完全、无约束控制。PHP 虚拟机与服务器是一个整体。在另一个线程上管理连接可能复杂得多当每个操作都在一个线程内时许多决策都变得更简单。顺便一提多工作进程模式在一定程度上抵消了工作进程崩溃的缺点一个工作进程崩溃不会拖垮其余进程。为什么用 C 而不是 PHPPHP 生态中已有不少现代服务器项目。FrankenPHP 基于 Go 语言实现的 Caddy还有 Rust 语言实现的服务器项目。TrueAsync Server 用 C 编写有充分的理由这是将服务器直接嵌入 PHP 的便捷方式——尽可能贴近 PHP 内核。底层使用了已经成为事实标准的 C 库nghttp2用于 HTTP/2ngtcp2nghttp3用于 HTTP/3llhttpNode.js 使用的同一个解析器用于 HTTP/1.1。服务器直接链接到 OpenSSL后者已经是 PHP 构建的一部分。不过对于 HTTP/3需要用 3.5 以上版本替换——这是其所需要的 QUIC TLS API 首次出现的版本。服务器使用了 Zend VM。这有利有弊。利——更好的资源控制服务器和 PHP 代码的内存在单个memory_limit内统一核算。弊——Zend VM 存在一些性能问题有时会影响服务器表现。服务器尽可能将数据结构直接解析为 PHP 数组。单端口多协议单个服务器支持多种协议。HTTP/1.1、HTTP/2、WebSocket、SSE 和 gRPC 共享同一个 TCP 端口和同一个事件循环协议选择通过 ALPNTLS 场景下或 HTTP Upgrade 完成。HTTP/3 通过 QUIC 运行在同一 UDP 端口上并通过Alt-Svc头部告知客户端使客户端在后续请求中无缝切换。这意味着一次$server-start()调用即可同时通过 HTTP/2 提供 REST API、通过 Server-Sent Events 推送事件、维持 WebSocket 连接并暴露 gRPC 端点。服务器优化策略高吞吐量并非靠一个大招实现——而是许多小决策的总和热路径上的池化。请求体缓冲区、压缩编码器、HTTP/3 流、连接槽位——一切都在池中管理。重复请求不会打扰分配器和内核编码器被复用而非重新创建。大缓冲区的几何增长。PHP 标准的smart_str存在一个隐藏的性能悬崖超过某个阈值后每次增长都会变成一次系统调用其开销随缓冲区大小而增长。在大请求体上这曾消耗多达一半的请求时间。热路径上的零拷贝。multipart 解析器直接操作传入缓冲区。HTTP/2 无需中间 PHP 缓冲区即可提供静态内容HTTP/1 对大文件回退到sendfile()。与内核网络栈友好协作。SO_REUSEPORT将连接分散到工作进程将头部与响应体合并发送chunk 大小匹配 TLS 记录大小。并发请求间的共享内存。一个请求打开的文件其缓冲区可被另一个请求复用。这些优化使代码相对于实际工作负载保持轻量。服务器嵌入到 TrueAsync 事件循环中使其在协程之间工作当 PHP 代码等待数据库响应时服务器接受下一个请求。当协程进入 I/O 等待时反应器立即处理下一个就绪事件——没有线程空闲等待。API 概览服务器的公开 API 包含两个基本类HttpServerConfig——配置对象。HttpServer——服务器本身由配置创建并启动。一个最小应用示例use TrueAsync\HttpServer; use TrueAsync\HttpServerConfig; $server new HttpServer( (new HttpServerConfig())-addListener(0.0.0.0, 8080) ); $server-addHttpHandler(function ($request, $response) { $response-setStatusCode(200)-setBody(Hello, World!); }); $server-start(); // 阻塞当前线程直到 stop() 被调用监听器监听器是协议 传输层 主机 端口的组合。addListener()——TCPHTTP/1.1 HTTP/2通过首字节或 ALPN 选择addHttp1Listener()/addHttp2Listener()——限制为单一协议的端口addHttp3Listener()——UDP/QUICaddUnixListener()——Unix 域套接字。处理器处理器是在每个新请求到达时被调用的函数。它们接收请求和响应对象并可以像往常一样操作它们。服务器支持多种类型的处理器每种对应特定的协议$server-addHttpHandler(fn ($req, $res) /* ... */); // HTTP/1.1 HTTP/2 $server-addHttp2Handler(fn ($req, $res) /* ... */); // HTTP/2 专用 $server-addWebSocketHandler(fn ($req, $res) /* ... */);每个处理器在自己的协程中运行HTTP/1——每个请求一个协程HTTP/2 和 HTTP/3——每个流一个协程。当处理器进入 await例如等待数据库响应时它既不会阻塞其他连接也不会阻塞其他流。请求与响应请求对象是只读的。其 API 包括getMethod()、getUri()、getHttpVersion()、getHeader()/getHeaderLine()/getHeaders()/hasHeader()头部名称不区分大小写、getContentType()、getContentLength()、getBody()。对于表单和文件上传——getPost()、getFiles()、getFile()。响应对象是唯一的输出通道。设置器支持链式调用流式接口$response -setStatusCode(200) -setHeader(Content-Type, text/plain) -setBody(payload) -end();流式传输由于服务器支持 HTTP/2 和 HTTP/3它从底层就为流式传输而构建。开发者可以完全控制数据何时发送到客户端而无需等待处理器完成。服务器不强制在内存中持有整个响应体才能一次性发送。响应有两种模式缓冲模式——setBody()/write()累积响应体在结束时一次性通过网络发出流式模式——send()将下一个数据块直接推送到网络。$server-addHttpHandler(function ($req, $res) { $res-setStatusCode(200)-setHeader(Content-Type, text/event-stream); foreach (fetch_events() as $event) { // 数据源可能是无限的 $res-send(data: {$event}\n\n); // 数据块立即发出 } $res-end(); // 关闭流 });第一次调用send()会提交头部此后对于 HTTP/1.1 这是Transfer-Encoding: chunked对于 HTTP/2 和 HTTP/3 则是独立的 DATA 帧。换句话说同一段处理器代码可以在任何协议下生成正确的流——服务器负责处理协议差异。流式传输对 HTTP/2 尤其有用。HTTP/2 中每个请求是同一连接内的独立流每个流在自己的协程中运行。流式输出意味着可以随着数据生成即时发送——Server-Sent Events、导出大型报告、gRPC 流式传输——而无需将全部数据展开到内存中。由于 HTTP/2 具有每个流的流量控制窗口慢速客户端不应该撑爆服务器内存。为此提供了$res-sendable()方法——它报告流是否准备好立即接收下一个数据块如果未就绪协程只需让出给其他协程直到窗口释放。请求体也可以作为流到达。使用Request::readBody()方法可以分块读取请求体而无需等待完整接收while (($chunk $req-readBody()) ! null) { $sink-write($chunk); // 每次处理 64 KiB而不是一次性处理 2 GiB }这样一来GB 级别的上传可以被代理到文件或另一个服务而不必在内存中组装完整数据。文件处理器服务器实现了一个专门优化的静态文件服务路径。它是一个构建器类StaticHandler在服务器上与常规处理器一起注册use TrueAsync\StaticHandler; $static (new StaticHandler(/assets/, /var/www/public)) -setIndexFiles(index.html) -enablePrecompressed(br, gzip, zstd) -setCacheControl(public, max-age86400); $server-addStaticHandler($static);第一个参数是 URL 前缀虚拟挂载路径第二个是磁盘上的目录。处理器的主要目标是以最快速度提供文件服务而不切换到 PHP 代码。以下特性已开箱即用MIME 类型——内置 44 种扩展名的表格二分查找外加通过setMimeType()自定义覆盖。条件请求——基于(mtime, size, inode)的弱 ETag处理If-None-Match/If-Modified-Since并返回 304 响应支持 HEAD 请求。范围请求——206 Partial Content、Content-Range对无效范围的正确 416 响应。预压缩文件——如果main.css.br/.gz/.zst与文件同目录且客户端接受服务器直接提供现成的压缩文件而非即时压缩。安全性——防止路径穿越../、%2e%2e、NUL 字节、反斜杠dotfile 策略.git/默认关闭符号链接策略拒绝 / 跟随 / OwnerMatch按 glob 掩码隐藏文件。打开文件缓存——可选的按处理器配置的打开文件缓存支持 LRU 和 TTL在热点文件集上跳过stat、ETag