Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 libgnutls28-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 --with-gnutls
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-gnutls --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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

YurunHttp,支持智能识别 Curl/Swoole 场景的高性能 Http Client。

支持链式操作,简单易用。支持并发批量请求、HTTP2、WebSocket 全双工通信协议。
支持链式操作,简单易用。支持并发批量请求、HTTP2、HTTP3、WebSocket 全双工通信协议。

非常适合用于开发通用 SDK 包,不必再为 Swoole 协程兼容而头疼!

Expand All @@ -28,6 +28,7 @@ YurunHttp 的目标是做最好用的 PHP HTTP Client 开发包!
* SSL 证书(HTTPS)
* 并发批量请求
* HTTP2
* HTTP3(Curl Handler,基于 QUIC)
* WebSocket
* Curl & Swoole 环境智能兼容
* 连接池
Expand All @@ -46,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)
Expand Down Expand Up @@ -267,6 +270,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
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ parameters:
-
message: '#Method \S+ has no return typehint specified.#'
paths:
- tests/unit/**Test.php
- tests/unit/**Test.php
- '#Constant CURL_HTTP_VERSION_3 not found.#'
35 changes: 34 additions & 1 deletion src/YurunHttp/Handler/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -419,6 +423,10 @@ private function getResponse($request, $handler, $body, $receiveHeaders)
// body
$result = new Response($body, curl_getinfo($handler, \CURLINFO_HTTP_CODE));

// 实际使用的 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);
foreach ($headers as $name => $value)
Expand All @@ -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 \defined('\CURL_HTTP_VERSION_1_0') ? \CURL_HTTP_VERSION_1_0 : 1:
return '1.0';
case \defined('\CURL_HTTP_VERSION_1_1') ? \CURL_HTTP_VERSION_1_1 : 2:
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:
return '3.0';
default:
return '';
}
}

/**
* parseHeaderOneRequest.
*
Expand Down Expand Up @@ -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');
}
Expand Down
4 changes: 4 additions & 0 deletions src/YurunHttp/Handler/Swoole.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
32 changes: 32 additions & 0 deletions src/YurunHttp/Http/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ class Response extends Psr7Response
*/
protected $streamId;

/**
* 实际使用的 HTTP 协议版本,如 '1.1'、'2.0'、'3.0'.
*
* @var string
*/
protected $httpVersion = '';

/**
* Request.
*
Expand Down Expand Up @@ -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;
}

/**
* 设置请求体.
*
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/Http3/Http3Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Yurun\Util\YurunHttp\Test\Http3;

use Yurun\Util\HttpRequest;
use Yurun\Util\YurunHttp;
use Yurun\Util\YurunHttp\Handler\Curl;
use Yurun\Util\YurunHttp\Test\BaseTest;

class Http3Test extends BaseTest
{
public function testHttp3(): void
{
// HTTP/3 仅 Curl Handler 支持
if (!\defined('\CURL_HTTP_VERSION_3'))
{
$this->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()
));
}
}
Loading