1. 为什么是 QUIC?从 TCP 的瓶颈说起
手机在 Wi-Fi 和 4G/5G 之间切换(即“切网”)导致 TCP 连接断开,是移动互联网开发中的经典痛点。
TCP 连接是基于源 IP、源端口、目的 IP、目的端口这“四元组”来标识的。当你从 Wi-Fi 切换到 5G 时,手机的 IP 地址发生了变化,旧的四元组立即失效,TCP 必须重新进行三次握手建立新连接,正在传输的数据(如视频缓冲、下载)就会中断。
QUIC 引入了 Connection ID 的概念。它不依赖于底层 IP 地址。只要 CID 不变,即便你的 IP 从 A 变成了 B,服务端依然能通过 CID 认出:“噢,你还是刚才那个客户端!”。这样对于业务层完全无感知,数据传输无缝继续。这就是所谓的 “连接迁移 (Connection Migration)”。
除了切网,QUIC 在弱网(丢包率高、延迟高)下更强的原因在于:
- 改进的拥塞控制: QUIC 在应用层实现,可以更激进地进行丢包恢复。
- 无队头阻塞: 在 TCP 中,丢一个包全家等死;在 QUIC 中,你刷朋友圈的图丢了一个包,不会影响你接收聊天消息的流。
2. go 库quic-go 简介
这是Go的库,在 Go 语言世界里,quic-go 是事实上的标准实现。它不仅完整实现了 IETF QUIC 协议,还提供了类标准库 net 的简洁接口。
- Connection (连接): 代表两个端点之间的 UDP 隧道。
- Stream (流): 连接内部的逻辑通道,双向且独立。
3. 证书准备:使用 OpenSSL 生成 Ed25519 证书
为了极致的性能与安全,弃用了传统的 RSA,选择 Ed25519 算法。它的签名速度更快,密钥更短。
执行以下命令生成自签名证书:
# 生成私钥
openssl genpkey -algorithm ed25519 -out server.key
# 生成自签名证书
# /C=国家(CN) /ST=省份 /L=城市 /O=公司名 /OU=部门名 /CN=你的域名
openssl req -new -x509 -key server.key -out server.crt -days 365 \
-subj "/C=CN/ST=Shanghai/L=Shanghai/O=MyTechCo/OU=IT/CN=yourdomain.com"
# 获取证书哈希
openssl x509 -in server.crt -noout -sha256 -fingerprint
# 获取公钥哈希
openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -hex
注:这里的 /CN=yourdomain.com 很关键,后续客户端校验域名时会用到。
4. 服务端实战:构建 Echo Server
这里使用一个回声服务作为示例。服务端的核心逻辑是:监听端口 -> 接受连接 -> 接受流 -> 读写数据。
// 核心逻辑演示
listener, _ := quic.ListenAddr("127.0.0.1:4242", tlsConfig, nil)
for {
conn, _ := listener.Accept(ctx)
go func(c quic.Conn) {
for {
stream, _ := c.AcceptStream(ctx)
// 经典的 Echo:将读取到的数据原样写回
go io.Copy(stream, stream)
}
}(conn)
}
关键点: AcceptStream 是阻塞的,一个 Connection 可以开启无数个 Stream。
5. 客户端实战:自定义证书校验与公钥哈希
在实际开发或测试中,我们常使用自签名证书。直接设置 InsecureSkipVerify: true 会导致中间人攻击,更优雅的做法是校验证书公钥的哈希值 (Pinning)。
我们需要跳过默认校验: 设置 InsecureSkipVerify: true。然后进行深度检查: 利用 tls.Config 的 VerifyConnection 回调。
tlsConfig := &tls.Config{
InsecureSkipVerify: true, // 必须跳过内置校验,否则自签名证书会报错
VerifyConnection: func(cs tls.ConnectionState) error {
// 1. 拿到对端发来的第一个证书
cert := cs.PeerCertificates[0]
// 2. 计算 SHA256
hash := sha256.Sum256(cert.Raw)
actualHash := hex.EncodeToString(hash[:])
// 3. 与你预埋的期望哈希比对
expectHash := "服务端给出的哈希串"
if actualHash != expectHash {
return fmt.Errorf("警报!证书指纹不匹配,可能存在中间人攻击")
}
return nil
},
}
虽然 InsecureSkipVerify 设置为 true 听起来很危险,但通过手动 VerifyConnection 校验哈希,安全性反而比信任系统根证书更高,因为这只允许你指定的特定证书通过。
另外 quic.DialAddr 时传入的 serverName 必须和 openssl 命令中的 /CN= 填入的域名保持一致。
6. 总结:QUIC 的生命周期
通过这个 Echo 服务,我们可以清晰地梳理出 QUIC 的工作流:
- 握手阶段: 客户端发起 Dial,在 1 个 RTT 内完成 UDP 联通与 TLS 1.3 密钥交换。
- 多路复用: 双方可以在同一个 Connection 上 OpenStream。每个 Stream 都有自己的偏移量控制,互不干扰。
- 可靠性保障: 虽然底层是 UDP,但 quic-go 在应用层实现了丢包重传和流量控制。
结语: QUIC 不仅仅是加速版的 HTTPS,它为实时音视频、游戏底层协议提供了一个高效、安全且可控的基石。