编程

使用 PHP Curl 扩展发送 HTTP/3 请求

1169 2023-11-12 23:30:00

HTTP/3 是 HTTP 的第三个大版本,它基于 QUIC。不像基于 TCP 的 HTTP/1.1 和 HTTP/2,HTTP 是基于一个多路复用的 UDP 协议,QUIC。HTTP/3 以及 TLS 1.3,在性能和延迟上进行了巨大改进。虽然 HTTP/3 修改了许多传输层语义(比如,从 TCP 到 UDP 的转变),但保留了 HTTP 语法请求头、请求方法、响应和状态方法。

现在大部分浏览器都支持 HTTP/3,而 HTTP 客户端和网络服务器比如 Curl、Nginx 和 Litespeed 提供了实验性支持。Caddy 服务器甚至默认启用 HTTP/3 支持。

利用 Curl 中提供的实验性 HTTP/3 支持,PHP 的 Curl 扩展可以使用 HTTP/3 支持构建。本文解释了如何使用  HTTP/3 支持编译 PHP Curl 扩展及其依赖项,以及如何使用 PHP 发出 HTTP/3 请求

如何 使用 PHP Curl 扩展发起 HTTP/3 请求

Curl 有一个名为 CURLOPT_HTTP_VERSION 的选项,它可用于设置 Curl handler 在 HTTP 请求中使用的 HTTP 版本。默认情况下,当前 Curl 构建默认为带有 HTTP 1.1 回退的 HTTP/2。如果 web 服务器不支持 HTTP/2,Curl 将无缝地使用 HTTP/1.1。

对于 HTTP/3,Curl 也是一样的行为。Curl 有称为 HTTPS Eyeballing 的方式,它尝试建立一个 QUIC 握手,不过有一个 200ms 的硬超时。这样可以确保在连接足够快的情况下使用 HTTP/3,但不会对不使用 HTTP/3 的请求产生任何重大影响。

它确保了如果连接够快,HTTP/3 将被使用,不过 

使用 Curl 发起 HTTP/3 请求:

  • Curl 必需使用 HTTP/3 支持构建
  • Curl 版本大于 7.66
  • PHP 版本大于 8.2

使用 PHP Curl 扩展发起 HTTP/3 请求,与设置 CURLOPT_HTTP_VERSION 选项一样简单:

$ch = curl_init("https://php.watch/");
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
curl_exec($ch);

CURL_HTTP_VERSION_3 常量未在 PHP 8.3 及之前的 PHP 版本中声明。此处是一个修复该功能的 PR

要确保 CURL_HTTP_VERSION_3 常量未被声明的情况下的兼容性,可以在用户端对其进行声明,或者直接传入常量值到 curl_setopt 函数中。

if (!defined('CURL_HTTP_VERSION_3')) {
    define('CURL_HTTP_VERSION_3', 30);
}

$ch = curl_init("https://php.watch/");

curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
// or
curl_setopt($ch, CURLOPT_HTTP_VERSION, 30);

curl_setopt($ch, CURLOPT_HEADER, true);
curl_exec($ch);

如果 Curl 支持 HTTP/3,下面代码段未使用 Curl 发出 HTTP/3 请求:

HTTP/3 200
strict-transport-security: max-age=31536000;includeSubDomains;preload
content-type: text/html; charset=UTF-8
date: Sat, 28 Oct 2023 18:38:46 GMT
...

CURL_HTTP_VERSION_3 选项说明 Curl 可以使用的 HTTP 版本最高可为 HTTP/3。如果远程服务器不支持 HTTP/3,Curl 会静默并无缝回退到另一个服务器和 Curl 都支持的 HTTP 版本。

请注意,没有使用 HTTP/3 支持构建的 Curl 扩展将导致请求在 curl_setoptcurl_exec 调用时都返回 false

下面代码段使用 CURL_HTTP_VERSION_3ONLY (= 31),告诉 Curl 不回退版本使用 HTTP/3。如果远程服务器和 Curl 不支持 HTTP/3,请求会失败。

if (!defined('CURL_HTTP_VERSION_3ONLY')) {
    define('CURL_HTTP_VERSION_3ONLY', 31);
}

$ch = curl_init("https://php.watch/");

curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3ONLY);
// or
curl_setopt($ch, CURLOPT_HTTP_VERSION, 31);

curl_exec($ch);

在使用 Curl 发起 HTTP/3 请求前,请确保检测 Curl 是否有 HTTP/3 支持。

CURL_HTTP_VERSION_3 还是 CURL_VERSION_HTTP3?

  • CURL_VERSION_HTTP3 是一个 feature flag,它带有一个可用于位运算的值,用以确定是否支持 HTTP/3。在 Curl 7.66 及 PHP 8.2 起中可用
  • CURL_HTTP_VERSION_3CURL_HTTP_VERSION_3ONLY 是 Curl 选项 CURLOPT_HTTP_VERSION 中的常量。CURL_HTTP_VERSION_3 z在 Curl 7.66 中添加,CURL_HTTP_VERSION_3ONLY 在 Curl 7.88 中添加。 
  • CURL_VERSION_HTTP3CURL_HTTP_VERSION_3 是不同的的。

检测 PHP Curl 扩展中的 HTTP/3 支持

PHP 常量 CURL_VERSION_HTTP3CURL_HTTP_VERSION_3CURL_HTTP_VERSION_3ONLY 被声明,并不能说明 Curl 是使用 HTTP/3 支持进行构建的。

有三种方法可以检测 Curl 扩展是否支持 HTTP/3。

检测 phpinfo 输出

phpinfo() 输出及php -i 显示 Curl 是否是使用 HTTP/3 支持进行构建:

phpinfo 输出显示 HTTP/3 支持

使用 curl_version 函数

 curl_version() 函数的 features 值返回 Curl 支持的所有特性的位掩码。 

下面代码显示使用 curl_version() 函数检测 HTTP/3 支持,特性标志为  CURL_VERSION_HTTP3

if (defined('CURL_VERSION_HTTP3') && curl_version()['features'] & CURL_VERSION_HTTP3) {
    // HTTP/3 supported 
}

curl_setopt 调用返回值

CURLOPT_HTTP_VERSION 选项设置为 CURL_HTTP_VERSION_3 时,如果 Curl 扩展没有使用 HTTP/3 构建,Curl 将返回 false

$ch = curl_init("https://php.watch/");

$h3_status = curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
if ($h3_status === false) {
    curl_setopt($ch, CURLOPT_HTTP_VERSION, null)
}

curl_exec($ch);

Setting CURLOPT_HTTP_VERSION = null resets the failed attempt to select HTTP/3, and leaves Curl to pick the best HTTP version.

使用 HTTP/3 支持构建 PHP Curl 扩展

HTTP/3 标准本身仍处于“建议标准”状态,这比成为互联网标准晚了一个级别。虽然大多数主流浏览器已经支持 HTTP/3,但网络服务器之间可能还有其他硬件和软件(其中,最初支持 HTTP/3 的并不多)不支持 HTTP/3 或满足 HTTP/3 的基本要求,例如允许 UDP 流量到端口。

Curl 本身对 HTTP/3 的支持也被标记为实验性的。此外,Debian/Uubuntu 和 Fedora/REL 及其衍生产品中的 PHP Curl 扩展的预构建包都没有使用 HTTP/3 支持构建。

在 PHP Curl 扩展中启用 HTTP/3 支持需要使用 Curl 本身所依赖的必要库编译 libcurl,然后使用该 libcurl 编译 Curl 扩展。

不建议在生产环境中使用。

Curl 的 HTTP/3 文档提供了有关使用 HTTP/3 编译 Curl 的最新说明。Curl 可以是不同的加密和传输库,但根据 PHP 的多次测试,使用ngtcp2nghttp3WolfSSL 构建 Curl 产生了最好的结果。一些包含修补版本的 OpenSSL 的组合根本不起作用,并且在尝试执行请求时出错。

1. 构建依赖

构建工具比如 C 编译器,make 工具,和其他必要工具必需在系统中安装。

2. 使用 ngtcp2, nghttp3 和 WolfSSL 构建 Curl

Curl 文档中提供了最新的说明。

# Prepare working space
cd ~
mkdir curl
mkdir build
cd curl

# Build WolfSSL
git clone https://github.com/wolfSSL/wolfssl.git
cd wolfssl
autoreconf -fi
./configure --prefix=~/build/~/build/wolfssl --enable-quic --enable-session-ticket --enable-earlydata --enable-psk --enable-harden --enable-altcertchains
make
make install

# Build nghttp3
cd ..
git clone -b v1.0.0 https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -fi
./configure --prefix=~/build/nghttp3 --enable-lib-only
make
make install

# Build ngtcp2
cd ..
git clone -b v1.0.1 https://github.com/ngtcp2/ngtcp2
cd ngtcp2
autoreconf -fi
./configure PKG_CONFIG_PATH=~/build/wolfssl/lib/pkgconfig:~/build/nghttp3/lib/pkgconfig LDFLAGS="-Wl,-rpath,~/build/wolfssl/lib" --prefix=~/build/ngtcp2 --enable-lib-only --with-wolfssl
make
make install

# Build curl
cd ..
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
./configure --with-wolfssl=~/build/wolfssl --with-nghttp3=~/build/nghttp3 --with-ngtcp2=~/build/ngtcp2
make
make install

根据需要修改前缀 ~/build/wolfssl, ~/build/nghttp3, 和~/build/nghttp3

3. 使用新的 libcurl 构建 PHP Curl 扩展

因为上述在 Curl 中调用 make install,Curl 二进制及 libcurl 在系统上进行了安装。当 PHP 使用 Curl 扩展进行编译时,现在请使用 HTTP/3 支持的新 libcurl 构建。

请确保使用 --with-curl 配置 ./configure PHP。如果 Curl 不是在系统范围上进行安装(比如,不再 /urs/local 中),也可以在此自定目录。

以下编译 PHP 的快速指南:

sudo apt install build-essential autoconf libtool bison re2c
git clone https://github.com/php/php-src.git --branch=master
cd php-src
./buildconf
./configure --with-curl
make -j $(nproc)
sudo make install

总结

PHP 的 Curl 扩展可以使用 HTTP/3 的实验性支持进行编译。不幸的是,这需要编译 Curl 扩展,这对于依赖从操作系统包仓库中进行操作系统更新,以进行安全维护或漏洞修复有一定的挑战。

下面代码显示了如何在支持 HTTP/3 的系统上发起 HTTP/3 请求:

$ch = curl_init("https://php.watch/");
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
curl_exec($ch);
  • 如果远程服务器不支持 HTTP/3,它会尝试使用 HTTP/3 或者 HTTP/1.1。
  • 如果 Curl 没有 HTTP/3 支持,curl_setoptcurl_exec 调用都会失败。

下面代码中发起的的 HTTP/3 请求更有防御性。它检测 Curl 是否内置了 HTTP/3 支持,检测是否声明了 CURL_HTTP_VERSION_3 常量并允许 Curl 在 HTTP/3 不可用时,回退到不同的 HTTP 版本:

$ch = curl_init("https://php.watch/");

if (defined('CURL_VERSION_HTTP3') && defined('CURL_HTTP_VERSION_3') && curl_version()['features'] & CURL_VERSION_HTTP3) {
    curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
}

curl_exec($ch);

 

PHP