摘要 :计算机语言(C 语言,Java 等)是人 > 类和计算机沟通的协议,能够教给一台计算机完成你的工作,但是,要想一大片机器互相协作、共同完成一件事,你需要用到网络协议。

想成为技术牛人,先搞定网络协议。

为什么要学习网络协议?

计算机语言(C 语言,Java 等)是人类和计算机沟通的协议,通过这种协议,计算机可以知道我们想让它做什么。但是这种协议计算机不能直接读懂,对于计算机,它只认识 0 和 1,所以计算机语言还需要编译之后,计算机才会读懂。

协议三要素:

  • 语法,就是这一段内容要符合一定的规则和格式。例如,括号要成对,结束要使用分号等。
  • 语义,就是一段内容代表某种意义。例如数字减去数字是有意义的,数字减去文本一般来说就没有意义。
  • 顺序,就是先干啥,后干啥。例如,先加上某个值,再减去某个值。

计算机语言,能够教给一台计算机完成你的工作,但是,要想一大片机器互相协作、共同完成一件事,只教给一台机器做什么是不够的,你需要学会给一大片机器做什么。这就需要网络协议

网络分层的真实含义是什么?

复杂的程序都要分层。比如,复杂的电商还会分数据库层、缓存层、Compose 层、Controller 层和接入层,每一层专注做本层的事情。

程序是如何工作的?

只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。

例如:TCP 在三次握手的时候,IP 层和 MAC 层在做什么呢?当然是 TCP 发送每一个消息,都会带着 IP 层和 MAC 层了。因为,TCP 每发送一个消息,IP 层和 MAC 层的所有机制都要运行一遍。而你只看到 TCP 三次握手了,其实,IP 层和 MAC 层为此也忙活好久了。

所以,对 TCP 协议来说,三次握手也好,重试也好,只要想发出去包,就要有 IP 层和 MAC 层,不然是发不出去的。

所谓的二层设备、三层设备,都是这些设备上跑的程序不同而已。一个 HTTP 协议的包经过一个二层设备,二层设备收进去的是整个网络包。这里面 HTTP、TCP、IP、MAC 都有。什么叫二层设备呀,就是只把 MAC 头摘下来,看看到底是丢弃、转发,还是自己留着。那什么叫三层设备呢?就是把 MAC 头摘下来之后,再把 IP 头摘下来,看看到底是丢弃、转发,还是自己留着。

ifconfig:最熟悉又陌生的命令行

怎么查看 IP 地址?
Windows 上是ipconfig,在 Linux 上是ifconfigip addr

IP 地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码。大部分的网卡都会有一个 IP 地址,当然,这不是必须的。

32 位的 IP 地址就被分成了 5 类:

C 类地址能包含的最大主机数量只有 254 个,现在估计一个网吧都不够用。
B 类地址能包含的最大主机数量又太多了。6 万多台机器放在一个网络下面,一般的企业基本达不到这个规模,闲着的地址就是浪费。

无类型域间选路(CIDR)

CIDR,打破了原来设计的几类地址的做法,将32位的 IP 地址一分为二,前面是网络号,后面是主机号。从哪里分呢?你如果注意观察的话可以看到,10.100.122.2/24
这个 IP 地址中有一个斜杠,斜杠后面有个数字24。这种地址表示形式,就是 CIDR。后面24的意思是,32位中,前24位是网络号,后8位是主机号。

伴随着 CIDR 存在的,一个是广播地址10.100.122.255。如果发送这个地址,所有10.100.122网络里面的机器都可以收到。另一个是子网掩码255.255.255.0
将子网掩码和 IP 地址进行AND计算,就可得到网络号

公有 IP 地址和私有 IP 地址

在日常的工作中,几乎不用划分 A 类、B 类或者 C 类,所以时间长了,很多人就忘记了这个分类,而只记得 CIDR。但是有一点还是要注意的,就是公有 IP 地址和私有 IP 地址。

表格最右列是私有 IP 地址段。平时我们看到的数据中心里,办公室、家里或学校的 IP 地址,一般都是私有 IP 地址段。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。因此,你学校的某个私有 IP 地址段和我学校的可以是一样的。

这就像每个小区有自己的楼编号和门牌号,你们小区可以叫 6 栋,我们小区也叫 6 栋,没有任何问题。但是一旦出了小区,就需要使用公有 IP 地址。

192.168.0.x是最常用的私有 IP 地址。一般你家里地上网设备不会超过 256 个,所以/24基本就够了。有时候我们也能见到/16 =的 CIDR,这两种是最常见的。
很明显192.168.0是网络号,整个网络里面的第一个地址192.168.0.1, 往往就是你这个私有网络的出口地址。例如你家的路由器地址就是192.168.0.1
192.168.0.255就是广播地址。一旦发送这个地址,整个192.168.0网络里面的所有机器都能收到。

一个容易 “犯错” 的 CIDR

16.158.165.91/22这个 CIDR。求一下这个网络的第一个地址、子网掩码和广播地址。

要是上来就写16.158.165.1,那就大错特错了。

/22不是8的整数倍,不好办,只能先变成二进制来看。16.158的部分不会动,它占了前16位。中间的165,变为二进制为10100101。除了前面的16位,还剩6位。所以,这8位中前6位是网络号,16.158.<101001>,而<01>.91是机器号。第一个地址是16.158.<101001><00>.1,即16.158.164.1。子网掩码是255.255.<111111><00>.0,即255.255.252.0。广播地址为16.158.<101001><11>.255,即16.158.167.255

D 类是组播地址。使用这一类地址,属于某个组的机器都能收到。这有点类似在公司里面大家都加入了一个邮件组。发送邮件,加入这个组的都能收到。

root@test:~\# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff
inet 10.100.122.2/24 brd 10.100.122.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fec7:7975/64 scope link
valid_lft forever preferred_lft forever

上面的输出,IP 地址的后面有个scope,对于eth0这张网卡来讲,是global,说明这张网卡是可以对外的,可以接收来自各个地方的包。对于lo来讲,是host,说明这张网卡仅仅可以供本机相互通信。

lo全称是loopback,又称环回接口,往往会被分配到127.0.0.1这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现。这就是为什么你可以在浏览器通过访问127.0.0.1这个地址来访问本地服务,而且一般在你本机的host文件,会有127.0.0.1 localhost,这是个映射关系,访问localhost相当于127.0.0.1

MAC 地址

link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff,这个被称为MAC 地址,网卡的物理地址,用十六进制,6 个 byte 表示。

MAC 地址既然全局唯一,不会有两个网卡有相同的 MAC 地址,那么为什么不直接用 MAC 地址来进行通信?

一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。IP 地址,才是有远程定位功能的。

MAC 地址更像是身份证,是一个唯一的标识。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。从硬件角度,保证不同的网卡有不同的标识。

例如,你去杭州市网商路 599 号 B 楼 6 层找刘超,你在路上问路,可能被问的人不知道 B 楼是哪个,但是可以给你指网商路怎么去。但是如果你问一个人,你知道这个身份证号的人在哪里吗?可想而知,没有人知道。

MAC 地址是有一定定位功能的,只不过范围非常有限。你可以根据 IP 地址,找到杭州市网商路 599 号 B 楼 6 层,但是依然找不到我,你就可以靠吼了,大声喊身份证 XXXX 的是哪位?我听到了,我就会站起来说,是我啊。

MAC 地址的通信范围比较小,局限在一个子网里面。例如,例如,从192.168.0.2/24访问192.168.0.3/24是可以用 MAC 地址的。一旦跨子网,即从192.168.0.2/24192.168.1.2/24,MAC 地址就不行了,需要 IP 地址起作用了。

网络设备的状态标识

<BROADCAST,MULTICAST,UP,LOWER_UP>是干什么的?这个叫作net_device flags网络设备的状态标识

  • UP 表示网卡处于启动的状态
  • BROADCAST 表示这个网卡有广播地址,可以发送广播包
  • MULTICAST 表示网卡可以发送多播包
  • LOWER_UP 表示 L1 是启动的,也即网线插着呢。
  • MTU1500 是指最大传输单元 MTU 为 1500,这是以太网的默认值。MTU 是二层 MAC 层的概念。MAC 层有 MAC 的头,以太网规定连 MAC 头带正文合起来,不允许超过 1500 个字节。
    正文里面有 IP 的头、TCP 的头、HTTP 的头。如果放不下,就需要分片来传输。

DHCP:IP 是怎么来的,又是怎么没的?

如何配置 IP 地址

命令行自己配置一个地址。可以使用 ifconfig,也可以使用 ip addr。设置好了以后,用这两个命令,将网卡 up 一下,就可以开始工作了。

但是不能随便配置,例如192.168.1.6就在你这台机器的旁边,甚至是在同一个交换机上,而你把机器的地址设为了16.158.23.6。在这台机器上,你企图去ping 192.168.1.6,你看着它有自己的源 IP 地址16.158.23.6,也有目标 IP 地址192.168.1.6,但是包发不出去,这是因为 MAC 层还没填。IP 只有是一个网段的,它才会发送 ARP 请求,获取 MAC 地址
如果不是,它便不会直接将包发送到网络上,而是企图将包发送到网关

如果你配置了网关的话,Linux 会获取网关的 MAC 地址,然后将包发出去。对于192.168.1.6这台机器来讲,虽然路过它家门的这个包,目标 IP 是它,但是无奈 MAC 地址不是它的,所以它的网卡是不会把包收进去的。如果没有配置网关,那包压根就发不出去。

网关要和当前的网络至少一个网卡是同一个网段的,否则不会配置成功。

动态主机配置协议(DHCP)

有了这个协议,网络管理员只需要配置一段共享的 IP 地址。每一台新接入的机器都通过 DHCP 协议,来这个共享的 IP 地址里申请,然后自动配置好就可以了。等人走了,或者用完了,还回去,这样其他的机器也能用。

如果是数据中心里面的服务器,IP 一旦配置好,基本不会变,这就相当于买房自己装修。 DHCP 的方式就相当于租房。你不用装修,都是帮你配置好的。你暂时用一下,用完退租就可以了。

解析 DHCP 的工作方式

一台机器新加入一个网络的时候,只知道自己的 MAC 地址。怎么办?先吼一句,我来啦,有人吗?这时候的沟通基本靠 “吼”。这一步,我们称为DHCP Discover

第一步:
新来的机器使用 IP 地址 0.0.0.0 发送了一个广播包,目的 IP 地址为 255.255.255.255。

第二步:
DHCP Server立刻能知道来了一个 “新人”,这个时候,我们可以体会 MAC 地址唯一的重要性了。当一台机器带着自己的 MAC 地址加入一个网络的时候,MAC 是它唯一的身份,
如果连这个都重复了,就没办法配置了。只有 MAC 唯一,IP 管理员才能知道这是一个新人。租给它一个 IP 地址,这个过程我们称为DHCP Offer。同时,DHCP Server 为
此客户保留为它提供的 IP 地址,从而不会为其他 DHCP 客户分配此 IP 地址。DHCP Offer 里面有新的分配的地址

DHCP Server 仍然使用广播地址作为目的地址,因为,此时请求分配 IP 的新人还没有自己的 IP。

第三步:
如果有多个 DHCP Server,这台新机器会收到多个 IP 地址,选择其中一个 DHCP Offer,一般是最先到达的那个。并且会向网络发送一个 DHCP Request 广播数据包,包中包含客户端的 MAC 地址、接受的租约中的 IP 地址、提供此租约的 DHCP 服务器地址等,并告诉所有 DHCP Server 它将接受哪一台服务器提供的 IP 地址,告诉其他 DHCP 服务器请求撤销它们提供的 IP 地址,以便提供给下一个 IP 租用请求者。

由于还没有得到 DHCP Server 的最后确认,客户端仍然使用0.0.0.0为源 IP 地址、255.255.255.255为目标地址进行广播。

第四步:
DHCP Server 接收到客户机的 DHCP request 之后,会广播返回给客户机一个 DHCP ACK 消息包,表明已经接受客户机的选择,并将这一 IP 地址的合法租用信息和其他的配置信息都放入该广播包,发给客户机,欢迎它加入网络大家庭。

IP 地址的收回和续租

客户机会在租期过去 50% 的时候,直接向为其提供 IP 地址的 DHCP Server 发送 DHCP request 消息包。客户机接收到该服务器回应的 DHCP ACK 消息包,会根据包中所提供的新的租期以及其他已经更新的 TCP/IP 参数,更新自己的配置。这样,IP 租用更新就完成了。

从物理层到 MAC 层:如何在宿舍里自己组网玩联机游戏?

第一层(物理层)

宿舍两个人的电脑怎么连接起来?可以使用路由器,但是路由器是在第三层上。我们先从第一层物理层开始说。

电脑连电脑。这种方式就是一根网线,有两个头。一头插在一台电脑的网卡上,另一头插在另一台电脑的网卡上。还需要配置这两台电脑的 IP 地址、子网掩码和默认网关。要想两台电脑能够通信,这三项必须配置成为一个网络,可以一个是192.168.0.1/24,另一个是192.168.0.2/24,否则是不通的。构成了一个最小的局域网,也即LAN

两台电脑之间的网络包,包含 MAC 层吗?当然包含,要完整。IP 层要封装了 MAC 层才能将包放入物理层。

怎么把三台电脑连在一起呢?有一个叫作 Hub 的东西,也就是集线器。这种设备有多个口,可以将宿舍里的多台电脑连接起来。但是,和交换机不同,集线器没有大脑,它完全在物理层工作。它会将自己收到的每一个字节,都复制到其他端口上去。这是第一层物理层联通的方案。

第二层(数据链路层)

Hub 采取的是广播的模式,如果每一台电脑发出的包,宿舍的每个电脑都能收到。这就需要解决几个问题:

  1. 这个包是发给谁的?谁应该接收?
  2. 大家都在发,会不会产生混乱?有没有谁先发、谁后发的规则?
  3. 如果发送的时候出现了错误,怎么办?

这几个问题都是第二层,数据链路层,也即 MAC 层要解决的问题。MAC 的全称是 Medium Access Control,即媒体访问控制

第二个问题,有很多算法可以解决:

  • 方式一:分多个车道。每个车一个车道,你走你的,我走我的。这在计算机网络里叫作 信道划分
  • 方式二:今天单号出行,明天双号出行,轮着来。这在计算机网络里叫作 轮流协议
  • 方式三:不管三七二十一,有事儿先出门,发现特堵,就回去。错过高峰再出。我们叫作 随机接入协议 。著名的以太网,用的就是这个方式。

解决了第二个问题,就是解决了媒体接入控制的问题。

第一个问题:这里用到链路层地址,也被称为MAC 地址

第二层的网络包格式:

有了这个目标 MAC 地址,数据包在链路上广播,MAC 的网卡才能发现,这个包是给它的。MAC 的网卡把包收进来,然后打开 IP 包,发现 IP 地址也是自己的,再打开 TCP 包,发现端口是自己,也就是 80,而 nginx 就是监听 80。

于是将请求提交给 nginx,nginx 返回一个网页。然后将网页需要发回请求的机器。然后层层封装,最后到 MAC 层。因为来的时候有源 MAC 地址,返回的时候,源 MAC 就变成了目标 MAC,再返给请求的机器。

第三个问题:CRC,也就是循环冗余检测。通过 XOR 异或的算法,来计算整个包是否在发送的过程中出现了错误。

ARP 协议

当源机器知道目标机器的时候,可以将目标地址放入包里面,如果不知道呢?一个广播的网络里面接入了 N 台机器,我怎么知道每个 MAC 地址是谁呢?这就是ARP 协议,也就是已知 IP 地址,求 MAC 地址的协议。

在一个局域网里面,当知道了 IP 地址,不知道 MAC 怎么办呢?靠 “吼”。

为了避免每次都用 ARP 请求,机器本地也会进行 ARP 缓存。当然机器会不断地上线下线,IP 也可能会变,所以 ARP 的 MAC 地址缓存过一段时间就会过期。

局域网

Hub 组网的方式,一旦机器数目增多,问题就出现了。因为 Hub 是广播的,不管某个接口是否需要,所有的 Bit 都会被发送出去,然后让主机来判断是不是需要。这种方式路上的车少就没问题,车一多,产生冲突的概率就提高了。而且把不需要的包转发过去,纯属浪费。

这就需要二层设备,交换机

因为每个口都只连接一台电脑,这台电脑又不怎么换 IP 和 MAC 地址,只要记住这台电脑的 MAC 地址,如果目标 MAC 地址不是这台电脑的,这个口就不用转发了。交换机怎么知道每个口的电脑的 MAC 地址呢?这需要交换机会学习。

一台 MAC1 电脑将一个包发送给另一台 MAC2 电脑,当这个包到达交换机的时候,一开始交换机也不知道 MAC2 的电脑在哪个口,所以没办法,它只能将包转发给出了来的那个口之外的其他所有的口。这个时候,交换机会记住 MAC1 是来自一个明确的口。以后有包的目的地址是 MAC1 的,直接发送到这个口就可以了。

交换机作为一个关卡一样,过了一段时间之后,就有了整个网络的一个结构了,这个时候,基本上不用广播了,全部可以准确转发。当然,每个机器的 IP 地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为转发表,是有一个过期时间的。

交换机与 VLAN:办公室太复杂,我要回学校

拓扑结构是怎么形成的

常见到的办公室大多是一排排桌子,每个桌子都有网口,一排就有十几个网口,一个楼层就会有几十个甚至上百个网口。如果算上所有楼层,这个场景自然比宿舍里的复杂多了。

这个时候,一个交换机肯定不够用,需要多台交换机,交换机之间连接起来,就形成一个稍微复杂的拓扑结构

下图中,两台交换机连接着三个局域网,每个局域网上都有多台机器。如果机器 1 只知道机器 4 的 IP 地址,当它想要访问机器 4,把包发出去的时候,它必须要知道机器 4 的 MAC 地址。

于是机器 1 发起广播,机器 2 收到这个广播,但是这不是找它的,所以没它什么事。交换机 A 一开始是不知道任何拓扑信息的,在它收到这个广播后,采取的策略是,除了广播包来的方向外,它还要转发给其他所有的网口。于是机器 3 也收到广播信息了,但是这和它也没什么关系。当然,交换机 B 也是能够收到广播信息的,但是这时候它也是不知道任何拓扑信息的,因而也是进行广播的策略,将包转发到局域网三。这个时候,机器 4 和机器 5 都收到了广播信息。机器 4 主动响应说,这是找我的,这是我的 MAC 地址。于是一个 ARP 请求就成功完成了。

在上面的过程中,交换机 A 和交换机 B 都是能够学习到这样的信息:机器 1 是在左边这个网口的。当了解到这些拓扑信息之后,情况就好转起来。当机器 2 要访问机器 1 的时候,机器 2 并不知道机器 1 的 MAC 地址,所以机器 2 会发起一个 ARP 请求。这个广播消息会到达机器 1 ,也同时会到达交换机 A 。这个时候交换机 A 已经知道机器 1 是不可能在右边的网口的,所以这个广播信息就不会广播到局域网二和局域网三。

当机器 3 要访问机器 1 的时候,也需要发起一个广播的 ARP 请求。这个时候交换机 A 和交换机 B 都能够收到这个广播请求。交换机 A 当然知道主机 A 是在左边这个网口的,所以会把广播消息转发到局域网一。同时,交换机 B 收到这个广播消息之后,由于它知道机器 1 是不在右边这个网口的,所以不会将消息广播到局域网三。

如何解决常见的环路问题

当整个拓扑结构复杂了,这么多网线,绕过来绕过去,不可避免地会出现一些意料不到的情况。其中常见的问题就是环路问题。

下面途中,就出现了环路。

想象一下机器 1 访问机器 2 的过程。一开始,机器 1 并不知道机器 2 的 MAC 地址,所以它需要发起一个 ARP 的广播。广播到达机器 2,机器 2 会把 MAC 地址返回来,看起来没有这两个交换机什么事情。

但是问题来了,这两个交换机还是都能够收到广播包的。交换机 A 一开始是不知道机器 2 在哪个局域网的,所以它会把广播消息放到局域网二,在局域网二广播的时候,交换机 B 右边这个网口也是能够收到广播消息的。交换机 B 会将这个广播息信息发送到局域网一。局域网一的这个广播消息,又会到达交换机 A 左边的这个接口。交换机 A 这个时候还是不知道机器 2 在哪个局域网,于是将广播包又转发到局域网二。左转左转左转,好像是个圈。

并且这种情况,交换机是学习不到拓扑结构的,为什么?

机器 1 的广播包到达交换机 A 和交换机 B 的时候,本来两个交换机都学会了机器 1 是在局域网一的,但是当交换机 A 将包广播到局域网二之后,交换机 B 右边的网口收到了来自交换机 A 的广播包。根据学习机制,这彻底损坏了交换机 B 的三观,刚才机器 1 还在左边的网口呢,怎么又出现在右边的网口呢?哦,那肯定是机器 1 换位置了,于是就误会了,交换机 B 就学会了,机器 1 是从右边这个网口来的,把刚才学习的那一条清理掉。
同理,交换机 A 右边的网口,也能收到交换机 B 转发过来的广播包,同样也误会了,于是也学会了,机器 1 从右边的网口来,不是从左边的网口来。

然而当广播包从左边的局域网一广播的时候,两个交换机再次刷新三观,原来机器 1 是在左边的,过一会儿,又发现不对,是在右边的,过一会,又发现不对,是在左边的。

破除环路,STP 协议中那些难以理解的概念

计算机网络中,生成树的算法叫作 STP,全称 Spanning Tree Protocol。

  • Root Bridge,也就是 根交换机 。这个比较容易理解,可以比喻为 “掌门” 交换机,是某棵树的老大,是掌门,最大的大哥。
  • Designated Bridges,有的翻译为 指定交换机 。这个比较难理解,可以想像成一个 “小弟”,对于树来说,就是一棵树的树枝。所谓 “指定” 的意思是,我拜谁做大哥,其他交换机通过这个交换机到达根交换机,也就相当于拜他做了大哥。这里注意是树枝,不是叶子,因为叶子往往是主机。
  • Bridge Protocol Data Units (BPDU) 网桥协议数据单元 。可以比喻为 “相互比较实力” 的协议。行走江湖,比的就是武功,拼的就是实力。当两个交换机碰见的时候,也就是相连的时候,就需要互相比一比内力了。BPDU 只有掌门能发,已经隶属于某个掌门的交换机只能传达掌门的指示。
  • Priority Vector,优先级向量 。可以比喻为实力(值越小越牛)。实力是啥?就是一组 ID 数目,[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。为什么这样设计呢?
    这是因为要看怎么来比实力。先看 Root Bridge ID。拿出老大的 ID 看看,发现掌门一样,那就是师兄弟;再比 Root Path Cost,也即我距离我的老大的距离,也就是拿和掌门关系比,看同一个门派内谁和老大关系铁;最后比 Bridge ID,比我自己的 ID,拿自己的本事比。

VLAN

机器多了,交换机也多了,就算交换机比 Hub 智能一些,但是还是难免有广播的问题,一大波机器,相关的部门、不相关的部门,广播一大堆,性能就下来了。公司有不同的部门,有的部门需要保密的,比如人事部门,肯定要讨论升职加薪的事儿。由于在同一个广播域里面,很多包都会在一个局域网里面飘啊飘,碰到了一个会抓包的程序员,就能抓到这些包,如果没有加密,就能看到这些敏感信息了。怎么办?两种办法:

  • 物理隔离,每个部门有单独的交换机,配置单独的子网,这样部门之间的沟通就需要路由器了。但是有的部门人多,有的人少,如果每个部门有单独的交换机,口多了浪费,少了又不够用。
  • 虚拟隔离,也就是 VLAN。或者叫虚拟局域网。使用 VLAN,一个交换机上会连属于多个局域网的机器。

交换机怎么区分哪个机器属于哪个局域网?

只需要在原来的二层的头上加一个 TAG,里面有一个 VLAN ID,一共 12 位。

如果我们买的交换机是支持 VLAN 的,当这个交换机把二层的头取下来的时候,就能够识别这个 VLAN ID。这样只有相同 VLAN 的包,才会互相转发,不同 VLAN 的包,是看不到的。

可以设置交换机每个口所属的 VLAN。如果某个口坐的是程序员,他们属于 VLAN 10;如果某个口坐的是人事,他们属于 VLAN 20;如果某个口坐的是财务,他们属于 VLAN30。这样,财务发的包,交换机只会转发到 VLAN 30 的口上。

对于交换机来讲,每个 VLAN 的口都是可以重新设置的。一个财务走了,把他所在的作为的口从 VLAN 30 移除掉,来了一个程序员,坐在财务的位置上,就把这个口设置为 VLAN 10,十分灵活。

对于支持 VLAN 的交换机,有一种口叫作Trunk 口。它可以转发属于任何 VLAN 的口。交换机之间可以通过这种口相互连接。

ICMP 与 ping:投石问路的侦察兵

ping 是基于 ICMP 协议工作的。ICMP 全称 Internet Control Message Protocol,就是互联网控制报文协议

  • ICMP 相当于网络世界的侦察兵。两种类型的 ICMP 报文,一种是主动探查的查询报文,一种异常报告的差错报文。
  • ping 使用查询报文,Traceroute 使用差错报文。

世界这么大,我想出网关:欧洲十国游与玄奘西行

怎么在宿舍上网?

路由器,路由器会有内网网口和外网网口。把外网网口的线插到校园网的网口上,将这个外网网口配置成和网管部的一样。内网网口连上你们宿舍的所有的电脑。这种情况下,如果你们宿舍的人要上网,就需要一直开着路由器。

在任何一台机器上,当要访问另一个 IP 地址的时候,都会使用 CIDR 和子网掩码先判断是否在同一个网段。

  • 如果是同一个网段,例如,你访问你旁边的兄弟的电脑,那就没网关什么事情,直接将源地址和目标地址放入 IP 头中,然后通过 ARP 获得 MAC 地址,将源 MAC 和目的 MAC 放入 MAC 头中,发出去就可以了。
  • 如果不是同一网段,例如,你要访问你们校园网里面的 BBS,该怎么办?这就需要发往默认网关 Gateway。Gateway 的地址一定是和源 IP 地址是一个网段的。往往不是第一个,就是第二个。例如 192.168.1.0/24 这个网段,Gateway 往往会是 192.168.1.1/24 或者 192.168.1.2/24

如何发往默认网关呢?网关不是和源 IP 地址是一个网段的么?这个过程就和发往同一个网段的其他机器是一样的:将源地址和目标 IP 地址放入 IP 头中,通过 ARP 获得网关的 MAC 地址,将源 MAC 和网关的 MAC 放入 MAC 头中,发送出去。网关所在的端口,例如192.168.1.1/24将网络包收进来,然后接下来怎么做,就完全看网关的了。

网关往往是一个路由器,是一个三层转发的设备。啥叫三层设备?前面也说过了,就是把 MAC 头和 IP 头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。

路由器是一台设备,它有五个网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的 IP 地址都和局域网的 IP 地址相同的网段,每只手都是它握住的那个局域网的网关。

静态路由是什么?

静态路由,其实就是在路由器上,配置一条一条规则。这些规则包括:想访问 BBS 站(它肯定有个网段),从 2 号口出去,下一跳是 IP2;想访问教学视频站(它也有个自己的网段),从 3 号口出去,下一跳是 IP3,然后保存在路由器里。

IP 头和 MAC 头哪些变、哪些不变?

MAC 地址是一个局域网内才有效的地址。因而,MAC 地址只要过网关,就必定会改变,因为已经换了局域网。两者主要的区别在于 IP 地址是否改变。不改变 IP 地址的网关,
我们称为转发网关;改变 IP 地址的网关,我们称为NAT 网关

“欧洲十国游” 型

服务器 A 要访问服务器 B。首先,192.168.4.101和我不在同一个网段的,需要先发给网关。那网关是谁呢?已经静态配置好了,网关是192.168.1.1。发送 ARP 获取网关的 MAC 地址,然后发送包。包的内容是这样的:

源 MAC:服务器 A 的 MAC
目标 MAC:192.168.1.1 这个网口的 MAC
源 IP:192.168.1.101
目标 IP:192.168.4.101

包到达192.168.1.1这个网口,发现 MAC 一致,将包收进来,开始思考往哪里转发。

在路由器 A 中配置了静态路由之后,要想访问192.168.4.0/24,要从192.168.56.1这个口出去,下一跳为192.168.56.2。发送 ARP 获取192.168.56.2的 MAC 地址,然后发送包。
包的内容是这样的:

源 MAC:192.168.56.1 的 MAC 地址
目标 MAC:192.168.56.2 的 MAC 地址
源 IP:192.168.1.101
目标 IP:192.168.4.101

包到达192.168.56.2这个网口,发现 MAC 一致,将包收进来,开始思考往哪里转发。

路由器 B 中配置了静态路由,要想访问192.168.4.0/24,要从192.168.4.1这个口出去,没有下一跳了。因为我右手这个网卡,就是这个网段的,我是最后一跳了。发送 ARP 获取192.168.4.101的 MAC 地址,然后发送包。包的内容是这样的:

源 MAC:192.168.4.1 的 MAC 地址
目标 MAC:192.168.4.101 的 MAC 地址
源 IP:192.168.1.101
目标 IP:192.168.4.101

包到达服务器 B,MAC 地址匹配,将包收进来。

这个过程可以看出,每到一个新的局域网,MAC 都是要变的,但是 IP 地址都不变。在 IP 头里面,不会保存任何网关的 IP 地址。所谓的下一跳是,某个 IP 要将这个 IP 地址转换为 MAC 放入 MAC 头。

之所以将这种模式比喻称为欧洲十国游,是因为在整个过程中,IP 头里面的地址都是不变的。IP 地址在三个局域网都可见,在三个局域网之间的网段都不会冲突。在三个网段之间传输包,IP 头不改变。 这就像在欧洲各国之间旅游,一个签证就能搞定。

“玄奘西行” 型

遇见的第一个问题是,局域网之间没有商量过,各定各的网段,因而 IP 段冲突了。最左面大唐的地址是192.168.1.101,最右面印度的地址也是192.168.1.101,如果单从 IP 地址上看,简直是自己访问自己,其实是大唐的192.168.1.101要访问印度的192.168.1.101。怎么解决这个问题呢?既然局域网之间没有商量过,你们各管各的,那到国际上,也即中间的局域网里面,就需要使用另外的地址。就像出国,不能用咱们自己的身份证,而要改用护照一样,玄奘西游也要拿着专门取经的通关文牒,而不能用自己国家的身份证。

首先,目标服务器 B 在国际上要有一个国际的身份,我们给它一个192.168.56.2。在网关 B 上记下来,国际身份192.168.56.2对应国内身份192.168.1.101。凡是要访问192.168.56.2,都转成192.168.1.101

源服务器 A 要访问目标服务器 B,要指定的目标地址为192.168.56.2。这是它的国际身份。192.168.56.2和我不是一个网段的,因而需要发给网关192.168.1.1,发送 ARP 获取网关的 MAC 地址,然后发送包。包的内容是这样的:

源 MAC:服务器 A 的 MAC
目标 MAC:192.168.1.1 这个网口的 MAC
源 IP:192.168.1.101
目标 IP:192.168.56.2

路由器 A 中配置了静态路由:要想访问192.168.56.2/24,要从192.168.56.1这个口出去,没有下一跳了,因为我右手这个网卡,就是这个网段的,我是最后一跳了。发送 ARP
获取192.168.56.2的 MAC 地址。

当网络包发送到中间的局域网的时候,服务器 A 也需要有个国际身份,因而在国际上,源 IP 地址也不能用192.168.1.101,需要改成192.168.56.1。发送包的内容是这样的:

源 MAC:192.168.56.1 的 MAC 地址
目标 MAC:192.168.56.2 的 MAC 地址
源 IP:192.168.56.1
目标 IP:192.168.56.2

路由器 B 是一个 NAT 网关,它上面配置了,要访问国际身份192.168.56.2对应国内身份192.168.1.101,于是改为访问192.168.1.101

路由器 B 中配置了静态路由:要想访问192.168.1.0/24,要从192.168.1.1这个口出去,没有下一跳了,因为我右手这个网卡,就是这个网段的,我是最后一跳了。发送 ARP 获取192.168.1.101的 MAC 地址,然后发送包。内容是这样的:

源 MAC:192.168.1.1 的 MAC 地址
目标 MAC:192.168.1.101 的 MAC 地址
源 IP:192.168.56.1
目标 IP:192.168.1.101

服务器 B 接收的包可以看出,源 IP 为服务器 A 的国际身份,因而发送返回包的时候,也发给这个国际身份,由路由器 A 做 NAT,转换为国内身份。

这个过程可以看出,IP 地址也会变。这个过程用英文说就是Network Address Translation,简称NAT

第二种方式我们经常见,现在大家每家都有家用路由器,家里的网段都是192.168.1.x,所以你肯定访问不了你邻居家的这个私网的 IP 地址的。所以,当我们家里的包发出去的时候,都被家用路由器 NAT 成为了运营商的地址了。

路由协议:西出网关无故人,敢问路在何方

如何配置路由

路由器就是一台网络设备,它有多张网卡。当一个入口的网络包送到路由器时,它会根据一个本地的转发信息库,来决定如何正确地转发流量。这个转发信息库通常被称为路由表

一张路由表中会有多条路由规则。每一条规则至少包含这三项信息。

  • 目的网络:这个包想去哪儿?
  • 出口设备:将包从哪个口扔出去?
  • 下一跳网关:下一个路由器的地址。

根据目的 IP 地址来配置路由,通过 route 命令和 ip route 命令。

配置策略路由

除了可以根据目的 ip 地址配置路由外,还可以根据多个参数来配置路由,这就称为策略路由

可以配置多个路由表,可以根据源 IP 地址、入口设备、TOS 等选择路由表,然后在路由表中查找路由。这样可以使得来自不同来源的包走不同的路由。

动态路由算法

上面的都是静态路由。但是网络环境复杂并且多变,使用动态路由路由器,可以根据路由协议算法生成动态路由表,随网络运行状况的变化而变化。

可以想象唐僧西天取经,无论是一个国家内部,还是国家之间,我们都可以将复杂的路径,抽象为一种叫作图的数据结构。至于唐僧西行取经,肯定想走得路越少越好,道路越短越好,因而这就转化成为如何在途中找到最短路径的问题

  1. 距离矢量路由算法
    基于 Bellman-Ford 算法,这种算法的基本思路是,每个路由器都保存一个路由表,包含多行,每行对应网络中的一个路由器,每一行包含两部分信息,一个是要到目标路由器,
    从那条线出去,另一个是到目标路由器的距离。

  2. 链路状态路由算法

基本思路是:当一个路由器启动的时候,首先是发现邻居,向邻居 say hello,邻居都回复。然后计算和邻居的距离,发送一个 echo,要求马上返回,除以二就是距离。然后将自
己和邻居之间的链路状态包广播出去,发送到整个网络的每个路由器。这样每个路由器都能够收到它和邻居之间的关系的信息。因而,每个路由器都能在自己本地构建一个完整的图,然后针对这个图使用 Dijkstra 算法,找到两点之间的最短路径。

UDP 协议:因性善而简单,难免碰到 “城会玩”

所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性

  • TCP 提供可靠交付。通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。
  • UDP 继承了 IP 包的特性,不保证不丢失,不保证按顺序到达。IP 包是没有任何可靠性保证的。
  • TCP 是面向字节流的。发送的时候发的是一个流,没头没尾。IP 包可不是一个流,而是一个个的 IP 包。之所以变成了流,这也是 TCP 自己的状态维护做的事情。
  • UDP 继承了 IP 的特性,基于数据报的,一个一个地发,一个一个地收。
  • TCP 是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。
  • UDP 就不会,应用让我发,我就发,管它洪水滔天。
  • TCP 其实是一个有状态服务,通俗地讲就是有脑子的,里面精确地记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。
  • UDP 则是无状态服务。

网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。UDP 完全继承了这些特性。

UDP 包头

IP 头里面有个 8 位协议,这里会存放,数据里面到底是 TCP 还是 UDP。解析玩 UDP,一台机器上跑着这么多的应用程序,应该给谁?无论应用程序使用 TCP 还是 UDP 传数据,都要监听一个端口。正是这个端口,用来区分应用程序。

三个特点

  • 第一,沟通简单,相信网络通路默认就是很容易送达的,不容易被丢弃的。
  • 第二,轻信他人。它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据。也可以传给任何人数据。
  • 第三,愣头青,做事不懂权变。不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。

三个场景

  • 需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。
  • 不需要一对一沟通,建立连接,而是可以广播的应用。
  • 需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。

TCP 协议(上):因性恶而复杂,先恶后善反轻松

TCP 包头

  • 包的序号,为了解决乱序的问题。
  • 确认序号,发出去的包要确认,不然怎么知道有没有收到。没有收到就重发。为了解决不丢包的问题。
  • 状态位,SYN 是发起一个连接,ACK 是回复,RST 是重连,FIN 是结束连接。
  • 窗口大小,TCP 要做流量控制,双方各自声明一个窗口,表示自己当前能够的处理能力,避免发的太快或者太慢。TCP 还会做拥塞控制。

三次握手

为什么是三次,不是两次?为什么不是四次?因为通信双方都要保证通信可以有来有回。例如,A 和 B,A 发起一个连接(第一次握手),B 收到请求,并发送应答给 A(第二次握手),说明 B 可以建立连接,但是 B 的应答包,B 不知道 A 是否收到,可能丢失了,所以 A 需要应答 B 的应答包(第三次握手),B 收到这个消息,下能确认连接建立。

这也就是说,其实四次握手甚至更多也是可以的,但是只要双方的消息有去有回,就基本可以了。

三次握手除了建立连接,还要沟通 TCP 包的序号。A 告诉 B 我发起的请求从哪个序号开始,B 告诉 A,B 发起的包的序号从哪个开始。
为什么序号不都从 1 开始?为了防止冲突。

例如,A 连上 B,发送了 1,2,3 三个包,但是 3 丢失了或者绕路了,重新发送,但是后来 A 又掉线了,重新连上 B 后,又从 1 开始发,但是只发了 1,2,但是上次绕路的那个 3 又回来了,发给了 B,B 自然认为,这就是下一个包,于是发生了错误。

起始序号是随时间变化的,32 位,每 4S 加一。

四次挥手

A:B 啊,我不想玩了。
B:哦,你不想玩了啊,我知道了。

这个时候,还只是 A 不想玩了,也即 A 不会再发送数据,但是 B 能不能在 ACK 的时候,直接关闭呢?当然不可以了,很有可能 A 是发完了最后的数据就准备不玩了,但是 B 还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。

B:A 啊,好吧,我也不玩了,拜拜。
A:好的,拜拜

这样整个连接就关闭了。

但是这个过程有没有异常情况呢?

一种情况是,A 说完 “不玩了” 之后,直接跑路,是会有问题的,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。另一种情况是,A 说完 “不玩了”,B 直接跑路,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。

A 收到 “B 说知道了”,就进入FIN_WAIT_2的状态,如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整tcp_fin_timeout这个参数,设置一个超时时间。

如果 B 没有跑路,发送了 “B 也不玩了” 的请求到达 A 时,A 发送 “知道 B 也不玩了” 的 ACK 后,从 FIN_WAIT_2 状态结束,按说 A 可以跑路了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个 “B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了” 会重发的,A 会重新发一个 ACK 并且足够时间到达 B。

A 直接跑路还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都死翘翘,再空出端口来。

等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。

还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这个时候 A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑了。

TCP 协议(下):西行必定多妖孽,恒心智慧消磨难

如何实现一个靠谱的协议?

为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)

为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录(为了效率,不能每发送一个包,要等到收到了应答,再发送下一个,所以现将事情几下来,办完一件回复一件)。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

  • 第一部分:发送了并且已经确认的。这部分就是你交代下属的,并且也做完了的,应该划掉的。
  • 第二部分:发送了并且尚未确认的。这部分是你交代下属的,但是还没做完的,需要等待做完的回复之后,才能划掉。
  • 第三部分:没有发送,但是已经等待发送的。这部分是你还没有交代给下属,但是马上就要交代的。
  • 第四部分:没有发送,并且暂时还不会发送的。这部分是你还没有交代给下属,而且暂时还不会交代给下属的。

为什么要区分第三部分和第四部分?
因为流量控制,要考虑接收端的处理能力。

在 TCP 里,接收端会给发送端报一个窗口的大小,叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

对于接收端来讲,它的缓存里记录的内容要简单一些。

  • 第一部分:接受并且确认过的。也就是我领导交代给我,并且我做完的。
  • 第二部分:还没接收,但是马上就能接收的。也即是我自己的能够接受的最大工作量。
  • 还没接收,也没法接收的。也即超过工作量的部分,实在做不完。

  • MaxRcvBuffer:最大缓存的量;
  • LastByteRead 之后是已经接收了,但是 还没被应用层读取的
  • NextByteExpected 是第一部分和第二部分的分界线。

NextByteExpected 和 LastByteRead 的差其实是还没被应用层读取的部分占用掉的 MaxRcvBuffer 的量,我们定义为 A。AdvertisedWindow 其实是 MaxRcvBuffer 减去 A。也就是:
AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)

其中第二部分里面,由于受到的包可能不是顺序的,会出现空挡,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了

顺序问题与丢包问题

刚才的图,在发送端来看,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。

在接收端来看,1、2、3、4、5 是已经完成 ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是没有 ACK 的。

当前的状态如下:

  • 1、2、3 没有问题,双方达成了一致。
  • 4、5 接收方说 ACK 了,但是发送方还没收到,有可能丢了,有可能在路上。
  • 6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到,出现了乱序,缓存着但是没办法 ACK。

顺序问题和丢包问题都有可能发生。

确认与重发的机制

假设 4 的确认到了,不幸的是,5 的 ACK 丢了,6、7 的数据包丢了,这该怎么办?

超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。

估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断的变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive RetransmissionAlgorithm)

如果 7 丢了,重传之后又丢了,TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送

快速重传

超时触发重传存在的问题是,超时周期可能相对较长。有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。

Selective Acknowledgment (SACK)

这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

流量控制问题

流量控制机制,在对于包的确认中,同时会携带一个窗口的大小

先假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。

这个时候,假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。

当对于包 5 的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第 14 个包才可以发送。

如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。

我们假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。

这个新的窗口 8 通过 6 的确认消息到达发送端的时候,你会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从 9 改成了 8。

如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。

当这个窗口通过包 14 的确认到达发送端的时候,发送端的窗口也调整为 0,停止发送。

如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,
然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口

拥塞控制问题

拥塞控制的问题,也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满

LastByteSent - LastByteAcked <= min {cwnd, rwnd},是拥塞窗口和滑动窗口共同控制发送的速度。

发送方怎么判断网络是不是满了?TCP 发送包常被比喻为往一个水管里面灌水,而TCP 的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽

水管有粗细,网络有带宽,也即每秒钟能够发送多少数据;水管有长度,端到端有时延。在理想状态下,水管里面水的量 = 水管粗细 x 水管长度。对于到网络上,通道的容量 = 带宽 × 往返延迟

如果我们设置发送窗口,使得发送但未确认的包为为通道的容量,就能够撑满整个管道。

如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。已经过去了 8s,则 8 个包都发出去了,其中前 4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为 8 个包,正好等于带宽,也即每秒发送 1 个包,乘以来回时间 8s。

TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?
如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动

一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是指数性的增长

有一个值 ssthresh 为 65535 个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,可能快满了,再慢下来。

每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞,这时候一般就会一下子降低倒水的速度,等待溢出的水慢慢渗下去。拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 cwnd/2,将 cwnd 设为 1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。

当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh +3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,TCP 的拥塞控制主要来避免的两个现象都是有问题的。

  • 第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
  • 第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。

为了优化这两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

HTTP 协议:看个新闻原来这么麻烦

http://www.163.com是个 URL,叫作统一资源定位符。之所以叫统一,是因为它是有格式的。HTTP称为协议,www.163.com是一个域名,表示互联网上的一个位置。有的 URL 会有更详细的位置标识,例如http://www.163.com/index.html。正是因为这个东西是统一的,所以当你把这样一个字符串输入到浏览器的框里的时候,浏览器才知道如何进行统一处理。

HTTP 请求的准备

  • 浏览器会将 www.163.com 这个域名发送给 DNS 服务器,让它解析为 IP 地址。
  • HTTP 是基于 TCP 协议的,要先建立 TCP 连接,目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样建立的 TCP 连接,就可以在多次请求中复用。

HTTP 请求的构建

建立了连接以后,浏览器就要发送 HTTP 的请求。

HTTP 的报文的三大部分:

  • 请求行,
  • 请求的首部,
  • 请求的正文实体。

请求行

URL 就是http://www.163.com,版本为 HTTP 1.1。方法有几种类型,get,post,put,delete

首部字段

首部是 key value,通过冒号分隔。重点缓存,为啥要使用缓存?因为一个非常大的页面有很多东西。

例如,我浏览一个商品的详情,里面有这个商品的价格、库存、展示图片、使用手册等等。展示图片会保持较长时间不变,而库存会根据用户购买的情况经常改变。如果图片非常大,而库存数非常小,如果我们每次要更新数据的时候都要刷新整个页面,对于服务器的压力就会很大。

对于这种高并发场景下的系统,在真正的业务逻辑之前,都需要有个接入层,将这些静态资源的请求拦在最外面。

和这一节关系比较大的就是 Nginx 这一层,它如何处理 HTTP 协议呢?对于静态资源,有 Vanish 缓存层。当缓存过期的时候,才会访问真正的 Tomcat 应用集群。

在 HTTP 头里面,Cache-control是用来控制缓存的。

HTTP 请求的发送

HTTP 协议是基于 TCP 协议的,所以它使用面向连接的方式发送请求,通过 stream 二进制流的方式传给对方。当然,到了 TCP 层,它会把二进制流变成一个的报文段发送给服务器。IP 层 -> ARP 获取 MAC -> 路由器 -> 找到机器 -> 解析 MAC IP TCP 根据端口号 -> 找到 HTTP 服务,

HTTP 返回的构建

返回报文的三大部分:

  • 状态行,
  • 首部
  • 实体。

状态码会反应 HTTP 请求的结果。“200” 意味着大吉大利;而我们最不想见的,就是 “404”,也就是 “服务端无法响应这个请求”。

首部是 key value,通过冒号分隔

HTTP 2.0

HTTP 1.1 在应用层以纯文本的形式进行通信。每次通信都要带完整的 HTTP 的头,而且不考虑 pipeline 模式的话,每次的过程总是像上面描述的那样一去一回。这样在实时性、并发性上都存在问题。

为了解决这些问题,HTTP 2.0 会对 HTTP 的头进行一定的压缩,将原来每次都要携带的大量 key value 在两端建立一个索引表,对相同的头只发送索引表中的索引。

HTTP 2.0 协议将一个 TCP 的连接中,切分成多个流,每个流都有自己的 ID,而且流可以是客户端发往服务端,也可以是服务端发往客户端。它其实只是一个虚拟的通道。流是有优先级的。

HTTP 2.0 还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。常见的帧有 Header 帧,用于传输 Header 内容,并且会开启一个新的流。再就是 Data 帧,用来传输正文实体。多个 Data 帧属于同一个流。

通过这两种机制,HTTP 2.0 的客户端可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输。这些帧可以打散乱序发送,然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。

举一个例子:

假设我们的一个页面要发送三个独立的请求,一个获取 css,一个获取 js,一个获取图片 jpg。如果使用 HTTP 1.1 就是串行的,但是如果使用 HTTP 2.0,就可以在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一对一对应。

HTTP 2.0 成功解决了 HTTP 1.1 的队首阻塞问题,同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条 TCP 连接来实现并行请求与响应;减少了 TCP 连接数对服务器性能的影响,同时将页面的多个数据 css、js、jpg 等通过一个数据链接进行传输,能够加快页面组件的传输速度。

HTTP 2.0 其实是将三个请求变成三个流,将数据分成帧,乱序发送到一个 TCP 连接中。

HTTP 2.0 成功解决了 HTTP 1.1 的队首阻塞问题,同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条 TCP 连接来实现并行请求与响应;减少了 TCP 连接数对服务器性能的影响,同时将页面的多个数据 css、js、jpg 等通过一个数据链接进行传输,能够加快页面组件的传输速度。

QUIC

QUIC 协议通过基于 UDP 自定义的类似 TCP 的连接、重试、多路复用、流量控制技术,进一步提升性能。

HTTPS 协议:点外卖的过程原来这么复杂

加密分为两种方式一种是对称加密(加密和解密使用的是同一个密钥),一种是非对称加密(加密使用的密钥和解密使用的密钥是不相同的。一把是作为公开的公钥,另一把是作为谁都不能给的私钥。
公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密)。

HTTPS 的工作模式

非对称加密在性能上不如对称加密,那是否能将两者结合起来呢?例如,公钥私钥主要用于传输对称加密的秘钥,而真正的双方大数据量的通信都是通过对称加密进行的。

证书校验这一步,一般浏览器的” 证书管理器” 会有” 受信任的根证书颁发机构” 列表。如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。
如果这张数字证书不是由受信任的机构颁发的,浏览器会发出另一种警告。

客户端拿到服务端证书之后,从自己信任的 CA 仓库中,拿 CA 的证书里面的公钥去解密外卖网站的证书。如果能够成功,则说明外卖网站是可信的。这个过程中,你可能会不断往上追溯 CA、CA 的 CA、CA 的 CA 的 CA,反正直到一个授信的 CA,就可以了。

流媒体协议:如何在直播里看到美女帅哥?

无论是直播还是点播,其实都是对于视频数据的传输。

视频是什么?其实就是快速播放一连串连续的图片。

每一张图片,我们称为一。只要每秒钟帧的数据足够多,也即播放得足够快。比如每秒 30 帧,以人的眼睛的敏感程度,是看不出这是一张张独立的图片的,这就是我们常说的帧率(FPS)

每一张图片,都是由像素组成的,假设为 1024*768(这个像素数不算多)。每个像素由 RGB 组成,每个 8 位,共 24 位。

我们来算一下,每秒钟的视频有多大?

30 帧× 1024 × 768 × 24 = 566,231,040Bits = 70,778,880Bytes

如果一分钟呢?4,246,732,800Bytes,已经是 4 个 G 了。

这个数据量实在是太大,根本没办法存储和传输。如果这样存储,你的硬盘很快就满了;如果这样传输,那多少带宽也不够用啊!

编码

于是出现了编码,就是看如何用尽量少的 Bit 数保存视频,使播放的时候画面看起来仍然很精美。编码是一个压缩的过程

之所以能够对视频流中的图片进行压缩,因为视频和图片有这样一些特点。

  • 空间冗余 :图像的相邻像素之间有较强的相关性,一张图片相邻像素往往是渐变的,不是突变的,没必要每个像素都完整地保存,可以隔几个保存一个,中间的用算法计算出来。
  • 时间冗余 :视频序列的相邻图像之间内容相似。一个视频中连续出现的图片也不是突变的,可以根据已有的图片进行预测和推断。
  • 视觉冗余 :人的视觉系统对某些细节不敏感,因此不会每一个细节都注意到,可以允许丢失一些数据。
  • 编码冗余 :不同像素值出现的概率不同,概率高的用的字节少,概率低的用的字节多,类似霍夫曼编码(Huffman Coding)的思路。

编码过程:

视频编码的标准

ITU-T(国际电信联盟电信标准化部门,ITU Telecommunication Standardization Sector)与 MPEG 联合制定了H.264/MPEG-4 AVC

经过编码之后,一帧一帧的图像,就变成了二进制,这个二进制可以放在一个文件里面,按照一定的格式保存起来,例如 RMVB 和 MP4。

如何在直播里看到帅哥美女?

这个二进制也可以通过某种网络协议进行封装,放在互联网上传输,这个时候就可以进行网络直播了。

网络协议将编码好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为接流

服务端接到视频流之后,可以对视频流进行一定的处理,例如转码,也即从一个编码格式,转成另一种格式。因为观众使用的客户端千差万别,要保证他们都能看到直播。

流处理完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为拉流

如果有非常多的观众,同时看一个视频直播,那都从一个服务器上拉流,压力太大了,因而需要一个视频的分发网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。当观众的客户端将视频流拉下来之后,就需要进行解码,也即通过上述过程的逆过程,将一串串看不懂的二进制,再转变成一帧帧生动的图片,在客户端播放出来,这样你就能看到美女帅哥啦。

直播过程:

编码:如何将丰富多彩的图片变成二进制流?

虽然我们说视频是一张张图片的序列,但是如果每张图片都完整,就太大了,因而会将视频序列分成三种帧:

  • I 帧 ,也称关键帧。里面是完整的图片,只需要本帧数据,就可以完成解码。
  • P 帧 ,前向预测编码帧。P 帧表示的是这一帧跟之前的一个关键帧(或 P 帧)的差别,解码时需要用之前缓存的画面,叠加上和本帧定义的差别,生成最终画面。
  • B 帧 ,双向预测内插编码帧。B 帧记录的是本帧与前后帧的差别。要解码 B 帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的数据与本帧数据的叠加,取得最终的画面。

I 帧最完整,B 帧压缩率最高,而压缩后帧的序列,应该是在 IBBP 的间隔出现的。这就是通过时序进行编码。

在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块,可以方便进行空间上的编码

尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的网络提取层单元(NALU,Network Abstraction Layer Unit)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。

一个视频,可以拆分成一系列的帧,每一帧拆分成一系列的片,每一片都放在一个 NALU 里面,NALU 之间都是通过特殊的起始标识符分隔,在每一个 I 帧的第一片前面,要插入单独保存 SPS 和 PPS 的 NALU,
最终形成一个长长的 NALU 序列

每一个 NALU 首先是一个起始标识符,用于标识 NALU 之间的间隔

NALU 头里面,主要的内容是类型 NAL Type:

  • 0x07 表示 SPS,是序列参数集,包括一个图像序列的所有信息,如图像尺寸、视频格式等。
  • 0x08 表示 PPS,是图像参数集,包括一个图像的所有分片的所有相关信息,包括图像类型、序列号等。

Payload 里面是 NALU 承载的数据。

推流:如何把数据流打包传输到对端

使用RTMP 协议将这个二进制的流打包成网络包进行发送。这就进入了第二个过程,推流

RTMP 是基于 TCP 的,因而肯定需要双方建立一个 TCP 的连接。在有 TCP 的连接的基础上,还需要建立一个 RTMP 的连接

RTMP 为什么需要建立一个单独的连接?

因为它们需要商量一些事情,保证以后的传输能正常进行。主要就是两个事情,一个是版本号,如果客户端、服务器的版本号不一致,则不能工作。另一个就是时间戳,视频播放中,时间是很重要的,后面的数据流互通的时候,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。

握手之后,双方需要互相传递一些控制信息,例如 Chunk 块的大小、窗口大小等。

真正传输数据的时候,还是需要创建一个流 Stream,然后通过这个 Stream 来推流 publish。

推流的过程,就是将 NALU 放在 Message 里面发送,这个也称为 RTMP Packet 包。Message 的格式就像这样。

RTMP 在收发数据的时候并不是以 Message 为单位的,而是把 Message 拆分成 Chunk 发送,而且必须在一个 Chunk 发送完成之后,才能开始发送下一个 Chunk。每个 Chunk 中都带有 Message ID,表示属于哪个 Message,接收端也会按照这个 ID 将 Chunk 组装成 Message。

数据推送到流媒体服务器过程:

然后直播的观众就可以通过 RTMP 协议从流媒体服务器上拉取,但是这么多的用户量,都去同一个地方拉取,服务器压力会很大,而且用户分布在全国甚至全球,如果都去统一的一个地方下载,也会时延比较长,需要有分发网络

拉流:观众的客户端如何看到视频

P2P 协议:我下小电影,99% 急死你

下载最简单的方式 HTTP,但是下载很慢,可以使用 FTP(文件传输协议)。

FTP 采用两个 TCP 连接来传输一个文件:

  • 控制连接 :服务器以被动的方式,打开众所周知用于 FTP 的端口 21,客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。
    常用的命令有:list—— 获取文件目录;reter—— 取一个文件;store—— 存一个文件。
  • 数据连接 :每当一个文件在客户端与服务器之间传输时,就创建一个数据连接。

P2P 是什么

无论是 HTTP 的方式,还是 FTP 的方式,都有一个比较大的缺点,就是难以解决单一服务器的带宽压力,因为它们使用的都是传统的客户端服务器的方式。

P2P 就是 peer-to-peer。资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们姑且称为 peer。

想要下载一个文件的时候,你只要得到那些已经存在了文件的 peer,并和这些 peer 之间,建立点对点的连接,而不需要到中心服务器上,就可以就近下载文件。一旦下载了文件,你也就成为 peer 中的一员,你旁边的那些机器,也可能会选择从你这里下载文件,所以当你使用 P2P 软件的时候,例如 BitTorrent,往往能够看到,既有下载流量,也有上传的流量,也即你自己也加入了这个 P2P 的网络,自己从别人那里下载,同时也提供给其他人下载。可以想象,这种方式,参与的人越多,下载速度越快,一切完美。

种子(.torrent)文件

怎么知道哪些 peer 有你要下载的文件?

这就用到种子啦,也即咱们比较熟悉的.torrent文件。.torrent 文件由两部分组成,分别是:announce(tracker URL)和文件信息

文件信息里面有这些内容:

  • info 区 :这里指定的是该种子有几个文件、文件有多长、目录结构,以及目录和文件的名字。
  • Name 字段 :指定顶层目录名字。
  • 每个段的大小 :BitTorrent(简称 BT)协议把一个文件分成很多个小段,然后分段下载。
  • 段哈希值 :将整个种子中,每个段的 SHA-1 哈希值拼在一起。

下载时,BT 客户端首先解析 .torrent 文件,得到 tracker 地址,然后连接 tracker 服务器。tracker 服务器回应下载者的请求,将其他下载者(包括发布者)的 IP 提供给下载者。下载者再连接其他下载者,根据.torrent 文件,两者分别对方告知自己已经有的块,然后交换对方没有的数据。此时不需要其他服务器参与,并分散了单个线路上的数据流量,因此减轻了服务器的负担。

下载者每得到一个块,需要算出下载块的 Hash 验证码,并与.torrent 文件中的对比。如果一样,则说明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容的准确性问题。

这种方式特别依赖 tracker。tracker 需要收集下载者信息的服务器,并将此信息提供给其他下载者,使下载者们相互连接起来,传输数据。虽然下载的过程是非中心化的,但是加入这个 P2P 网络的时候,都需要借助 tracker 中心服务器,这个服务器是用来登记有哪些用户在请求哪些资源。

一旦 tracker 服务器出现故障或者线路遭到屏蔽,BT 工具就无法正常工作了。

去中心化网络(DHT)

DHT(Distributed Hash Table)去中心化网络,每个加入这个 DHT 网络的人,都要负责存储这个网络里的资源信息和其他成员的联系信息,相当于所有人一起构成了一个庞大的分布式存储数据库

有一种著名的 DHT 协议,叫 Kademlia 协议。这个和区块链的概念一样。

任何一个 BitTorrent 启动之后,它都有两个角色。一个是 peer,监听一个 TCP 端口,用来上传和下载文件,这个角色表明,我这里有某个文件。另一个角色 DHT node,监听一个 UDP 的端口,通过这个角色,这个节点加入了一个 DHT 的网络。

在 DHT 网络里面,每一个 DHT node 都有一个 ID。这个 ID 是一个很长的串。每个 DHT node 都有责任掌握一些知识,也就是文件索引,也即它应该知道某些文件是保存在哪些节点上。
它只需要有这些知识就可以了,而它自己本身不一定就是保存这个文件的节点。

哈希值

每个 DHT node 不会有全局的知识,也即不知道所有的文件保存在哪里,它只需要知道一部分。那应该知道哪一部分呢?这就需要用哈希算法计算出来。

每个文件可以计算出一个哈希值,而DHT node 的 ID 是和哈希值相同长度的串

DHT 算法是这样规定的:如果一个文件计算出一个哈希值,则和这个哈希值一样的那个 DHT node,就有责任知道从哪里下载这个文件,即便它自己没保存这个文件

当然不一定这么巧,总能找到和哈希值一模一样的,有可能一模一样的 DHT node 也下线了,所以 DHT 算法还规定:除了一模一样的那个 DHT node 应该知道,ID 和这个哈希值非常接近的 N 个 DHT node 也应该知道

什么叫和哈希值接近呢?例如只修改了最后一位,就很接近;修改了倒数 2 位,也不远;修改了倒数 3 位,也可以接受。总之,凑齐了规定的 N 这个数就行。

刚才那个图里,文件 1 通过哈希运算,得到匹配 ID 的 DHT node 为 node C,当然还会有其他的,我这里没有画出来。所以,node C 有责任知道文件 1 的存放地址,虽然 node C 本身没有存放文件 1。

接下来一个新的节点 node new 上线了。如果想下载文件 1,它首先要加入 DHT 网络,如何加入呢?

在这种模式下,种子.torrent 文件里面就不再是 tracker 的地址了,而是一个 list 的 node 的地址,而所有这些 node 都是已经在 DHT 网络里面的。当然随着时间的推移,很可能有退出的,有下线的,但是我们假设,不会所有的都联系不上,总有一个能联系上。

node new 只要在种子里面找到一个 DHT node,就加入了网络。

node new 会计算文件 1 的哈希值,并根据这个哈希值了解到,和这个哈希值匹配,或者很接近的 node 上知道如何下载这个文件,例如计算出来的哈希值就是 node C。

但是 node new 不知道怎么联系上 node C,因为种子里面的 node 列表里面很可能没有 node C,但是它可以问,DHT 网络特别像一个社交网络,node new 只有去它能联系上的 node 问, 你们知道不知道 node C 的联系方式呀?

在 DHT 网络中,每个 node 都保存了一定的联系方式,但是肯定没有 node 的所有联系方式。DHT 网络中,节点之间通过互相通信,也会交流联系方式,也会删除联系方式。和人们的方式一样,你有你的朋友圈,你的朋友有它的朋友圈,你们互相加微信,就互相认识了,过一段时间不联系,就删除朋友关系。

所以,node new 想联系 node C,就去万能的朋友圈去问,并且求转发,朋友再问朋友,很快就能找到。如果找不到 C,也能找到和 C 的 ID 很像的节点,它们也知道如何下载文件 1。

在 node C 上,告诉 node new,下载文件 1,要去 B、D、F,于是 node new 选择和 node B 进行 peer 连接,开始下载,它一旦开始下载,自己本地也有文件 1 了,于是 node new 告诉 node C 以及和 node C 的 ID 很像的那些节点,我也有文件 1 了,可以加入那个文件拥有者列表了。

但是你会发现 node new 上没有文件索引,但是根据哈希算法,一定会有某些文件的哈希值是和 node new 的 ID 匹配上的。在 DHT 网络中,会有节点告诉它,你既然加入了咱们这个网络,你也有责任知道某些文件的下载地址。

DNS 协议:网络世界的地址簿

DNS 服务器

网络世界是很难记住网站的 IP 地址,于是,就需要一个地址簿,根据名称,就可以查看具体的地址,就是DNS 服务器

DNS 在日常生活中非常重要。每个人上网,都需要访问它。一旦它出了故障,整个互联网都将瘫痪。另外,上网的人分布在全世界各地,如果大家都去同一个地方访问某一台服务器,时延将会非常大。因而,DNS 服务器,
一定要设置成高可用、高并发和分布式的

  • 根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器:返回相应主机的 IP 地址

DNS 解析流程

为了提高 DNS 的解析性能,很多网络都会就近部署 DNS 缓存服务器。于是,就有了以下的 DNS 解析流程。

  1. 电脑客户端会发出一个 DNS 请求,问 www.163.com 的 IP 是啥啊,并发给本地域名服务器 (本地 DNS)。那本地域名服务器 (本地 DNS) 是什么呢?如果是通过 DHCP 配置,本地 DNS 由你的网络服务商(ISP),如电信、移动等自动分配,它通常就在你网络服务商的某个机房。
  2. 本地 DNS 收到来自客户端的请求。你可以想象这台服务器上缓存了一张域名与之对应 IP 地址的大表格。如果能找到 www.163.com,它直接就返回 IP 地址。如果没有,本地 DNS 会去问它的根域名服务器:“老大,能告诉我 < code>www.163.com 的 IP 地址吗?” 根域名服务器是最高层次的,全球共有 13 套。它不直接用于域名解析,但能指明一条道路。
  3. 根 DNS 收到来自本地 DNS 的请求,发现后缀是 .com,说:“哦,www.163.com 啊,这个域名是由 .com 区域管理,我给你它的顶级域名服务器的地址,你去问问它吧。
  4. 本地 DNS 转向问顶级域名服务器:“老二,你能告诉我 < code>www.163.com 的 IP 地址吗?” 顶级域名服务器就是大名鼎鼎的比如 .com.net.org 这些一级域名,它负责管理二级域名,比如 163.com,所以它能提供一条更清晰的方向。
  5. 顶级域名服务器说:“我给你负责 < code>www.163.com 区域的权威 DNS 服务器的地址,你去问它应该能问到。”
  6. 本地 DNS 转向问权威 DNS 服务器:“您好,www.163.com 对应的 IP 是啥呀?”163.com 的权威 DNS 服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。
  7. 权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  8. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。

负载均衡

站在客户端角度,这是一次DNS 递归查询过程。因为本地 DNS 全权为它效劳,它只要坐等结果即可。在这个过程中,DNS 除了可以通过名称映射为 IP 地址,它还可以做另外一件事,就是负载均衡

以访问 “外婆家” 为例,它可能有很多地址,因为它在杭州可以有很多家。所以,如果一个人想去吃 “外婆家”,他可以就近找一家店,而不用大家都去同一家,这就是负载均衡。

DNS 首先可以做内部负载均衡

例如,应用要访问数据库,配置这个数据库的域名,这样数据库如果 IP 地址修改,就不需要一个个修改应用的配置,直接修改 DNS 配置。
例如,当某个被访问的应用撑不住的时候,可以部署多个,只要配置域名,在域名解析的时候,我们只要配置策略,这次返回第一个 IP,下次返回第二个 IP,就可以实现负载均衡了。

DNS 还可以做全局负载均衡

例如,我们肯定希望北京的用户访问北京的数据中心,上海的用户访问上海的数据中心,这样,客户体验就会非常好,访问速度就会超快。这就是全局负载均衡的概念。

DNS 访问数据中心中对象存储上的静态资源

假设全国有多个数据中心,托管在多个运营商,每个数据中心三个可用区(Available Zone)。对象存储通过跨可用区部署,实现高可用性。在每个数据中心中,都至少部署两个内部负载均衡器,内
部负载均衡器后面对接多个对象存储的前置服务器(Proxy-server)。

  1. 当一个客户端要访问 object.yourcompany.com 的时候,需要将域名转换为 IP 地址进行访问,所以它要请求本地 DNS 解析器。
  2. 本地 DNS 解析器先查看看本地的缓存是否有这个记录。如果有则直接使用,因为上面的过程太复杂了,如果每次都要递归解析,就太麻烦了。
  3. 如果本地无缓存,则需要请求本地的 DNS 服务器。
  4. 本地的 DNS 服务器一般部署在你的数据中心或者你所在的运营商的网络中,本地 DNS 服务器也需要看本地是否有缓存,如果有则返回,因为它也不想把上面的递归过程再走一遍。
  5. 如果本地没有,本地 DNS 才需要递归地从根 DNS 服务器,查到 .com 的顶级域名服务器,最终查到 yourcompany.com 的权威 DNS 服务器,给本地 DNS 服务器,权威 DNS 服务器按说会返回真实要访问的 IP 地址。

对于不需要做全局负载均衡的简单应用来讲,yourcompany.com的权威 DNS 服务器可以直接将object.yourcompany.com这个域名解析为一个或者多个 IP 地址,然后客户端可以通过多个 IP 地址,进行简单的轮询,实现简单的负载均衡。

但是对于复杂的应用,尤其是跨地域跨运营商的大型应用,则需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是全局负载均衡器(GSLB,Global Server LoadBalance)

在 yourcompany.com 的 DNS 服务器中,一般是通过配置 CNAME 的方式,给object.yourcompany.com起一个别名,例如 object.vip.yourcomany.com,然后告诉本地 DNS 服务器,让它请求 GSLB 解析这个域名,GSLB 就可以在解析这个域名的过程中,通过自己的策略实现负载均衡。

图中画了两层的 GSLB,是因为分运营商和地域。我们希望不同运营商的客户,可以访问相同运营商机房中的资源,这样不跨运营商访问,有利于提高吞吐量,减少时延.

  1. 第一层 GSLB,通过查看请求它的本地 DNS 服务器所在的运营商,就知道用户所在的运营商。假设是移动,通过 CNAME 的方式,通过另一个别名 object.yd.yourcompany.com,告诉本地 DNS 服务器去请求第二层的 GSLB。
  2. 第二层 GSLB,通过查看请求它的本地 DNS 服务器所在的地址,就知道用户所在的地理位置,然后将距离用户位置比较近的 Region 里面,六个 内部负载均衡(SLB,Server Load Balancer) 的地址,返回给本地 DNS 服务器。
  3. 本地 DNS 服务器将结果返回给本地 DNS 解析器。
  4. 本地 DNS 解析器将结果缓存后,返回给客户端。
  5. 客户端开始访问属于相同运营商的距离较近的 Region 1 中的对象存储,当然客户端得到了六个 IP 地址,它可以通过负载均衡的方式,随机或者轮询选择一个可用区进行访问。对象存储一般会有三个备份,从而可以实现对存储读写的负载均衡。

HTTPDNS:网络世界的地址簿也会指错路

有时候这个地址簿也经常给你指错路,明明距离你 500 米就有个吃饭的地方,非要把你推荐到 5 公里外。为什么会出现这样的情况呢?

当我们发出请求解析 DNS 的时候,首先,会先连接到运营商本地的 DNS 服务器,由这个服务器帮我们去整棵 DNS 树上进行解析,然后将解析的结果返回给客户端。但是本地的 DNS 服务器,作为一个本地导游,往往有自己的 “小心思”。

传统 DNS 存在哪些问题?

域名缓存问题

它可以在本地做一个缓存,也就是说,不是每一个请求,它都会去访问权威 DNS 服务器,而是访问过一次就把结果缓存到自己本地,当其他人来问的时候,直接就返回这个缓存数据。

这就相当于导游去过一个饭店,自己脑子记住了地址,当有一个游客问的时候,他就凭记忆回答了,不用再去查地址簿。这样经常存在的一个问题是,人家那个饭店明明都已经搬了,结果作为导游,他并没有刷新这个缓存,结果你辛辛苦苦到了这个地点,发现饭店已经变成了服装店,你是不是会非常失望?

另外,有的运营商会把一些静态页面,缓存到本运营商的服务器内,这样用户请求的时候,就不用跨运营商进行访问,这样既加快了速度,也减少了运营商之间流量计算的成本。在域名解析的时候,不会将用户导向真正的网站,而是指向这个缓存的服务器。

很多情况下是看不出问题的,但是当页面更新,用户会访问到老的页面,问题就出来了。例如,你听说一个餐馆推出了一个新菜,你想去尝一下。结果导游告诉你,在这里吃也是一样的。有的游客会觉得没问题,但是对于想尝试新菜的人来说,如果导游说带你去,但其实并没有吃到新菜,你是不是也会非常失望呢?

再就是本地的缓存,往往使得全局负载均衡失败,因为上次进行缓存的时候,缓存中的地址不一定是这次访问离客户最近的地方,如果把这个地址返回给客户,那肯定就会绕远路。

就像客户要吃西湖醋鱼,导游知道西湖边有一家,因为当时游客就在西湖边,可是,下一次客户在灵隐寺,想吃西湖醋鱼的时候,导游还指向西湖边的那一家,那这就绕的太远了。

域名转发问题

这样的问题是,如果是 A 运营商的客户,访问自己运营商的 DNS 服务器,如果 A 运营商去权威 DNS 服务器查询的话,权威 DNS 服务器知道你是 A 运营商的,就返回给一个部署在 A 运营商的网站地址,这样针对相同运营商的访问,速度就会快很多。

但是 A 运营商偷懒,将解析的请求转发给 B 运营商,B 运营商去权威 DNS 服务器查询的话,权威服务器会误认为,你是 B 运营商的,那就返回给你一个在 B 运营商的网站地址吧,结果客户的每次访问都要跨运营商,速度就会很慢。

出口 NAT 问题

很多机房都会配置 NAT,也即网络地址转换,使得从这个网关出去的包,都换成新的 IP 地址,当然请求返回的时候,在这个网关,再将 IP 地址转换回去,所以对于访问来说是没有任何问题。

但是一旦做了网络地址的转换,权威的 DNS 服务器,就没办法通过这个地址,来判断客户到底是来自哪个运营商,而且极有可能因为转换过后的地址,误判运营商,导致跨运营商的访问。

域名更新问题

本地 DNS 服务器是由不同地区、不同运营商独立部署的。对域名解析缓存的处理上,实现策略也有区别,有的会偷懒,忽略域名解析结果的 TTL 时间限制,在权威 DNS 服务器解析变更的时候,解析结果在全网生效的周期非常漫长。但是有的时候,在 DNS 的切换中,场景对生效时间要求比较高。

例如双机房部署的时候,跨机房的负载均衡和容灾多使用 DNS 来做。当一个机房出问题之后,需要修改权威 DNS,将域名指向新的 IP 地址,但是如果更新太慢,那很多用户都会出现访问异常。

解析延迟问题

DNS 的查询过程需要递归遍历多个 DNS 服务器,才能获得最终的解析结果,这会带来一定的时延,甚至会解析超时。

HTTPDNS 的工作模式

HTTPNDS 其实就是,不走传统的 DNS 解析,而是自己搭建基于 HTTP 协议的 DNS 服务器集群,分布在多个地点和多个运营商。当客户端需要 DNS 解析的时候,直接通过 HTTP 协议进行请求这个服务器集群,得到就近的地址

这就相当于每家基于 HTTP 协议,自己实现自己的域名解析,自己做一个自己的地址簿,而不使用统一的地址簿。但是默认的域名解析都是走 DNS 的,因而使用 HTTPDNS 需要绕过默认的 DNS 路径,就不能使用默认的客户端。使用 HTTPDNS 的,往往是手机应用,需要在手机端嵌入支持 HTTPDNS 的客户端 SDK。

解析 HTTPDNS 的工作模式。

在客户端的 SDK 里动态请求服务端,获取 HTTPDNS 服务器的 IP 列表,缓存到本地。随着不断地解析域名,SDK 也会在本地缓存 DNS 域名解析的结果。

当手机应用要访问一个地址的时候,首先看是否有本地的缓存,如果有就直接返回。这个缓存和本地 DNS 的缓存不一样的是,这个是手机应用自己做的,而非整个运营商统一做的。如何更新、何时更新,手机应用的客户端可以和服务器协调来做这件事情。

如果本地没有,就需要请求 HTTPDNS 的服务器,在本地 HTTPDNS 服务器的 IP 列表中,选择一个发出 HTTP 的请求,会返回一个要访问的网站的 IP 列表。

当所有这些都不工作的时候,可以切换到传统的 LocalDNS 来解析。

HTTPDNS 的缓存设计

解析 DNS 过程复杂,通信次数多,对解析速度造成很大影响。为了加快解析,因而有了缓存,但是这又会产生缓存更新速度不及时的问题。最要命的是,这两个方面都掌握在别人手中,也即本地 DNS 服务器手中,它不会为你定制,你作为客户端干着急没办法。

而 HTTPDNS 就是将解析速度和更新速度全部掌控在自己手中。一方面,解析的过程,不需要本地 DNS 服务递归的调用一大圈,一个 HTTP 的请求直接搞定,要实时更新的时候,马上就能起作用;另一方面为了提高解析速度,本地也有缓存,缓存是在客户端 SDK 维护的,过期时间、更新时间,都可以自己控制。

HTTPDNS 的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端、缓存、数据源三层。

只要是缓存模式,就存在缓存的过期、更新、不一致的问题,解决思路也是很像的。

例如 DNS 缓存在内存中,也可以持久化到存储上,从而 APP 重启之后,能够尽快从存储中加载上次累积的经常访问的网站的解析结果,就不需要每次都全部解析一遍,再变成缓存。这有点像 Redis 是基于内存的缓存,但是同样提供持久化的能力,使得重启或者主备切换的时候,数据不会完全丢失。

SDK 中的缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析,保障记录是更新的。

解析可以同步进行,也就是直接调用 HTTPDNS 的接口,返回最新的记录,更新缓存;也可以异步进行,添加一个解析任务到后台,由后台任务调用 HTTPDNS 的接口。

HTTPDNS 的调度设计

由于客户端嵌入了 SDK,因而就不会因为本地 DNS 的各种缓存、转发、NAT,让权威 DNS 服务器误会客户端所在的位置和运营商,而可以拿到第一手资料。

在客户端,可以知道手机是哪个国家、哪个运营商、哪个省,甚至哪个市,HTTPDNS 服务端可以根据这些信息,选择最佳的服务节点返回。

在服务端,应用可以通过调用 HTTPDNS 的管理接口,配置不同服务质量的优先级、权重。HTTPDNS 会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的 IP 地址。

CDN:你去小卖部取过快递么

你去电商网站下单买个东西,这个东西一定要从电商总部的中心仓库送过来吗?原来基本是这样的,每一单都是单独配送,所以你可能要很久才能收到你的宝贝。但是后来电商网站的物流系统学聪明了,他们在全国各地建立了很多仓库,而不是只有总部的中心仓库才可以发货。

电商网站根据统计大概知道,北京、上海、广州、深圳、杭州等地,每天能够卖出去多少书籍、卫生纸、包、电器等存放期比较长的物品。这些物品用不着从中心仓库发出,所以平时就可以将它们分布在各地仓库里,客户一下单,就近的仓库发出,第二天就可以收到了。

全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本上都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据的时候,就可以就近访问了呢?

当然是可以的。这些分布在各个地方的各个数据中心的节点,就称为边缘节点

由于边缘节点数目比较多,但是每个集群规模比较小,不可能缓存下来所有东西,因而可能无法命中,这样就会在边缘节点之上。有区域节点,规模就要更大,缓存的数据会更多,命中的概率也就更大。在区域节点之上是中心节点,规模更大,缓存数据更多。如果还不命中,就只好回源网站访问了。

这就是CDN 的分发系统的架构。CDN 系统的缓存,也是一层一层的,能不访问后端真正的源,就不打扰它。这也是电商网站物流系统的思路,北京局找不到,找华北局,华北局找不到,再找北方局。

客户端如何找到相应的边缘节点进行访问

如何访问边缘节点?

CDN 分发网络也是一个分布在多个区域、多个运营商的分布式系统,用基于 DNS 的全局负载均衡的思路。

图中实线是在有 CDN 的情况下,在web.com这个权威 DNS 服务器上,会设置一个 CNAME 别名,指向另外一个域名www.web.cdn.com,返回给本地 DNS 服务器。

当本地 DNS 服务器拿到这个新的域名时,需要继续解析这个新的域名。这个时候,再访问的就不是web.com的权威 DNS 服务器了,而是 web.cdn.com 的权威 DNS 服务器,这是 CDN 自己的权威 DNS 服务器。
在这个服务器上,还是会设置一个 CNAME,指向另外一个域名,也即 CDN 网络的全局负载均衡器。

接下来,本地 DNS 服务器去请求 CDN 的全局负载均衡器解析域名,全局负载均衡器会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:

  • 根据用户 IP 地址,判断哪一台服务器距用户最近
  • 用户所处的运营商
  • 根据用户所请求的 URL 中携带的内容名称,判断哪一台服务器上有用户所需的内容
  • 查询各个服务器当前的负载情况,判断哪一台服务器尚有服务能力

进行综合分析之后,全局负载均衡器会返回一台缓存服务器的 IP 地址。

缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

CDN 可以进行缓存的内容有很多种

保质期长的日用品比较容易缓存,因为不容易过期,对应到就像电商仓库系统里,就是静态页面、图片等,因为这些东西也不怎么变,所以适合缓存。

但是静态内容中,有一种特殊的内容,也大量使用了 CDN,这个就是前面讲过的流媒体。

CDN 支持流媒体协议,例如前面讲过的 RTMP 协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。

对于静态页面来讲,内容的分发往往采取拉取的方式,也即当发现未命中的时候,再去上一级进行拉取。但是,流媒体数据量大,如果出现回源,压力会比较大,所以往往采取主动推送的模式,将热点数据主动推送到边缘节点。

对于流媒体来讲,很多 CDN 还提供预处理服务,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同的网络带宽的用户需求;再如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的,“我要看超清、标清、流畅等”。

防盗链问题

视频是要花大价钱买版权的,为了挣点钱,收点广告费,如果流媒体被其他的网站盗走,在人家的网站播放,那损失可就大了。

最常用也最简单的方法就是HTTP 头的 refer 字段,当浏览器发送请求的时候,一般会带上 referer,告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果 refer 信息不是来自本站,就阻止访问或者跳到其它链接。

refer 的机制相对比较容易破解,所以还需要配合其他的机制

一种常用的机制是时间戳防盗链。使用 CDN 的管理员可以在配置界面上,和 CDN 厂商约定一个加密字符串。

客户端取出当前的时间戳,要访问的资源及其路径,连同加密字符串进行签名算法得到一个字符串,然后生成一个下载链接,带上这个签名字符串和截止时间戳去访问 CDN。

在 CDN 服务端,根据取出过期时间,和当前 CDN 节点时间进行比较,确认请求是否过期。然后 CDN 服务端有了资源及路径,时间戳,以及约定的加密字符串,根据相同的签名算法计算签名,如果匹配则一致,访问合法,才会将资源返回给客户。

动态 CDN

有两种模式:

  • 一种为 生鲜超市模式 ,也即 边缘计算的模式 。既然数据是动态生成的,所以数据的逻辑计算和存储,也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据,然后在边缘进行计算得到结果。就像对生鲜的烹饪是动态的,没办法事先做好缓存,因而将生鲜超市放在你家旁边,既能够送货上门,也能够现场烹饪,也是边缘计算的一种体现。
  • 另一种是 冷链运输模式 ,也即 路径优化的模式 。数据不是在边缘计算生成的,而是在源站生成的,但是数据的下发则可以通过 CDN 的网络,对路径进行优化。因为 CDN 节点较多,能够找到离源站很近的边缘节点,也能找到离用户很近的边缘节点。中间的链路完全由 CDN 来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。

趣谈网络协议第二部分 在这里

原文地址:https://blog.shipengqi.top/2018/11/23/network-protocol/