本站开始支持 QUIC

一. QUIC 是什么

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 的功能,比如 拥塞控制、丢包恢复、流量控制等特性。

二. 为什么要部署 QUIC

本站笔者已经部署了 TLS 1.3,再次握手可以达到 0-RTT 了,速度已经比较快了,但是为何笔者还要再部署一套 QUIC 呢?

  1. HTTP + TLS 的首次握手还是需要花费多次 RTT,仍有优化空间
  2. 手机在弱网环境下,TCP head of line 阻塞和 TLS record HOF 加剧了网络恶化
  3. 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 前置条件

要想在浏览器上实现 QUIC ,有一些前置条件。由于现在好像只有 chrome 浏览器支持 QUIC 协议,所以下面的条件是针对 chrome 浏览器的。

1. 首次连接

当 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 的协议发现过程是通过识别响应头中的特殊字段实现的

2. 后续连接

Chrome 始终原生支持 QUIC,并且启用 QUIC 的服务器会一直支持 0-RTT 握手。当 Chrome 向之前使用过 QUIC 的服务器发出请求时,它还会与 TCP 进行竞争。 由于 Chrome 将能够进行 0-RTT 握手,因此 QUIC 还会立即获胜,并且继续在 QUIC 连接上发出请求。

3. 连接失败

如果 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 的部署方案也就出来了。

四. 部署 QUIC 方案

由于 nginx 的生态比较完善,因为部署 QUIC 完全抛弃 nginx ,这有点本末倒置了。但是当前 nginx 还没有支持 QUIC,那我们怎么能提前体验 QUIC 呢?分两步来部署。

(一). nginx 配置响应头

add_header alt-svc 'quic=":443"; ma=2592000; v="39"';

这一步比较简单,目的是为了告诉 chrome 浏览器当前服务器支持 QUIC。

(二). 实现 QUIC 协议

整个 QUIC 协议比较复杂,想自己完全实现一套对笔者来说还比较困难。所以先看看开源实现有哪些:

1. Chromium

这个是官方支持的。优点自然很多,Google 官方维护基本没有坑,随时可以跟随 chrome 更新到最新版本。不过编译 Chromium 比较麻烦,它有单独的一套编译工具。暂时不考虑这个方案。

2. proto-quic

从 chromium 剥离的一个 QUIC 协议部分,但是其 github 主页已宣布不再支持,仅作实验使用。不考虑这个方案。

3. goquic

goquic 封装了 libquic 的 go 语言封装,而 libquic 也是从 chromium 剥离的,好几年不维护了,仅支持到 quic-36, goquic 提供一个反向代理,测试发现由于 QUIC 版本太低,最新 chrome 浏览器已无法支持。不考虑这个方案。

4. quic-go

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

至此,服务端的操作全部完成了。

六. 验证 QUIC

1. 验证端口

先验证一下 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

2. chrome 开启 QUIC 特性

再开启 chrome 的 QUIC 特性

在 chrome://flags/ 中找到 Experimental 中 QUIC protocol, 设置为Enabled. 重启浏览器生效。

3. 安装 HTTP/2 and SPDY indicator 插件

在 chrome store 里面安装 HTTP/2 and SPDY indicator 插件。这个插件在前面提到过了,如果开启了 QUIC,这个插件会很明显的显示为绿色。如上图右上角,可以看见绿色的闪电⚡️,即代表已经开启了 QUIC。

4. chrome developer tools

在 chrome 的 develop tools 工具里面,打开 security 选项卡,在 connection 项里面会看到 QUIC,如果显示的是 QUIC,代表开启 QUIC。

5. chrome net-internals

打开 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 分钟了)。

6. wireshark 抓包

最后的办法就是可以用 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 问题。

1. --add-host 命令

$ 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

2. 修改 dockerfile

在 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

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/