QUIC 是 Quick UDP Internet Connections 的缩写,谷歌发明的新传输协议。与 TCP 相比,QUIC 可以减少延迟。从表面上看,QUIC 非常类似于在 UDP 上实现的 TCP + TLS + HTTP/2。由于 TCP 是在操作系统内核和中间件固件中实现的,因此对 TCP 进行重大更改几乎是不可能的。但是,由于 QUIC 建立在 UDP 之上,因此没有这种限制。QUIC 可以实现可靠传输,而且相比于 TCP,它的流控功能在用户空间而不在内核空间,那么使用者就 不受限于 CUBIC 或是 BBR,而是可以自由选择,甚至根据应用场景自由调整优化。
QUIC 与现有 TCP + TLS + HTTP/2 方案相比,有以下几点主要特征:
- 利用缓存,显著减少连接建立时间
- 改善拥塞控制,拥塞控制从内核空间到用户空间
- 没有 head of line 阻塞的多路复用
- 前向纠错,减少重传
- 连接平滑迁移,网络状态的变更不会影响连接断线。
QUIC 在 UDP 之上,如果想要和 TCP/IP 体系类比,那么就是上图。QUIC 可以类比 TCP/IP 中的 TLS 一层。但是功能又不完全是 TLS ,还有一部分 HTTP/2 ,下面还包括一部分 TCP 的功能,比如 拥塞控制、丢包恢复、流量控制等特性。
本站笔者已经部署了 TLS 1.3,再次握手可以达到 0-RTT 了,速度已经比较快了,但是为何笔者还要再部署一套 QUIC 呢?
- HTTP + TLS 的首次握手还是需要花费多次 RTT,仍有优化空间
- 手机在弱网环境下,TCP head of line 阻塞和 TLS record HOF 加剧了网络恶化
- HTTPS 一套方案过于单一,如果部署多套链路,系统可用性更高
其实最重要的一点还是速度快。如果经常用 Google 服务的用户就会观察到,Google 目前主要业务都已经部署了 QUIC,并且支持了 4 个大版本,quic-44、quic-43、quic-39、quic-35(同时支持多个版本主要是为了兼容多个版本 chrome 浏览器,目前最新的 chrome 浏览器 68 - 70 都只有 quic-44,而低版本的 62 - 66 都只有 quic-39,为了各个版本的用户,也只能支持这么多版本了)。
另外还有一点,如果是 HTTP/2 的开发者,一定会知道,HTTP/2 and SPDY indicator 这个谷歌官方插件。如果只是普通的 HTTP/2 + TLS,这个插件会显示蓝色,并显示“ HTTP/2-enabled(h2) ”;但是如果开启了 QUIC,那么它会显示成炫酷的绿色,并显示“SPDY-enabled(http/2+quic/39)”,绿色表示网络通道更加畅通。
好了,说了这么多理由了,那接下来看看如何部署吧。
要想在浏览器上实现 QUIC ,有一些前置条件。由于现在好像只有 chrome 浏览器支持 QUIC 协议,所以下面的条件是针对 chrome 浏览器的。
当 Chrome 向之前从未发过请求的服务端发出请求时,它不知道对方是否支持 QUIC,因此先通过 TCP 发送第一个请求。服务器响应该请求以后,要发送 Alt-Svc HTTP 响应头告诉 chrome 它支持 QUIC。 (例如,响应头中 "alt-svc: quic=":443"; ma=2592000; v="44,43,39,35" 告诉 Chrome 服务端支持端口443上的QUIC,且支持的版本号是 44,43,39,35,max-age 为 2592000 秒)。 现在 Chrome 知道服务端支持 QUIC,于是尝试使用 QUIC 来进行下一个请求。发出请求后,Chrome 将采取 QUIC 和 TCP 竞争的方式与服务端建立连接。(建立这些连接,但是不发送请求)如果第一个请求通过 TCP 发出,TCP 赢得竞争,第二个请求将通过 TCP 发出。 在随后的某个时刻,QUIC 如果一旦连接成功,将来所有请求都将通过 QUIC 连接发送。
所以 QUIC 的协议发现过程是通过识别响应头中的特殊字段实现的。
Chrome 始终原生支持 QUIC,并且启用 QUIC 的服务器会一直支持 0-RTT 握手。当 Chrome 向之前使用过 QUIC 的服务器发出请求时,它还会与 TCP 进行竞争。 由于 Chrome 将能够进行 0-RTT 握手,因此 QUIC 还会立即获胜,并且继续在 QUIC 连接上发出请求。
如果 QUIC 握手失败(例如,如果UDP被阻止,或者服务器与 chrome 的 QUIC 版本不兼容),则 Chrome 会将 QUIC 标记为该主机已损坏 broken。任何正在进行的请求都将通过 TCP 重新发送。 虽然 QUIC 被标记为主机已损坏,所以不会立即尝试 QUIC 连接了。5分钟后,损坏的连接将过期的 QUIC 标记为“最近破坏”。当向服务器发出下一个请求时,Chrome 又将继续让 TCP 和 QUIC 进行竞争。由于QUIC“最近被破坏”,因此将禁用 0-RTT 握手。如果握手再次失败,则 QUIC 将在此次再次标记为该连接已损坏 10 分钟,将 QUIC 标记为已损坏的前一周期的 2 倍,如此往后都会不断的标记为 2 倍。如果握手成功,请求将通过 QUIC 发送,QUIC 将不再标记为“最近损坏”。
另外还有一点需要注意的是 QUIC 必须要同时部署在 TCP / UDP 443 端口上。没有理由,这算是规定。具体可以看这个帖子Why MUST a server use the same port for HTTP/QUIC?
知道这些以后,QUIC 的部署方案也就出来了。
由于 nginx 的生态比较完善,因为部署 QUIC 完全抛弃 nginx ,这有点本末倒置了。但是当前 nginx 还没有支持 QUIC,那我们怎么能提前体验 QUIC 呢?分两步来部署。
add_header alt-svc 'quic=":443"; ma=2592000; v="39"';
这一步比较简单,目的是为了告诉 chrome 浏览器当前服务器支持 QUIC。
整个 QUIC 协议比较复杂,想自己完全实现一套对笔者来说还比较困难。所以先看看开源实现有哪些:
这个是官方支持的。优点自然很多,Google 官方维护基本没有坑,随时可以跟随 chrome 更新到最新版本。不过编译 Chromium 比较麻烦,它有单独的一套编译工具。暂时不考虑这个方案。
从 chromium 剥离的一个 QUIC 协议部分,但是其 github 主页已宣布不再支持,仅作实验使用。不考虑这个方案。
goquic 封装了 libquic 的 go 语言封装,而 libquic 也是从 chromium 剥离的,好几年不维护了,仅支持到 quic-36, goquic 提供一个反向代理,测试发现由于 QUIC 版本太低,最新 chrome 浏览器已无法支持。不考虑这个方案。
quic-go 是完全用 go 写的 QUIC 协议栈,开发很活跃,已在 Caddy 中使用,MIT 许可,目前看是比较好的方案。
于是可以确定方案是最后一个,采用 caddy 来部署实现 QUIC。
部署方案如下:
nginx 还是继续使用,不过 nginx 只用来响应 TCP/443 端口,UDP/443 交给 caddy 来响应。nginx 返回响应头告诉 chrome 浏览器支持 QUIC,然后 caddy 作为反向代理 proxy 来转发。
caddy 这个项目本意并不是专门用来实现 QUIC 的,它是用来实现一个免签的 HTTPS web 服务器的。caddy 会自动续签证书。QUIC 只是它的一个附属功能(不过好像用它来实现 QUIC 的人更多🤣)。
如果直接在服务器上面运行 caddy ,会报一个错误:
Activating privacy features... done.
2018/07/22 14:03:15 listen tcp :443: bind: address already in use
原来 caddy 也会监听 TCP/443 ,所以要首先解决这个问题,我们只需要 caddy 监听 UDP/443,把 caddy 的 TCP/443 功能“屏蔽”掉。这里比较好的方法就是用 docker 来实现。因为 docker 容器的网段和宿主机的网段默认是隔离的。
那我们就新建一个 docker 吧。
FROM ubuntu:latest
LABEL maintainer="ydz@627@gmail.com"
RUN apt-get update
RUN set -x \
&& apt-get install curl -y \
&& curl https://getcaddy.com | bash -s personal && which caddy
如果不想再做了,可以直接 pull 一下笔者 docker pub 里面的这个镜像,REPOSITORY:halfrost/blog,TAG:caddy-0.0.1。
然后要新建一个 caddy 的配置文件
https://halfrost.com
gzip
tls /conf/chained.pem /conf/domain.key
proxy / http://127.0.0.1:2368 {
header_upstream Host {host}
header_upstream X-Real-IP {remote}
header_upstream X-Forwarded-For {remote}
header_upstream X-Forwarded-Proto {scheme}
}
log /caddy-conf/caddy_blog.log
errors /caddy-conf/caddy_errors.log
上面文件中具体的 HTTPS 的证书路径,还有 log 和 error 的路径根据个人喜好配置。
接下来就可以启动 docker 啦。
$ docker container run -d -p 443:443/udp --name halfrost-blog -v /www/ssl:/conf -v /www/caddy:/caddy-conf halfrost/blog:caddy-0.0.1 caddy -quic -conf /caddy-conf/caddy.conf
上面这个命令解释一下:
-p 就是映射的端口,把宿主机的 443/udp ,映射到 docker 的 443 端口
-name 就是给这个 docker 起一个名字,如果不设置,会随机生成一个名字。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
366d4b392ed0 halfrost/blog:caddy-0.0.1 "caddy -quic -conf..." 10 hours ago Up 10 hours 0.0.0.0:443->443/udp halfrost-blog
-v 是挂载一些目录,冒号前面是宿主机的目录,冒号后面是 docker 里面的目录。
最后的 -conf 加载宿主机的 conf 文件。
如果 docker 启动失败了,查看一下 docker 的 log,看一下启动失败的原因:
$ docker logs -f -t --tail 10 366d4b392ed0
至此,服务端的操作全部完成了。
先验证一下 caddy 是否在监听 UDP/443:
netstat -anp | grep "443"
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 9748/nginx: master
tcp 0 0 172.16.9.240:443 101.86.117.100:54518 TIME_WAIT -
tcp 0 0 172.16.9.240:443 124.90.178.236:33162 FIN_WAIT2 -
tcp 0 0 172.16.9.240:64854 140.205.230.3:443 TIME_WAIT -
udp6 0 0 :::443 :::* 5629/./caddy
可以看到确实在监听 UDP/443
再开启 chrome 的 QUIC 特性
在 chrome://flags/ 中找到 Experimental 中 QUIC protocol, 设置为Enabled. 重启浏览器生效。
在 chrome store 里面安装 HTTP/2 and SPDY indicator 插件。这个插件在前面提到过了,如果开启了 QUIC,这个插件会很明显的显示为绿色。如上图右上角,可以看见绿色的闪电⚡️,即代表已经开启了 QUIC。
在 chrome 的 develop tools 工具里面,打开 security 选项卡,在 connection 项里面会看到 QUIC,如果显示的是 QUIC,代表开启 QUIC。
打开 chrome://net-internals/#quic 页面
如果你看到了 QUIC sessins,则开启成功。
左侧 Alt-svc 里面会记录所有支持 QUIC 的相应头。
在上面的字典里,还会记录下次尝试的时间。如果之前出现了 broken,可以在这个页面清空这里的所有记录。清空方法是在这个页面点击右上角的小三角,里面有一项“Clear cache”,清空以后 chrome 在下次请求的时候就会立即尝试 QUIC 了。
更新:
新版的 Chrome 68 以上版本,net-internals 中没有笔者上面截图的页面了,这些工具都转到了 chrome://net-export/ 这个页面了。笔者写这篇文章的时候 Chrome 还是 67 的版本。
chrome://net-export/ 这个页面需要先把请求记录下来,存成 json 文件。
记录完成以后会出现下面这个页面:
然后在 https://netlog-viewer.appspot.com/ 这里解析 json 文件。解析出来以后就能看到左边的那些工具了。
至于如何清除 Alt-svc 的 cache,这个笔者还没有找到实时清除的方法,笔者现在都是在 dev-tool 里面点击清理 Network 中的 Disable cache 和 Application 中的 Clear storage。可能清除 Alt-svc 的 cache 目前还没有实现出来,如果以后笔者发现方法了,还会再回来更新这段话,如果读者看到这里知道如何清除 Alt-svc 中的 Alternate Service Mappings,也麻烦留言评论分享一下。不过不清除 Alt-svc 中的 cache 对开启 QUIC 影响不大,只是在 broken 以后需要多等待 5 分钟的过期时间(如果能立即清除 cache 就不用等这个 5 分钟了)。
最后的办法就是可以用 wireshark 进行抓包,观察发送的包是不是都是 GQUIC 的,如果是,就代表开启成功。
看到这里读者可能觉得本文就结束了。确实,进行到这里 QUIC 已经开启成功,并且验证完毕了。但是为何还有这一节呢?笔者最后的方案其实不是上述描述的,因为中间出现了一些蛋疼的情景,导致最终没有选用 docker 的方案。当然笔者的情况比较特殊,这里分享一下,仅仅是记录一下解决问题的过程。
笔者的浏览器版本是最新的 68,并且也安装了金丝雀,版本是 70 。在部署完 QUIC 以后,疯狂刷新,死活见不到绿色的小闪电⚡️。无奈最终只能采取抓包的方式查问题。抓包发现,chrome 68 的版本默认带的是 quic-43 的实现,在握手的时候会失败。于是就不会开启 QUIC 了。
上图可以看见握手失败了。
于是笔者为了验证 QUIC 开启成功,下载了一个老的版本 65。但是却出现了另外一个问题,打开页面出现 “502 Bad Gateway”。并且在 chrome://net-internals/#alt-svc 页面看见了 broken。
出现 broken 就说明了 UDP 通道不通。
UDP 通道不通可能是几个原因:阿里云或者运营商某个环节屏蔽了 UDP 包;服务端 QUIC 服务挂了,或者博客端口挂了。
先验证一下服务端 caddy 有没有挂,打印了一下 docker log,没有发现 caddy 崩溃。查看了 ghost ,也没有发现崩溃。
再检验一下 UDP 包是否没有发送过来。
笔者服务器是 centOS 的,用 netcat 来检测 UDP 通道。
$ yum install nc
$ nc --udp --listen 6111
在本地连接服务器:
$ nc -u <server ip> 6111
本地发送一串字符串,如果服务端也显示了相同的字符串,代表中间没有防火墙。经过验证,整个 UDP 通道是通的。
注意这里,如果对方没有收到包,不一定能说明 UDP 通道是不通的。因为如果对端开启了防火墙,防火墙把包 DROP 了,那么是收不到 ICMP 端口不可达消息的,那么使用 nc 命令就会发现实际不通的端口是通的。仔细想想 UDP 的原理就清楚了,UDP 不像 TCP 一样需要 ACK,所以过一段时间没收到端口不可达,UDP 就认为端口是通的,但是实际上 UDP 数据被防火墙 DROP 了。
这个时候笔者有点懵。静下心来再仔细分析一下 caddy 的日志:
11/Aug/2018:13:12:24 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:24 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:24 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:24 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:24 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:25 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:26 +0000 [ERROR 502 /serviceworker-v1.js] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:26 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:12:27 +0000 [ERROR 502 /serviceworker-v1.js] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:12 +0000 [ERROR 502 /serviceworker-v1.js.map] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:12 +0000 [ERROR 502 /assets/dist/sw-toolbox.js.map] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:14 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:16 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:17 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:27 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:29 +0000 [ERROR 502 /rss/] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:30 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
11/Aug/2018:13:14:31 +0000 [ERROR 502 /] dial tcp 127.0.0.1:2368: connect: connection refused
从日志上也看不出具体是什么错误。一直报 502,连接拒绝。
登录到服务器,模拟一下请求试试:
$ curl -X GET 127.0.0.1:2368
输出了博客首页 index.html ,说明 ghost 的 node 服务也没有挂啊,一切正常。那究竟是什么问题导致 502 错误呢?
我们是把宿主机的 UDP/443 端口转发到 docker 内的 443 端口,然后反向代理请求到 127.0.0.1:2368 端口,目前还需要排查的一点就是在 docker 内部是否能代理成功呢?
$ sudo docker exec -it 775c7c9ee1e1 /bin/bash
进入到 docker 内部了。
/# curl -X GET http://127.0.0.1:2368
curl: (7) Failed to connect to 127.0.0.1 port 2368: Connection refused
这里报错了,看到这里,就可以定位到问题的原因了。之所以 caddy 会报 502 错误,原因是因为 caddy 在 docker 内部无法访问到宿主机的 127.0.0.1:2368 端口。
在 docker 内部再验证一下, /etc/hosts/ 文件里面的内容
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
192.168.0.4 366d4b392ed0
可以看到 docker 内部也有一个 127.0.0.1,所以之前代理写的 127.0.0.1 并没有转发到宿主机上,而是被这里的 hosts 文件拦截在本地了。那如何从 docker 内部访问到宿主机呢?宿主机的 IP 是什么呢?
继续在 docker 内部执行 :
/# apt-get install iproute2
/# apt-get install net-tools
/# netstat -nr | grep '^0\.0\.0\.0' | awk '{print $2}'
192.168.0.1
输出 192.168.0.1 代表在 docker 这个网段的网关是 192.168.0.1。那么我们可以通过这个 IP 访问到外面宿主机的服务么?答案是可以的。
那难道每次我们都需要进入到 docker 里面执行这样一句话查看宿主机的 IP 么?答案当然是否定的。有两种方法可以解决这个 IP 问题。
$ HOST_IP=`ip -4 addr show scope global dev docker0 | grep inet | awk '{print \$2}' | cut -d / -f 1`
$ docker run --add-host outside:$HOST_IP --name busybox -it busybox /bin/sh
/ # cat /etc/hosts
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.1 outside <---- THIS ONE!
172.17.0.3 a8300156a695
在 dockerfile 最后加入一行:
RUN ip -4 route list match 0/0 | awk '{print $3 "host.docker.internal"}' >> /etc/hosts
一般 docker 生成有 3 种网络类型:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a6211ef82668 bridge bridge local
bb6cb9901ca2 host host local
c25d8f9f4ae3 none null local
默认是 bridge 的,所以可以通过 docker 所在网段的网关访问到宿主机。
$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "a6211ef82668f63c117d63fdc666a79dea8f3868bdc80434b9f00a05c9ae9d9b",
"Created": "2018-07-26T22:24:42.881531018+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "192.168.0.0/20",
"Gateway": "192.168.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"Containers": {
"0592382ce0319a58be1eaa07a612ee75092b192337871632b9b2af9baa8f2233": {
"Name": "serene_brahmagupta",
"EndpointID": "97349fed924efb4d49173943e0daff0459f0d8df373c639710c81f9efa0f7965",
"MacAddress": "02:42:c0:a8:00:02",
"IPv4Address": "192.168.0.2/20",
"IPv6Address": ""
},
"366d4b392ed071186c6442228442a929b20656242b75021beab6eaa7afbc352b": {
"Name": "halfrost-blog",
"EndpointID": "13c186ae9c460a87280593c3083f644bdbcd718e993b4ba3ee5a8efc66ae82c8",
"MacAddress": "02:42:c0:a8:00:04",
"IPv4Address": "192.168.0.4/20",
"IPv6Address": ""
},
"66b36b776c6f390dfb81f055e38e276cbab9e97fe1aa7736e50b08c04e4d9635": {
"Name": "infallible_saha",
"EndpointID": "18ea99dc8e5add8867d6933717b682701f01452439e699274a8b71a053c673df",
"MacAddress": "02:42:c0:a8:00:03",
"IPv4Address": "192.168.0.3/20",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
笔者把代理改成了 192.168.0.1:2368,依旧连接拒绝。代表还是访问方式不对。通过查询 ghost 文档,发现 2368 端口没有对外暴露,默认只能在宿主机上访问。所以不同网段之间不能访问到 2368 这个 node 服务的。
排查到这里,有 2 种选择,选择一,更改 ghost 的服务端口,改到 0.0.0.0:2368,不过这样改,ghost 里面很多配置都要更改。选择二,让 caddy docker 和宿主机在同一个网段。
笔者选择了第二种方案。
让 docker 和 宿主机在同一个网段可以通过启动的时候指定 --network=host
,这样 docker 和宿主机就在同一个网段了。不过这样又会出现新的问题。之前映射关系就不对了。因为这样 caddy 也会监听宿主机的 TCP/443 端口,如此一来我们之前用 docker 摆脱监听同一个端口的目的就没有意义了。
到这里,笔者还是决定放弃 docker 的方案。那只有改 caddy 源码的方案了。
下载 caddy 源码,在 caddy/caddyhttp/httpserver/server.go 文件中,找到 Listen 这个函数里面,把其中一行注释掉,换成:
// ln, err := net.Listen("tcp", s.Server.Addr)
ln, err := net.Listen("tcp", "127.0.0.1:61234")
// 随便写一个大一点的没有被占用的端口就可以
就是把原本的 443 地址换成一个没用的地址,然后重新编译,
$ cd $GOPATH/src/github.com/mholt/caddy/caddy
$ go run build.go --goos=linux --goarch=amd64
顺利编译完成以后,就能在已经开了 nginx 的情况下正常启动 Caddy 了。为了能让Caddy变成一个守护进程运行在后台,可以使用 nohup 命令:
$ nohup sudo ./caddy -quic -conf ./conf >/dev/null 2>&1 &
至此,笔者的 QUIC “完美”的部署好了。
Q: 我刷新了好几次,怎么还是没有你博客上绿色的小闪电⚡️? A: 首先要看你的 chrome 版本是不是 62-65 之间,如果高于这个版本或者低于这个版本,都会导致 QUIC 握手失败,进而无法进行 QUIC 通讯。因为 62-65 版本之间支持了是 quic-39,而目前 caddy 最新只支持到了 quic-39。
其次还需要看看你本地是否开启了类似 surge 全局翻墙软件,如果开启了类似这些软件,一般都会系统代理你本机的所有请求,那么所有的请求都是来自 127.0.0.1:XXXX,这样也不会触发 UDP 的请求了。所以要关闭 Surge “设置为系统代理” 这个设置。
最后,可以看看 chrome://net-internals/#alt-svc 页面里面有 broken 的情况,如果有,可以清除了以后立即刷新再看看。
Q: caddy 为何不支持最新版的 quic 协议? A: 这个问题可以看它的 issue,作者在今天 1-4 月,每个月都会更新一个新版本,并且持续的更近 quic 协议的更新。直到 5 月以后,就再也没有发布新版了。作者说要先重构 2 个引入的库的方式,不然以后越开发越乱。一直等到现在 quic-44 了,caddy 还没有更新上来。只能再等等了。
Q: QUIC 如何调试? A: 这个问题问的好,笔者也考虑查看 QUIC 里面加密包的内容。不过目前还没有方式可以查看。TLS 可以通过保存协商密钥来查看加密内容,但是 QUIC 目前好像还不行(至少笔者还没有在网上搜到相关的内容)
wireshark 抓包目前只能看到前期握手的明文,握手以后的包都是加密内容。所以调试 QUIC 的问题还有待进一步研究。
当前 caddy 最新只能支持到 quic-39,并且也不能同时兼容多个版本。最新的 chrome 已经支持到最新的 quic-44 了,只能静静等待 caddy 更新了。
经过一次更新,笔者的博客已经支持 quic-44、quic-43、quic-39 三个主流版本了。欢迎大家体验。
另外,在移动互联网时代,如果客户端也想享受到 QUIC 带来的优势的话,光依靠 Chrome 浏览器可不行,谷歌提供了 cronet 这个库,它是 chromium 网络协议栈的封装,可以用于 Android 和 iOS 平台,可以很方便集成到手机 APP 中。利用这个库,可以和服务端进行 QUIC 的通讯。
最后可以推荐大家看新浪微博今年 2018 Qcon 上分享的在 QUIC 上的一些实践心得。
关于 QUIC 协议的更详尽的分析,笔者会在接下来的文章中细致讲解。
Reference:
QUIC 官方介绍
Web服务器快速启用QUIC协议
reading-and-annotate-quic
怎么把网站升级到QUIC以及QUIC特性分析
本站开启支持 QUIC 的方法与配置
GitHub Repo:Halfrost-Field
Follow: halfrost · GitHub
Source: https://halfrost.com/quic_start/