From 11f8a50b57ba1cdfa60eb5c430ec08ee8f42fa17 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 19:36:17 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81=20HTTP?= =?UTF-8?q?/3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 75 ++++++++++++++++++++++++++++++++ README.md | 20 ++++++++- composer.json | 2 +- src/YurunHttp/Handler/Curl.php | 35 ++++++++++++++- src/YurunHttp/Handler/Swoole.php | 4 ++ src/YurunHttp/Http/Response.php | 32 ++++++++++++++ tests/unit/Http3/Http3Test.php | 58 ++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 tests/unit/Http3/Http3Test.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac38a1e..15befed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,3 +68,78 @@ jobs: - name: Print logs if: failure() run: php .github/print-logs.php + + http3: + name: HTTP/3 (Linux + PHP 8.4) + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf libtool pkg-config \ + cmake ninja-build libssl-dev libpsl-dev libnghttp2-dev git + + # 发行版自带 libcurl 未编译 QUIC,这里从源码构建带 HTTP/3 的 curl + - name: Build curl with HTTP/3 (ngtcp2 + nghttp3) + run: | + set -e + PREFIX=/usr/local + export PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH + + git clone --depth 1 --branch v1.6.0 https://github.com/ngtcp2/nghttp3.git + cd nghttp3 + git submodule update --init + autoreconf -fi + ./configure --prefix=$PREFIX --enable-lib-only + make -j"$(nproc)" + sudo make install + cd .. + + git clone --depth 1 --branch v1.6.0 https://github.com/ngtcp2/ngtcp2.git + cd ngtcp2 + git submodule update --init + autoreconf -fi + ./configure --prefix=$PREFIX --enable-lib-only + make -j"$(nproc)" + sudo make install + cd .. + + git clone --depth 1 --branch curl-8_10_1 https://github.com/curl/curl.git + cd curl + autoreconf -fi + ./configure --prefix=$PREFIX --with-openssl --with-nghttp2 \ + --with-nghttp3 --with-ngtcp2 + make -j"$(nproc)" + sudo make install + cd .. + + echo "$PREFIX/lib" | sudo tee /etc/ld.so.conf.d/zz-local.conf + sudo ldconfig + /usr/local/bin/curl --version | head -n1 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: curl, openssl, mbstring, json + coverage: none + tools: composer:v2 + + # 若环境仍不支持 HTTP/3,直接让 job 失败,避免“假绿” + - name: Verify HTTP/3 support + run: | + php -r 'exit((defined("CURL_VERSION_HTTP3") && (curl_version()["features"] & CURL_VERSION_HTTP3)) ? 0 : 1);' + php -r 'echo "libcurl: " . curl_version()["version"] . PHP_EOL;' + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run HTTP/3 tests + run: ./vendor/bin/phpunit -c ./tests/phpunit.xml --filter Http3Test + + - name: Print server logs (on failure) + if: failure() + run: php .github/print-logs.php diff --git a/README.md b/README.md index e11980a..88a9e94 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ YurunHttp,支持智能识别 Curl/Swoole 场景的高性能 Http Client。 -支持链式操作,简单易用。支持并发批量请求、HTTP2、WebSocket 全双工通信协议。 +支持链式操作,简单易用。支持并发批量请求、HTTP2、HTTP3、WebSocket 全双工通信协议。 非常适合用于开发通用 SDK 包,不必再为 Swoole 协程兼容而头疼! @@ -28,6 +28,7 @@ YurunHttp 的目标是做最好用的 PHP HTTP Client 开发包! * SSL 证书(HTTPS) * 并发批量请求 * HTTP2 +* HTTP3(Curl Handler,基于 QUIC) * WebSocket * Curl & Swoole 环境智能兼容 * 连接池 @@ -267,6 +268,23 @@ Curl: `php --ri curl` Swoole: `php --ri swoole` +### Http3 用法 + +> 仅支持 Curl Handler(基于 QUIC),Swoole Handler 会抛出异常。 + +```php +$http = new HttpRequest; +$http->protocolVersion = '3.0'; // 这句是关键 +$response = $http->get('https://www.taobao.com/'); +``` + +使用 HTTP/3 需注意: + +* 必须使用 HTTPS 地址(QUIC over TLS)。 +* 需要 libcurl >= 7.66 且编译了 QUIC 支持(ngtcp2 / quiche),可通过 `php --ri curl` 查看。 +* 当服务器不支持 HTTP/3 时,Curl 会自动降级到 HTTP/2 / HTTP/1.1。 +* Swoole Handler 不支持 HTTP/3,发起 `3.0` 请求会抛出 `Swoole handler does not support HTTP/3` 异常。 + ### Http2 全双工用法 > 该用法仅支持 Swoole diff --git a/composer.json b/composer.json index 1d5a69a..818d557 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "yurunsoft/yurun-http", - "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!", + "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、Http3、WebSocket!", "require": { "php": ">=7.1", "psr/http-message": "~1.0|~2.0", diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index 9d2c40e..8cfc7b1 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -366,6 +366,10 @@ private function buildCurlHandlerEx(&$request, $handler, $uri = null, $method = $httpVersion = \CURL_HTTP_VERSION_2; } break; + case '3.0': + // HTTP/3 基于 QUIC,需 libcurl >= 7.66 且编译了 QUIC 支持 + $httpVersion = \defined('\CURL_HTTP_VERSION_3') ? \CURL_HTTP_VERSION_3 : 30; + break; default: $httpVersion = \CURL_HTTP_VERSION_1_1; break; @@ -419,6 +423,10 @@ private function getResponse($request, $handler, $body, $receiveHeaders) // body $result = new Response($body, curl_getinfo($handler, \CURLINFO_HTTP_CODE)); + // 实际使用的 HTTP 协议版本 + $httpVersion = $this->getCurlHttpVersion(curl_getinfo($handler, \CURLINFO_HTTP_VERSION)); + + // headers $headers = $this->parseHeaderOneRequest($receiveHeaders); foreach ($headers as $name => $value) @@ -443,10 +451,35 @@ private function getResponse($request, $handler, $body, $receiveHeaders) return $result->withRequest($request) ->withCookieOriginParams($cookies) + ->withHttpVersion($httpVersion) ->withError(curl_error($handler)) ->withErrno(curl_errno($handler)); } + /** + * 将 curl 的 CURLINFO_HTTP_VERSION 枚举值转换为协议版本字符串. + * + * @param int $version + * + * @return string + */ + private function getCurlHttpVersion($version) + { + switch ($version) + { + case \CURL_HTTP_VERSION_1_0: + return '1.0'; + case \CURL_HTTP_VERSION_1_1: + return '1.1'; + case \CURL_HTTP_VERSION_2: + return '2.0'; + case \defined('\CURL_HTTP_VERSION_3') ? \CURL_HTTP_VERSION_3 : 30: + return '3.0'; + default: + return ''; + } + } + /** * parseHeaderOneRequest. * @@ -615,7 +648,7 @@ private function parseHeaders(&$request, &$options) { $request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, self::$defaultUA)); } - if (!$request->hasHeader('Connection') && $request->getProtocolVersion() >= 1.1) + if (!$request->hasHeader('Connection') && $request->getProtocolVersion() >= 1.1 && '3.0' !== $request->getProtocolVersion()) { $request = $request->withHeader('Connection', 'Keep-Alive')->withHeader('Keep-Alive', '300'); } diff --git a/src/YurunHttp/Handler/Swoole.php b/src/YurunHttp/Handler/Swoole.php index ec68b01..3a32206 100644 --- a/src/YurunHttp/Handler/Swoole.php +++ b/src/YurunHttp/Handler/Swoole.php @@ -232,6 +232,10 @@ public function send(&$request) */ public function sendDefer($request) { + if ('3.0' === $request->getProtocolVersion()) + { + throw new \RuntimeException('Swoole handler does not support HTTP/3'); + } $isHttp2 = '2.0' === $request->getProtocolVersion(); if ($poolIsEnabled = $this->poolIsEnabled) { diff --git a/src/YurunHttp/Http/Response.php b/src/YurunHttp/Http/Response.php index f2b6835..7d294f7 100644 --- a/src/YurunHttp/Http/Response.php +++ b/src/YurunHttp/Http/Response.php @@ -56,6 +56,13 @@ class Response extends Psr7Response */ protected $streamId; + /** + * 实际使用的 HTTP 协议版本,如 '1.1'、'2.0'、'3.0'. + * + * @var string + */ + protected $httpVersion = ''; + /** * Request. * @@ -374,6 +381,31 @@ public function getStreamId() return $this->streamId; } + /** + * 设置实际使用的 HTTP 协议版本. + * + * @param string $httpVersion + * + * @return static + */ + public function withHttpVersion($httpVersion) + { + $self = clone $this; + $self->httpVersion = $httpVersion; + + return $self; + } + + /** + * 获取实际使用的 HTTP 协议版本,如 '1.1'、'2.0'、'3.0'. + * + * @return string + */ + public function getHttpVersion() + { + return $this->httpVersion; + } + /** * 设置请求体. * diff --git a/tests/unit/Http3/Http3Test.php b/tests/unit/Http3/Http3Test.php new file mode 100644 index 0000000..4b91211 --- /dev/null +++ b/tests/unit/Http3/Http3Test.php @@ -0,0 +1,58 @@ +markTestSkipped('CURL_HTTP_VERSION_3 未定义,当前 PHP/libcurl 版本不支持 HTTP/3'); + } + + $curlVersion = \curl_version(); + // HTTP/3 需要 libcurl >= 7.66.0(0x074200) + if ($curlVersion['version_number'] < 0x074200) + { + $this->markTestSkipped(sprintf('libcurl %s < 7.66.0,不支持 HTTP/3', $curlVersion['version'])); + } + + // 检测 libcurl 是否编译了 HTTP/3(QUIC) 支持 + if (\defined('\CURL_VERSION_HTTP3') && !($curlVersion['features'] & \CURL_VERSION_HTTP3)) + { + $this->markTestSkipped('当前 libcurl 未编译 HTTP/3(QUIC) 支持'); + } + + // 强制使用 Curl Handler(Swoole 不支持 HTTP/3) + YurunHttp::setDefaultHandler(Curl::class); + + $http = new HttpRequest(); + $http->protocolVersion = '3.0'; + $http->timeout = 10000; + $http->connectTimeout = 10000; + + $response = $http->get('https://www.taobao.com/'); + + // 环境/网络不支持时跳过,而不是判定失败 + if (0 !== $response->errno()) + { + $this->markTestSkipped(sprintf('HTTP/3 请求失败(环境/网络原因): %s', $response->error())); + } + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()); + + // 校验响应确实是 HTTP/3(QUIC) + $this->assertEquals('3.0', $response->getHttpVersion(), sprintf( + '响应协议版本不是 HTTP/3,实际为: %s', + $response->getHttpVersion() + )); + } +} From 82d3cda2d794a95d115517424b633bdcca9f2fa7 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 19:46:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +++--- src/YurunHttp/Handler/Curl.php | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15befed..ad08c21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y build-essential autoconf libtool pkg-config \ - cmake ninja-build libssl-dev libpsl-dev libnghttp2-dev git + cmake ninja-build libgnutls28-dev libpsl-dev libnghttp2-dev git # 发行版自带 libcurl 未编译 QUIC,这里从源码构建带 HTTP/3 的 curl - name: Build curl with HTTP/3 (ngtcp2 + nghttp3) @@ -102,7 +102,7 @@ jobs: cd ngtcp2 git submodule update --init autoreconf -fi - ./configure --prefix=$PREFIX --enable-lib-only + ./configure --prefix=$PREFIX --enable-lib-only --with-gnutls make -j"$(nproc)" sudo make install cd .. @@ -110,7 +110,7 @@ jobs: git clone --depth 1 --branch curl-8_10_1 https://github.com/curl/curl.git cd curl autoreconf -fi - ./configure --prefix=$PREFIX --with-openssl --with-nghttp2 \ + ./configure --prefix=$PREFIX --with-gnutls --with-nghttp2 \ --with-nghttp3 --with-ngtcp2 make -j"$(nproc)" sudo make install diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index 8cfc7b1..86ac86b 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -423,9 +423,9 @@ private function getResponse($request, $handler, $body, $receiveHeaders) // body $result = new Response($body, curl_getinfo($handler, \CURLINFO_HTTP_CODE)); - // 实际使用的 HTTP 协议版本 - $httpVersion = $this->getCurlHttpVersion(curl_getinfo($handler, \CURLINFO_HTTP_VERSION)); - + // 实际使用的 HTTP 协议版本(CURLINFO_HTTP_VERSION 自 PHP 7.3 引入) + $curlHttpVersion = \defined('\CURLINFO_HTTP_VERSION') ? curl_getinfo($handler, \CURLINFO_HTTP_VERSION) : 0; + $httpVersion = $this->getCurlHttpVersion($curlHttpVersion); // headers $headers = $this->parseHeaderOneRequest($receiveHeaders); @@ -467,11 +467,11 @@ private function getCurlHttpVersion($version) { switch ($version) { - case \CURL_HTTP_VERSION_1_0: + case \defined('\CURL_HTTP_VERSION_1_0') ? \CURL_HTTP_VERSION_1_0 : 1: return '1.0'; - case \CURL_HTTP_VERSION_1_1: + case \defined('\CURL_HTTP_VERSION_1_1') ? \CURL_HTTP_VERSION_1_1 : 2: return '1.1'; - case \CURL_HTTP_VERSION_2: + case \defined('\CURL_HTTP_VERSION_2') ? \CURL_HTTP_VERSION_2 : 3: return '2.0'; case \defined('\CURL_HTTP_VERSION_3') ? \CURL_HTTP_VERSION_3 : 30: return '3.0'; From 8b85394135618a8b0cc4451cf42192b29144e73e Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 19:52:06 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpstan.neon | 3 ++- src/YurunHttp/Handler/Curl.php | 4 ++-- tests/unit/Http3/Http3Test.php | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 1be0aa9..bbb04d4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -26,4 +26,5 @@ parameters: - message: '#Method \S+ has no return typehint specified.#' paths: - - tests/unit/**Test.php \ No newline at end of file + - tests/unit/**Test.php + - '#Constant CURL_HTTP_VERSION_3 not found.#' diff --git a/src/YurunHttp/Handler/Curl.php b/src/YurunHttp/Handler/Curl.php index 86ac86b..99bc479 100644 --- a/src/YurunHttp/Handler/Curl.php +++ b/src/YurunHttp/Handler/Curl.php @@ -368,7 +368,7 @@ private function buildCurlHandlerEx(&$request, $handler, $uri = null, $method = break; case '3.0': // HTTP/3 基于 QUIC,需 libcurl >= 7.66 且编译了 QUIC 支持 - $httpVersion = \defined('\CURL_HTTP_VERSION_3') ? \CURL_HTTP_VERSION_3 : 30; + $httpVersion = \defined('\CURL_HTTP_VERSION_3') ? CURL_HTTP_VERSION_3 : 30; break; default: $httpVersion = \CURL_HTTP_VERSION_1_1; @@ -473,7 +473,7 @@ private function getCurlHttpVersion($version) return '1.1'; case \defined('\CURL_HTTP_VERSION_2') ? \CURL_HTTP_VERSION_2 : 3: return '2.0'; - case \defined('\CURL_HTTP_VERSION_3') ? \CURL_HTTP_VERSION_3 : 30: + case \defined('\CURL_HTTP_VERSION_3') ? CURL_HTTP_VERSION_3 : 30: return '3.0'; default: return ''; diff --git a/tests/unit/Http3/Http3Test.php b/tests/unit/Http3/Http3Test.php index 4b91211..477b347 100644 --- a/tests/unit/Http3/Http3Test.php +++ b/tests/unit/Http3/Http3Test.php @@ -17,7 +17,7 @@ public function testHttp3(): void $this->markTestSkipped('CURL_HTTP_VERSION_3 未定义,当前 PHP/libcurl 版本不支持 HTTP/3'); } - $curlVersion = \curl_version(); + $curlVersion = curl_version(); // HTTP/3 需要 libcurl >= 7.66.0(0x074200) if ($curlVersion['version_number'] < 0x074200) { From 1c7aa5c53ab4620f7fe3c313454824a5c69602b5 Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 19:59:12 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 88a9e94..4652d14 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ API 文档:[https://apidoc.gitee.com/yurunsoft/YurunHttp](https://apidoc.gitee > 每个小版本的更新日志请移步到 Release 查看 +v5.1.0 支持 HTTP/3(基于 QUIC,Curl Handler,需 libcurl >= 7.66 且编译 QUIC 支持;Swoole Handler 不支持) + v5.0.1 支持 WebSocket 压缩及指定 Opcode v5.0.0 支持 `psr/http-message` `~2.0` 版本 (PHP >= 7.1)