用 Rust 从零开始写 QUIC:写在刚开始
为什么要做这件事情
记录与自我学习
我一直有想写技术博客的冲动,因为每次看到有意思的技术细节,就很想和人分享,或者说至少是记录下来,但我一直没有付诸于行动,大概是好游戏太多了,时间不够用。现在,我突发奇想的这个项目(从零开始用 Rust 实现 QUIC)至少对我来说挺有吸引力的,就像是打一款新的 3A 游戏一样,让我比较有动力去完成它,希望这会是我个人博客一个好的开始。
另外,在我刚开始工作的时候,就有人告诉我,多看感兴趣项目的代码,然后尝试自己实现一遍,然后再重构优化,是可以帮助我成为更好的程序员。在多年的工作之后,我对这句话有了更多的理解。这么做最直观的好处,就是对项目细节理解更深了,毕竟自己动手实现,很多隐藏的细节,都会浮出水面。我很佩服的一个程序员章亦春也分享过手抄代码的学习方式,应该也是类似的效果,毕竟纸上得来终觉浅,光看不练肯定是不够的。
而细节强化后的好处,也是非常明显的,在处理线上故障或者客户紧急工单,或者平时日常开发功能的时候,这些细节的理解会帮助我们快速做出合理的决策,不断缩小问题的范围,查明根因,或者是写出来的功能会更健壮。举个例子,像福尔摩斯,他破案速度快,很大程度在于他的观察能力超乎常人,能够看到别人观察不到的地方,如果我们对技术细节的把控更细微,也可以获取更多信息输入,说不定有朝一日也会成为别人眼中的福尔摩斯 😂。
和 QUIC 的渊源
依稀记得在我 18年刚毕业的时候,我隔壁组的同事就在尝试将 QUIC 协议引入到我们自研的 NGINX 中来,那个时候我还完全没有听过 QUIC,只是在晚上加班的时候,偶尔听到了他们的讨论声,听起来感觉不明觉厉。现在看那个时候,各种 QUIC 开源库还非常少,也不够稳定,想在 NGINX 实现 QUIC,确实是一个很大的挑战。
大概在两年前,我换了一家新公司,首个项目就是尝试引入 QUIC 协议来提升我们的产品质量,我才正式开始接触 QUIC。当时,我反复阅读了 QUIC 的 rfc 以及当时 ngtcp2,nginx-quic,msquic 的相关代码,为了能够理解 QUIC 的实现细节,也反复调试相关开源代码,很多次感叹 QUIC 设计的巧妙,觉得 QUIC 真的是非常棒的协议。但是,随着时间流逝,我突然发现我开始遗忘了很多当时反复理解的细节,这让我有点烦恼。
所以,我最近突然想尝试实现一个 QUIC 协议栈,以及一个 QUIC 客户端,并且通过博客的方式,来记录下自己对 QUIC 的理解。这里肯定是会存在理解不到位的地方,但是我觉得记录下这些,后续随着自己理解的深入,来一一修正,或者也许有其他好心人在看了博客之后,不忍心我一直陷于错误的认知,来告知我理解有问题的地方,这都是我写这系列博客的动力所在。
希望能更熟练的使用 Rust
Rust 的优势我这边就不再赘述了,有太多的资料了。作为一个 C 程序员,我在刚开始尝试 Rust 的时候,就喜欢上了它。考虑到 c c++ 项目五花八门的构筑方式,还有躲不开的内存安全问题,每次写完代码,只能依赖 sanitizer 来让自己安心(但是总有测试不到的地方),编译时确保内存安全无疑是让人放心很多。另外就是 Rust 的写代码的体验,也比 c 代码好很多,所以搞得我每次写 c 代码的时候,心里都在想要是写的是 Rust 就好了。
因为工作中没有机会写 Rust,所以我只能用 Rust 写一些感兴趣的个人项目,这次想写个 QUIC 协议栈来玩,很大一部分动力是来源于可以使用 Rust 来写。
考虑到我今年刚接触到 Rust,其实我一直在尽力写出具有 Rust 风格的代码,但总是有很多地方写的不够 Rust,所以我希望能够通过对这个个人项目不断的重构迭代,来熟悉 Rust 代码风格。
我的 fc25 游戏账号被误封了
最后也是最重要的原因,我的 fc 25 游戏账号被误封了,所以我现在有了大量空闲时间来启动这个个人项目。在这里,我想很认真的对 ea 说几句话:你 TM 是世界上最烂的游戏公司,游戏里有大量的作弊行为,我每周都在和挂狗斗智斗勇,你没有能力解决。结果用最随意、最垃圾的检测机制,误封了我这种认认真真玩游戏的玩家(论坛里面像我这样被误封的有很多),我再也不会玩你这种垃圾公司出的游戏了。另外还有 TGA,你把 2024 最佳体育游戏颁给了 FC 25, 你也是真的是非常搞笑。
想实现怎么样的 QUIC 项目
首先,这不是给生产环境使用的,纯粹个人实践。打个比方,考虑到 QUIC 握手非常有意思,我都不打算使用 rustls 来实现 QUIC(更别说 OpenSSL 了),而是直接基于 ring 这样的加密算法库,来实现 QUIC 握手,所以注定离生产级别标准离得很远。
其次我在实现 QUIC 协议栈的同时,会优先完成一个 QUIC 客户端,它在我心中的定位更偏向于是一个工具,会支持尽可能多的 QUIC 协议相关自定义选项,不仅仅只是 QUIC transport parameters 这样基础的配置。举个例子,比如 QUIC key update 的行为,客户端可以指定发送多少报文,或者启动多久之后主动触发 key update。这样更细节更灵活的配置,会方便理解 QUIC 协议,至少我当初为了理解 QUIC 协议里面很多细节,需要手动对开源项目做出一些修改,才能触发我想看到的场景,还是挺麻烦的。
所以下面是我拍脑袋想到的,实现 QUIC 协议栈和客户端时,需要实现功能点,一共会分为几大类:
首先是基础功能,如果缺失,我们实现的 QUIC 客户端就没办法满足最基础的运行条件:
- QUIC 握手
- QUIC Reliability
- QUIC 挥手
- QUIC Key Update
- QUIC Stream
- QUIC Flow Control
其次是关于质量的功能,很多场景下,这些功能对质量的影响至关重要
- QUIC 0-RTT
- QUIC 拥塞控制
- QUIC Migration
- QUIC MTU 探测
- QUIC Unreliable Datagram
最后是一些非常有意思的 QUIC 草案,比如说 multipath,当然还有 qlog 这种。另外,我还会尝试接入到 tokio 中来,毕竟基于协程的异步网络编程才是主流。或者如果有精力,还会尝试实现下 HTTP/3 ,考虑到大部分 QUIC 协议流量都是为了 HTTP/3 服务的,如果一切顺利,希望能够实现的客户端可以和我现在这个静态博客网站可以正常交互。是的,我的这个静态博客,目前使用 openresty 搭建的,就支持 HTTP/3,哈哈。
前期的准备工作
好了,说完前面这么多,再看下正式开始这个项目之前,需要哪些准备,更准确的说,是我会做哪些准备。首先,文档方面,我推荐直接使用 QUIC Working Group 这个作为所有 QUIC 相关 RFC 文档的总入口,非常好用。
如果只有 RFC 肯定是远远不够的,我在阅读 RFC 的时候,经常会遇到难理解的地方。当然这肯定不是 RFC 的问题,哈哈,这是我的问题,当涉及到我不够熟悉的地方,我阅读起来自然会困难一些,不过只要花点时间去把前置信息去理解清楚,就可以继续阅读了。
所以,在 RFC 之外,我准备好了从源码构建出来的 NGINX 和 OpenSSL,并且具备调试信息和详细的日志(比如打开了对应的 DEBUG 宏),来作为服务端,和我实现的客户端进行交互(当然这里不会描述我做准备这些的细节,因为相关开源社区和文档都很完备)。如果我客户端编写存在任何的问题,我可以快速根据服务端中 NGINX 和 OpenSSL 的错误日志,明白我运行到了协议的哪一步,以及我究竟做错了什么。根据我的个人经验,实现某个功能,或者个人项目,这样的快速反馈对代码快速实现是必不可少的。
除了服务端的详细信息可以给我反馈以外,比如 Wireshark 这类的抓包软件,也可以让我快速知道,QUIC 协议进行的具体情况。当然有人会说 QUIC 是基于 TLS 加密的,wireshark 很难看到具体的协议细节。这个其实解决的办法有很多,考虑到我计划自己实现 TLS 层,所以直接通过 Wireshakr sslkeylog 的方式,将对称密钥写到相关文件,让 Wireshark 获取到对称密钥并且正常解密就可以了。最后我想说的是,我习惯使用 ssh + tmux 的方式远程开发,所以我一般使用 tshark (Wireshark 的命令行版本),非常好用,如果没有尝试过,可以考虑试一试。
接着就是 Rust 方面有哪些好用的工具,可以帮助我进一步提升代码的质量。这里不得不说,我今年 5 月的时候,尝试第一次使用 Rust 给 aya 提了一个 pull request,我才发现原来 Rust 有挺多自动化检测工具的(嗯,我在 CI/CD 的流程中遇到了,所以我不得不解决 pr 被检测出来的各种问题),我本来最多用用 cargo fmt,满足代码风格的强迫症。现在,我知道,原来 Rust 还有 clippy 和 mrio 这么好用的检查工具,所以我会加到这个项目的 CI/CD 里面来。
可靠的测试是必不可少,但是考虑到我可能会大幅度重构代码,我初期不会花费精力在 UT 上面,而是在项目具备基础能力后,加上自动化测试,来确保功能的稳定性。后期,我会考虑使用 cursor 这样 AI 工具来补上 UT。
最后,我在 github 上创建了这个项目: feather-quic,我觉得这段旅程肯定会充满乐趣。