困惑
上一份工作时,我对除了 HTTP 之外的网络知识一无所知。每次遇到一些底层的问题我就像《星际穿越》里面的主角进入了黑洞,觉得头晕眼花。比如
- 当时经常要接入第三方的 Web API,刚开始调试不通,到底问题在哪里?是我们自己出错了还是对方出错了?遇到过的问题有对方的服务器挂了、对方给的签名密钥是错误的、对方没有开防火墙等等。此时如何快速定位出错的环节是接入 API 的关键,否则扯皮了一周还没有进入程序实现流程。可对于当时对网络一知半解的我无疑非常困难,各种工具使用也磕磕绊绊。
- PING、curl、Telnet、Wireshark 这些软件分别是用来做什么的?有什么关系?
- 项目组使用的微服务框架底层使用什么实现的?如何对其截获数据和做 Mock 测试?当我阅读源码,发现底层使用了 TCP 协议,于是又进入了黑洞,遂放弃。只能从项目的 HTTP server 入口处开始测试或者写细粒度的单元测试。
- 看到某个公司做流媒体,底层协议使用了 UDP 而不是 TCP,为什么?这样有什么优劣?
- 经常看到介绍说 HTTP 是无状态的,这个无状态到底是什么意思(学完这门课会知道 TCP 协议是有状态的)?
带着这些困惑,我就想等有时间一定要系统补上计算机网络的知识。于是当去年 Hacker News 上说我关注已久的 Stanford CS144 终于把全部课程内容放出来之后,我就兴奋地收藏下来,今年终于有时间进攻。
如果你也有这些疑惑,这门课也很适合你。它既帮助我建立了对于计算机网络的全景图,又让我亲自动手实现了复杂的 TCP 协议,兼具深度和广度,对于提升工程能力颇有帮助。
简介
整个计算机研究的问题概括成一句话:如何把数据高效、可靠地从一点传输到另一点?
为了解决这个问题,工程师们设计了一个分层模型,每一层都完成自己的任务,越往下做的事情就越基础。同时每一层都要遵循各种协议,所谓的协议就是一套数据的标准。比如我们在应用端要做一个订单系统,订单需要金额和付款时间,如果顾客没有付款 15 分钟后就直接将其取消,这里涉及的数据格式和相关行为就构成了一个协议。
整个计算机网络分为四层:
- 应用层。典型的就是我们常用的 Web 应用,比如浏览器的网页,或者电脑、手机上看得网络视频。常见的有 HTTP、SSH、SMTP 协议。
- 网络层。这一层对于上层的感知就是一个字节流,字节流是一种抽象的数据结构,可以看做一个水管,一端写入数据,一端读取数据,底层可以用动态数组去实现。而这一层常用的协议有 TCP 和 UDP,而 TCP 协议就是本课的重点,一方面是它承前启后,另一方面它本身强大又复杂,是大多数软件工程师对于网络接触的最底层。
- 传输层。这一层对于上层来说就是一个个数据包,所谓的数据包其实就是一段数据,它分为 header 和 payload,header 里面放包的属性,payload 里面放需要传输的数据。这一层只有一种通用的协议:IP 协议,最大的特点就是不可靠,它只管传输过去,但是丢包、重复、乱序都是不处理的,需要 TCP 来实现可靠性。
- 链路层。这一层最大的特点就是直接涉及硬件,比如以太网、3G、WiFi。
lab
这门课的 lab 的特点是自顶向下,将 HTTP、TCP、Network Interface、ARP、Router 全部涉及了一遍,把一个 HTTP 的数据一步步切割放入 TCP、IP、以太网包,最后还把 Linux 的 TCP 实现替换成自己的实现,写了一个小小的爬虫。。能够 get your hands dirty 对于我理解课程有很大的帮助。
其中 lab 重心和难点还是放在了 TCP 协议上,几乎完整实现了 TCP 协议内容。
网络上的数据可能很大,但是在底层都会分解成一个个包,每个包都由 header、payload 组成,从 HTTP 》 TCP 》 IP 》 以太网,结构都是一致的。以 TCP 为例,一个包的上限大概一千多字节。
因为 IP 不可靠,所以必须要由 TCP 来实现可靠性。而 TCP 协议通过建立状态值来实现这一点。
具体点说,在 TCP 的通信方的客户端中,会在内存中放入和维护状态值,比如发送一个包的时间点,或者每个包的序号。最后通过超时时间、序号等等来判断丢包、重复、乱序等等错误情况,从而进行补发。其实做过应用开发的同学能感受到,我们做的应用程序也经常会有这样的容错方法:维护一些状态值,如果程序出错了就去进行重试,典型的场景就是电商的下单。所以 TCP 协议虽然底层,但是背后的 big idea 其实是非常具体直观的。
老师将 TCP 分为了四层:底层字节流数据、收集乱序 TCP 包的数据结构、TCP 发送客户端和接收客户端、TCP 连接。顺序为自底向上,能够非常顺畅地实现整个 TCP 协议。
整个 lab 的难点有三处:
- CPP。因为 CPP 有各种不安全特性,导致很容易有运行时的问题。比如各种 Undefined Behavior 、异常默认不打出调用栈,中间就有一个无符号类型小于零之后 Underflow 的 bug 耗费我一个小时。我的措施是尽量多加校验,针对异常行为打印日志。当用了内存检查之后编译会非常慢,所以我只有遇到很难定位的 segment fault 才会开内存检查模式。平时还是用 debug 模式编译。
- 最后实现了 TCP 协议之后的集成测试。这里注意的一点是集成测试的原理是把数据从一方发送到另一方,然后把原始数据和接收到的数据打印到 standard out 里面,然后做 IO Redirection 到两个文件里,然后对两个文件的哈希值作对比。我就把之前的日志打印到了一个日志文件里,否则会影响测试。
- 理解 TCP 连接的 teardown 的算法。对于这个问题,可以先读一读 Two General Problem,理解计算机通信的基础问题的背景。再的话就是把握两个细节,一是 sender 发送一个包给 receiver 之后,receiver 发送 ACK 信号给 sender,但是 sender 不会对 receiver 的 ACK 信号发送 ACK 信号,所以除非有后续的交互,否则 receiver 无法得知 sender 是否接受到了 ACK 信号;二是如果对方先发送 FIN 信号,接着我方发送 ACK 信号,再发送的 FIN 信号 里面是携带 ackno 的,所以一旦得到了这个信号的 ACK 信号,说明对方一定拿到了之前的 ACK 信号。所以才能进入 passive mode。
总结
这门课大概耗费了我两个半月的时间,每周大概学习四到五天,两到四小时。中间因水土不服持续中暑了好几个月,每天晕乎乎的;再就是兼职工作耗费了一些精力。如果不是这些因素大概一个月左右就写完了。
这门课最大的收获就是扫除了一大盲区,让我感觉自己在 CS 里更自由。除此之外摸清楚了这些好学校硬课的规律,就是理论介绍非常宽泛, lab 只会实现里面的子集。我发现自己的认知规律是如果没有具体的程序,很难理解抽象的描述,所以我会更多关注理论中需要实现的部分。如果看不懂理论也不会硬磕,而会结合程序去看,又是程序给出的信息也不够充分,就会看测试(毕竟测试是最正式化的语言)。很多知识点都要结合课程视频、文档、程序、测试才能完全理解。