我眼中的 TCP/IP

最近在某个微信大群里加到了 potatso 的作者 icodesign 。激发了我也写一款 iOS socket5 代理软件的想法。写网络层代码的第一步当然是学习基础的网络知识了。
因为工作中主要是做移动端开发的,网络层代码一般 OC 就用 AFN 框架 Swift 就用 almofire 。对于计算机网络的知识了解的比较少。最近买了两本书讲解 TCP/IP 和 http 的书,阅读完后做个超简略的总结。

网络结构

TCP/IP 协议将网络分为 5 层,由下向上是 1、物理层 2、数据链路层 3、网络层(IP层)4、传输层 5、应用层。
我们经常使用的 IP 协议属于网络层,TCP 协议属于传输层 ,HTTP 协议属于应用层。

为什么要分层?如何学习每一层的协议。

分层是为了各干各的事,每层都为上层提供接口,这样上层就可以专注于本层要解决的问题。就向我要计算一个物体的体积,我只要学会体积的计算公式即可,我没必要学习加减乘除的计算方法。只要给我一个计算器就可以了,计算器就是下层提供的接口。
每一层的协议其实就是在一个数据段的前端增加本层的所需要的信息,学习每一层的协议,其实就是学习每次 header 中的内容。能知道 header 中每个位置所代表的含义,就是学习了这层的协议。

简单的介绍试一下 IP TCP

IP

首先,我们要搞明白什么是互联网,也就是我们平时所说的 internet。互联网解决的是设备之间的连接问题,目标是让设备 A 的信息能准确的送达设备 B。为了实现这个目标,我们需要先了解互联网的结构。internet 其实是由无数个子网所构成,是一个二级的结构,第一级是子网,第二级才是子网中的设备。所以 internet 中设备 A 的信息要抵达设备 B,必须先要找到 B 所在的子网,进而再在子网中找到 B。这就是一个路由的过程,我们打个比方,换个角度来描述。

就拿最近火热的一部电视剧「大秦帝国之崛起」来说。战国时期,各诸侯国其实就构成了一个典型的 internet 结构,各国独立是一个子网,各大大小小的子网组合起来,就构成了 internet。秦国大王想送一封信到赵国,这就涉及一个完整的信息路由过程,分为以下几步:

首先秦国信使要离开王宫,带着信件踏出秦国。
其次,信使要先找到赵国位置,中间会路过其他各国。
最后抵达赵国,入赵国之后才能被进一步引见至赵王,递交信件。
上面三步,涉及两个过程。第一是寻找子网的过程,第二是在子网内寻找个体的过程。要做到这两点,IP 地址的设计就要能反映这两个过程。我们来看 IP 地址的机构。

IP 地址 = 网络地址 + 主机地址

IP Address = Network ID + Host ID

IP 地址就是这样一个二级的结构,因为只有二级的结构,才能实现我们上面所说,二级的路由寻址过程。那么对于一个 32 位的 IPv4 地址,我们如何确定哪部分是 Network ID,哪一部分又是 Host ID 呢?我们有请子网掩码(subnet mask)。

子网掩码是很多同学耳熟却不能详的一个概念,简单来说,subnet mask 就是为了分割 Network ID 和 Host ID 的。subnet mask 也有一段有趣的进化历史,值得一提,如何切割 IP 地址经历了一个从「偷懒」到「机智」的过程。

我们先看一个典型的 IP 地址,10 进制和 2 进制:

123.125.114.144

01111011.01111101.01110010.10010000

网络协议的设计者肯定都是聪明人,但最开始,这帮聪明人在设计 IP 地址的切割机制时,偷懒了。有人一拍脑袋,IP 地址不是 4 个字节吗,那简单点吧,我们就按字节切割,那么有以下几种方式:

第一个字节为 Network ID,剩下三个字节为 Host ID
第二个字节为 Network ID,剩下两个字节为 Host ID
第三个字节为 Network ID,剩下一个字节为 Host ID
上面这三种切割方式所划分出来的 IP 地址,就是我们经常所说的 A 类地址,B 类地址,C 类地址。实际上,还有 D 类和 E 类,只不过是做保留实验之用,一般很少提及。

那么我们如何确定一个 IP 地址是属于 A B C 的哪一类呢?我们以第一个字节来做一些约定:

如果第一个字节的起始比特位为 0,则是 A 类地址。
如果第一个字节的起始比特位为 10,则是 B 类地址。
如果第一个字节的起始比特位为 110,则是 C 类地址。
每一类地址里,Network ID 剩下的比特位则用作实际的 Network ID。依次,我们不难进一步得出结论,我们根据第一个字节的数字就可以判断地址类型:

A 类地址的范围为:1 - 126

B 类地址的范围为:128 - 191

C 类地址的范围为:192 - 223

好了,我们终于说完了第一种 IP 地址的切割方式,明白了所谓的 A B C 类 IP 地址。但这种方式很偷懒不是吗?它的问题很明显,IP 地址的切割方式太粗糙,粒度太大。比如说 B 类网络地址,Host ID 只占两个字节,一个子网里也有 65536 个 Host ID,现实世界中哪有那么大的机构和组织,可以使用完 6 万多个 IP 地址呢?即使是alibaba 这样的大公司,也无非是 2-3 万人,所以,这里面存在极大的地址浪费,这种机制太偷懒了!

于是有了第二种 IP 地址切割方式,有另外聪明人看不下去了,提出了 CIDR,来解决第一种切割方式挖的坑。接下来,我们好好说下 CIDR 的概念,这也是如今 internet 路由所使用的切割方式。好啦,先总结一下,所谓的路由寻址,第一步是找到正确的 Network ID 地址,其次再是找到正确的 Host ID。

TCP

上面说了学习 TCP 其实就是学习 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 为特殊用途数据。