# nginx 与 HTTP/3

# 开始之前

这是本站第三篇关于 HTTP/3 (或者说 QUIC) 的文章,从 caddy 到 nginx-quic 再到现在的 nginx 1.25,它终于正式支持了 HTTP/3 协议。

当然,openssl 还不支持且 2024 年前完成 QUIC 支持希望渺茫,因此本文使用 boringssl 代替 openssl。 quictlslibressl 同样可以完成此工作,未来可能会提供相关教程(挖坑)

本文带有一定的广告内容,如果无法接受,您可以点击左侧的目录跳过相关部分。

# 为什么选择 HTTP/3

这里有一个可能不太恰当的比喻:

一天中午,你选择到 HTTP 街用餐,在你面前有两家餐厅:HTTP/2 和 HTTP/3 餐厅。

第一天你选择了 HTTP/2 餐厅进入,你点了许多美食和一个三明治。HTTP/2 餐厅的后厨可以同时制作多道菜,因此你认为你很快就能吃到美食。然后 HTTP/2 餐厅的
服务员告诉你:“三明治出了点问题,后厨没法上菜。” 然后就出现了一个问题,根据 HTTP/2 餐厅的规则,在这道菜上菜之前,后面的菜不能上菜,即使它已经被制作好了。
你花了很多时间来和服务员沟通,以确定 “三明治是不是真的出问题了” 和 “下一道菜什么时候上”。但这个三明治阻塞住了整个队列,因此你只能等后厨花费一段时间来处理三明治问题。

第二天你选择了 HTTP/3 餐厅,在这里,你只需要扫描准备好的二维码,然后点菜,随后等待菜品即可。和昨天一样,三明治再次堵塞了队列,但是这次 HTTP/3 餐厅可以选择不按顺序上菜。
后厨很快就准备好了其他菜品,因此你并没有等待多久,而且消息已经被发送到了你的手机上,你并不需要和服务员花费时间沟通。

在上述例子中,“三明治” 代表了一个传输失败的数据包,而 HTTP2 虽然可以进行多路复用 (即同时制作多道菜品),但如果数据包出现问题,仍可能阻塞整个队列。并且 TCP 的握手流程相当繁琐,
需要花费很多时间来解决重发问题。而 HTTP3 则可以选择先发送什么后发送什么,即使 “三明治” 出了问题,也不会导致队列被整体阻塞,而且 UDP 在此方面效率较高,至少比 HTTP2 餐厅的服务员们要高多了。

一个更细致的解释

这个比喻用一个生动的场景来解释了 HTTP/2 和 HTTP/3 之间的主要区别。HTTP/2 和 HTTP/3 都是基于 TCP 和 UDP 的协议,它们用于在网络上传输数据。HTTP/2 使用 TCP,而 HTTP/3 使用 UDP。TCP 和 UDP 的区别在于,TCP 是一种可靠的协议,它保证了数据包的有序和完整的到达,但是如果数据包丢失或损坏,它会导致传输的延迟和阻塞。UDP 是一种不可靠的协议,它不保证数据包的有序和完整的到达,但是它可以更快地传输数据,而且不会因为数据包的丢失或损坏而阻塞整个流。

因此,HTTP/2 餐厅的规则类似于 TCP 的机制,它要求所有的菜品按照点菜的顺序上桌,如果有任何一道菜出了问题,就会影响后面的菜品,即使它们已经准备好了。这就是所谓的头部阻塞,它会降低传输的效率和用户的体验。HTTP/3 餐厅的规则类似于 UDP 的机制,它允许菜品按照任意的顺序上桌,如果有任何一道菜出了问题,也不会影响其他的菜品,它们可以继续被送到客人的手中。这就是所谓的无头部阻塞,它会提高传输的效率和用户的体验。

这个比喻也展示了 HTTP/2 和 HTTP/3 的另一个区别,就是多路复用多流的概念。多路复用是指在一个 TCP 连接上同时传输多个请求和响应,而多流是指在一个 UDP 连接上同时传输多个独立的数据流。HTTP/2 使用多路复用,而 HTTP/3 使用多流。多路复用和多流的区别在于,多路复用是在一个连接上共享资源,而多流是在一个连接上分配资源。多路复用的优点是可以减少连接的数量和开销,但是缺点是如果一个请求或响应出了问题,就会影响整个连接上的其他请求或响应。多流的优点是可以隔离不同的数据流,使它们互不干扰,但是缺点是需要更多的连接和开销。

因此,HTTP/2 餐厅的后厨类似于多路复用的机制,它可以同时制作多道菜,但是如果有一道菜出了问题,就会影响其他的菜。HTTP/3 餐厅的二维码类似于多流的机制,它可以同时传输多个数据流,但是如果有一个数据流出了问题,也不会影响其他的数据流。

注:可能存在问题,因为字数太多懒得看第二遍(逃

# 使用 Nginx-quic

# 前提条件

本教程需要一个基于 ubuntu 22.04 (可以更高) 的服务器,并需要宝塔面板。如果你还没有的话,AWS 的 EC2 就是一个好选择:
现在注册亚马逊云科技账户可以享受 12 个月免费套餐,其中包括 Amazon EC2 云服务器、S3 云储存、Cloudfront CDN 等多种热门产品。
为节省时间,此处省略注册账号的过程,仅保留新建实例的过程:
首先在控制台点击启动实例:

随后输入相关内容,实例选择 t2.micro ,系统选择 ubuntu 22.04LTS ,随后创建密钥对:

根据需求选择,一般建议使用 pem 格式,提前选择放行 HTTPS 端口,节省时间:

一切完成之后,启动实例,等待创建完成后使用 SSH 连接到终端即可。
另外建议开启 IPV6,操作如下:
进入此 EC2 对应的 VPC,选择 编辑CIDR ,随后添加新的 IPV6 CIDR 块,选择 Amazon 提供的 IPv6 CIDR 块 ,随后返回。

进入此 EC2 对应的子网,右键 编辑 IPV6 CIDR ,添加即可。
然后进入路由表,为 ::/0 添加一条和 0.0.0.0/0 一致的路由。
随后进入 EC2 设置 ,联网 -> 管理 IP 设置,分配新的 IPV6 地址。
现在,这台 EC2 实例已经支持 IPV6 了。

下文均可在此 EC2 实例的 SSH 终端内进行。
本文全程基于 root 权限进行。

# 安装依赖

在终端运行如下指令:

apt install build-essential ca-certificates zlib1g-dev libpcre3 libpcre3-dev tar unzip libssl-dev wget curl git cmake ninja-build golang
# 需要启用源代码软件源 (deb-src)
apt-get build-dep nginx

# 编译 ssl 库

# 编译 boringssl

下列指令需要 gcc 支持 C++14 或更高标准,建议升级为 gcc9 以上版本

git clone --depth=1 https://github.com/google/boringssl.git 
# github 速度问题自行解决
cd boringssl
mkdir build
cd build
cmake -GNinja ..
ninja
cd ../..

如果出现了网络问题,执行如下命令:

go env -w GOPROXY=https://goproxy.cn,direct

# 安装 nginx-brotli (可选)

brotli 是一种由 google 提供的新型压缩方式,比传统的 gzip 在体积方面要小很多[1]
安装步骤如下,为保证路径合理,在 /www/server 下进行:

cd /www/server
git clone https://github.com/google/ngx_brotli.git
cd ngx_brotli
# vim .gitmodules
# git submodule sync
git submodule update --init

如果 clone 时使用了 github 加速,请解除第 4 行注释并修改此文件内的 url 为加速后链接,随后执行第 5 行后再进行下一步。

# 获取和配置 nginx

截止本文写就时,Nginx 最新版为 1.25.3,因此下列链接使用此版本:

wget https://nginx.org/download/nginx-1.25.3.tar.gz
tar -xzvf nginx-1.25.3.tar.gz

随后运行:

nginx -V

输出内容应该类似于:

nginx version: nginx/1.25.3
built by gcc 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) 
built with OpenSSL 1.1.1q
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/www/server/nginx --add-module=/www/server/nginx/src/ngx_devel_kit --add-module=/www/server/nginx/src/lua_nginx_module --add-module=/www/server/nginx/src/ngx_cache_purge --with-pcre=pcre-8.43 --with-http_v2_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --add-module=/www/server/nginx/src/ngx_http_substitutions_filter_module-master --with-ld-opt=-Wl,-E --with-cc-opt=-Wno-error --with-ld-opt=-ljemalloc --with-http_dav_module --add-module=/www/server/nginx/src/nginx-dav-ext-module --with-openssl=/www/server/nginx/src/openssl

此时,删除 --with-openssl=... ,并添加 --with-http_v3_module--with-cc-opt=-I../boringssl/include --with-ld-opt='-L../boringssl/build/ssl -L../boringssl/build/crypto'
修改 --with-pcre=pcre-8.43--with-pcre=/www/server/nginx/src/pcre-8.43

注意 boringssl 路径,编译参数和 nginx-quic 有不同,请勿直接沿用

如果你在上文中配置了 brotli,再添加 --add-module=/www/server/ngx_brotli 在尾部
现版本 OSCP 装订已可正常使用,无需注释相关配置

# 编译 nginx

部分环境编译时可能出现警告,如果出现了建议检查 Boringssl 路径和依赖。
均确认无误请修改 objs/Makefile 中的第三行,删除 -Werror 参数

nginx 对应目录下运行如下指令:

./configure # 使用你的编译参数替换这段 (包括 #号)
make

如果成功结束则代表运行成功,此时可以替换 nginx 了

# 替换 nginx

mv /www/server/nginx/sbin/nginx{,.bak} # 备份原本的 Nginx 文件
cp objs/nginx /www/server/nginx/sbin
make upgrade

如果 make upgrade 未出现报错,则升级成功。

# 配置 HTTP/3

# 配置服务器的 UDP 443 端口

多数 ubuntu 系统使用 ufw 作为防火墙,运行下列指令放行:

ufw allow 443/udp

或者使用宝塔系统防火墙放行,默认显示 443udp/tcp ,实际上 udp 未放行,修改后保存即可

# 修改安全组

这里以 AWS 为例,需要在服务器安全组放行 UDP 443:
通过 EC2 下方的安全 -> 安全组,然后添加入站规则:

和图片一样添加 443 端口的 UDP 入站即可,IPV6 需要额外添加一条入站。
现在服务器的 QUIC 已经可以正常运行了

# 配置 HTTP/3

HTTP/3 需要 SSL 证书才能启用,若为本地或测试环境请自签证书

在宝塔中调整对应网站配置,在 listen 443 ssl http2; 后加入:

listen 443 quic reuseport;
add_header Alt-Svc 'h3=":443"; ma=86400';

随后开启 0-RTT 握手和 OSCP 装订:

ssl_early_data on;
ssl_stapling on;
ssl_stapling_verify on;

ssl_early_data 开启了 QUIC 和 TLS 1.3 的 0-RTT 握手机制,如果网站涉及反代 (或动态网页),请确保你的应用可以防御重放攻击[2].

# 测试

使用 firefox 或者 chrome (需调整 flags) 访问对应网页,开发者工具中如果可以看见协议为 h3 则成功。


  1. brotli github ↩︎

  2. 详细介绍 RFC 8446(即 TLS 1.3) ↩︎