TCP/IP 系列之 Header 篇
这是 TCP/IP 系列的第二篇,阅读目标是建立对网络包结构的初步认识。
上一篇里,我们提到一次完整的 TCP 会话其实是一个字节流,只不过我们可以按照一定的规则来切割这个字节流,从而划分出一个个的网络包。我们经常说抓包分析网络故障,这个抓包里所指的包在不同的语境下其实意义并不相同,首先我们得对包的构成形成具象的认知。
再看七层协议
刚毕业那会,大家都喜欢在简历上写「精通 TCP/IP 协议」,面试被问起时就必答三次握手和 OSI 七层模型,再多就说不出来了。问七层模型是什么就机械式的从上至下背诵一遍,这离精通还差了一本【TCP/IP 详解】。
我在完整的学习过一遍网络协议栈之后,深感所谓的七层模型是偏工业的说法,看到七层协议图之后,其实很难明白这七层是如何一层层互相构成的,更符合大脑感官的是另一种认知形式,是一种洋葱形的结构,层层叠叠互相包裹,可以用下图表示:
左边是教材上的结构,右边是我所说的洋葱式的结构。如果以一个 HTTP 请求为例,右图中 Application 部分就代表我们用 Charles 抓包时所感知的部分,这一部分要最后转化为光信号,在光纤中传输,还需要经过一层层的转化,这个转化过程说白了,就是在每一层加上一个 header。
- Application 层(HTTP)的数据在经过传输层(TCP Layer)的时候,会加上 TCP 的 header,成为一个 TCP Segment。
- 传输层(TCP)的 Segment 在经过网络层(IP Layer)的时候,会加上 IP 的 header,成为一个 IP Packet。
- 网络层的 IP Packet 在经过链路层(Link Layer)的时候,会加上Link Layer 的 header,成为一个 Frame。
- 最后 Frame 会在物理层,将数字信号转化为物理信号传输。
这里值得特别注意的是,在每一层,有不同的英文术语来对应包的概念,比如在 TCP 层的包叫做 Segment,在 IP 层的叫做 Packet,在链路层的叫做 Frame,另外和 TCP 位于同一层的 UDP 包我们一般叫做 Datagram,不同协议层里术语并不一样,好处是,在交流的时候,我们选择不同的英文单词就能预先确立是在那一层讨论协议。而 Segment、Packet、Frame、Datagram 等翻译成中文的时候,都是译为「包」,大家说读英文原版资料是不是更好,这些术语我们需要特别记忆,可以对照下图:
+-------------+-------------------------+
| Application | HTTP Packet |
+-------------+-------------------------+
| Transport | TCP Segment |
+-------------+-------------------------+
| Network | IP Packet |
+-------------+-------------------------+
| Link | Frame |
+-------------+-------------------------+ 光纤
| Physical | Bits | ====================>
+-------------+-------------------------+
不过有些场景下,我们也会用 Packet 来泛指每一层的包,但是用每一层自己的术语会更准确和专业,这些行话和习惯我们也需要了解。
我们可以用一个公式来表示每一层协议的构成:
Packet = Protocol Header + Payload
每一层的包都可以用这个公式来表示。Payload 指传入这一层的数据内容,比如:
TCP Segment = TCP header + HTTP data
有了这个认知之后,对于每一层协议的学习,最后就落实到每一层 header 的学习上了,学习 TCP 就是研究 TCP header 的构成,header 里的每一个 bit 位都有特别的用处,来实现协议层对于网络传输的控制。这也是为什么我经常会说,所谓的网络协议学习就是 header 学习,这也是本文标题的含义所在。
深入 Header
【TCP/IP 详解】大致有 1000 页,通读的过程会很漫长且枯燥,知道每一层协议都是关于 header 的设计之后,大家其实可以先跳跃式的阅读,先学习感兴趣的部分,有了收获知识的正向反馈之后,在回过头来填补更多的知识细节。比如大家一般都对 TCP 协议比较感兴趣(确实也是最有意思的部分,后面的文章也重点分析),那么可以先跳到第十二章:
Chapter 12 TCP: The Transmission Control Protocol(Preliminaries)
或者第十三章,真正明白所谓的三次握手:
Chapter 13 TCP Connection Management
由于每一层的设计都是独立的,所以先学习传输层并不会有什么障碍,这也是分层架构的意义所在,各层各司其职,互不依赖具体的实现。
我们的学习行为,大致上可以分为两类,理解(理解思想)和记忆(强行记忆)。对于 header 的学习,除了理解 header 每个 bit 的意义之外,还需要一些记忆行为,对于一些关键信息的强制性记忆,有助于我们形成更深刻的认知。我们以 TCP 的 header 为例:
上图是一个 TCP header,以下是一些需要「死记硬背」的信息点:
一个 TCP Header 一般有 20 个字节,如果启用了 options,header的长度可以达到 60 个字节。上图中每一行是 4 个 bytes,32 个 bits。我先带大家学习下前 5 行,每一行是 4 Bytes,五行刚好是 20 个 bytes。计算机世界中,通常会以 bit,byte,word(4 个 byte)等不同粒度来描述信息,header 的学习一般是以 4 个字节为一个单位来展示的。
- 第一行,由 Source port 和 Destination port 构成,二者各占 2 个字节,刚好一起占据第一行的 4 个字节。这两个字段分别表示 TCP 连接中的,发送方端口号和接收方的端口号,既然一个 port 只占 2 个 bytes,那么端口值的范围自然就是 0~65535 啦。
- 第二行,Sequence number,表示发送方的序列号。这个序列号表示的是什么呢?一个 TCP 流是有无数个 0 和 1 构成,这些 0 和 1 以 8 个 bit 为单位,可以分割成一个个的 byte,TCP 是可靠传输协议,每一个 byte 都是有标号的,因为我们需要追踪每个 byte 是否被成功传输了,每个 byte 的标号就是我们这里的 sequence number。假设我们建立 TCP 连接的时候,发一个 sync 包,我们就以 0 标记 sync 包的第一个字节,那么 sync 包中的 Sequence 值就是 0。实际应用中,处于安全考虑,TCP 流的第一个 Sequence number 一般不会是 0,而是一个随机数。Sequence number 占据 4 个字节,也就是 2 的 32 次方,这个数字并不算大,每个包都会用掉一些,如果达到最大值之后,就取余从 0 重新开始。
- 第三行,Acknowledge number,表示接收方 ack 的序列号。接收方收到发送方一个的 TCP 包之后,取出其中的 sequence number,在下一个接收方自己要发送的包中,设置 ack 比特位为 1,同时设置 acknowledge number 为 sequence number + 1。所以接收方的 acknowledge number 表示的是,接收方期待接收的下一个包起始字节的标号,大家可以仔细理解下这一句话。所以 acknowledge number 和 sequence number 是配对使用的。
- 第四行,这一行尤其重要,出于篇幅的考虑,其中细节会在后续的文章中讲解。这里简单提下从 CWR 到 FIN 的 8 个 bit,这 8 个 bit 里每一位都是一个标记位,用来标记当前 TCP 包的特殊含义。比如我们所说的三次握手,第一个 sync 包,就是将 SYN 位置为 1。第二个 syn + ack 包就是将 header 的 ACK 和 SYN 位都置为 1。第三个 ack 包即将 ACK 位置 1。剩余的几个 bit 位暂时不展开讲了,大家可以自己看书先学习下。
- 第五行,这一行只有两个字段,即 Checksum 和 Urgent pointer。checksum 是个通用的计算机概念,做完整性校验之用,在很多协议(IP,UDP,ICMP)中都有应用,这个值有包的发送方去计算,之后由包的接收方取出来校验。Urgent pointer 为两个字节的偏移量,加上当前包的 sequence number,用来标记某一个范围内的 bytes 为特殊用途数据。
怎么样、其实没有多少信息量对不对?这么跟着理解一遍 header 中的每一个 bytes 之后,是不是加深了对 TCP 的理解呢?
同理,学习完 TCP 的 header 之后,大家可以再去把 IP 的 header,frame 的 header 都搜索出来,对照关键字段去理解学习,最后再配合【TCP/IP 详解】一书阅读效果更好。
Tcpdump 实战
上面是理论部分,可能有些枯燥,大家可以在理解之后,使用 tcpdump 抓包实战下,进一步加深理解。我们就来抓包,基于上面 tcp header 的学习,抓下三次握手的包 :)
我们可以用如下命令来抓三次握手的包:
sudo tcpdump -i en0 "tcp[tcpflags] & (tcp-syn|tcp-ack) != 0"
输出结果为:
- 18:18:45.687476 IP 192.168.3.7.65284 > 59.37.116.101.https: Flags [S], seq 3942311653, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 589105996 ecr 0,sackOK,eol], length 0
- 18:18:45.719744 IP 59.37.116.101.https > 192.168.3.7.65284: Flags [S.], seq 303118583, ack 3942311654, win 14280, options [mss 1412,sackOK,TS val 1712930033 ecr 589105996,nop,wscale 8], length 0
- 18:18:45.719986 IP 192.168.3.7.65284 > 59.37.116.101.https: Flags [.], ack 1, win 4112, options [nop,nop,TS val 589106029 ecr 1712930033], length 0
你能根据上面 tcp header 的学习,理解上面 tcpdump 命令的含义吗?tcpflags 指的是 header 中的哪些位呢?
对于 tcpdump 的使用还不太了解的同学,可以翻阅我之前的一篇介绍文章。
当然 TCP 里还包含着很多有趣的知识点,大家可以先行阅读,后面我会逐步讲解,比如 TCP 的 ARQ 机制,Flow Control 等。
总结
这次新启的 TCP/IP 系列文章对我来说,会是一次耗时费力,旷日持久的旅途。希望能够慢火细熬,徐徐烹制,将这道倾注心力、营养丰盛的网络协议佳肴制作完成,与君共享。