基础知识:计算机网络--《趣谈网络协议》读书笔记

1. 什么是协议?

协议三要素:

  • 语法,就是一段内容要符合一定的规则和格式
  • 语义,就是这一段内容要代表某种意义
  • 顺序,先干啥,后干啥

2. ifconfig

2.1 面试考点

2.1.1 怎么参看 IP 地址?

Windows 上使用 ipconfig 命令,Linux 上使用 ifconfig 命令,Linux 上还可以使用 ip addr 命令

2.1.2 ifconfig 与 ip addr 有什么区别?

2.2 IP 地址分类

2.3 无类型域间选路(CIDR)

这种方式打破了原来设计的几类地址的做法,将 32 位的 IP 地址一分为二,前面是 网络号,后面是 主机号

将子网掩码和 IP 地址按位计算 AND,就可以得到网络号

2.4 MAC 地址

MAC 地址,是一个网卡的物理地址,用十六进制,6 byte 表示。

MAC 地址号称全局唯一,不会有两个网卡有相同的 MAC 地址。MAC 地址像是身份证号,是一个唯一的标识,而 IP 地址更像是现住址的门牌号,具有定位功能

3. DHCP

3.1 如何配置 IP 地址?

可以使用命令行自己配置一个地址,可以使用 ifconfig ,也可以使用 ip addr,设置好之后,用这两个命令,将网卡 up 一下,就可以开始工作了,这样配置自由度太高,可能自己会配置一个不能使用的 IP。

真正现实中可能是去管理员那里申请一段正确的 IP 地址,然后放在一个配置文件里面。不同系统的配置文件格式不同,大概包括 CIDR、子网掩码、广播地址和网关地址。

3.2 动态主机配置协议(DHCP)

将 IP 写入配置文件配置服务端机器还可以,不需要变化,但如果是客户端呢,每天人来来往往、走来走去,如果在使用写入配置文件的方式分配 IP 地址,效率太低

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

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

4. 如何组建一个网络

4.1 第一层:物理层

集线器有多个口,可以将多台电脑连接起来,但是,和交换机不同,集线器没有大脑,它完全在物理层工作,它会将自己收到的每一个字节,都复制到其他端口上去。这是第一层物理层联通的方案。

4.2 第二层:数据链路层

因为 Hub 采用的是广播的模式,如果每一台电脑发出的包,其他电脑都能收到,那就麻烦了,会出现以下问题:

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

这几个问题,就是数据链路层要解决的问题,就是 Medium Access Control,即 媒体访问控制,其实就是控制在往媒体上发数据的时候,谁先发、谁后发的问题,防止发生混乱。这解决的是第二个问题。这个问题中的规则,学名叫做 多路访问

第二个问题解决了,再看第一个问题,解决第一个问题就牵扯到第二层的 网络包格式 。对于以太网,第二层的最开始,就是目标的 MAC 地址和源的 MAC 地址。接下来就是 类型,大部分的类型是 IP 数据包,然后里面包含 TCP、UDP等层层封装的数据。而有了最开始的 MAC 地址,第一个问题也就解决了。

对于以太网,第二层数据包的最后面是 CRC,也就是 循环冗余检测。通过 XOR 异或的算法,来计算整个包在发送的过程中是否出现了错误,主要解决了第三个问题。

ARP 协议

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

在一个局域网里面,如果知道了 IP 地址,可以通过广播的模式来向局域网内所有机器发出信号 “哪位电脑的 IP 地址是 XXX.XXX.XXX.XXX,你的 MAC 地址是啥?” ,广而告之,发送一个广播包,谁是这个 IP 谁来回答。

机器本地会进行 ARP 缓存,机器也会不断上线下线,所以 IP 地址是可能会变的,所以 ARP 的 MAC 地址缓存有过期时间。

交换机

使用以上方法,用 Hub 连接起来,便可以组成一个局域网了,但是一旦机器数量增多,问题就出现了,因为 Hub 是广播的,不管某个接口是否需要,所有的 Bit 都会被发送出去,然后主机来判断是不是需要,一旦流量变大,产生冲突的概率就提高了,所以需要智能点儿的设备。因为每个口都只连接一台电脑,这台电脑又不怎么切换 IP 和 MAC 地址,只要记住这台电脑的 MAC 地址,如果目标 MAC 地址不是这台电脑的,这个口就不用转发了。

交换机就具有这种功能,交换机可以将数据包中的 MAC 头拿下来,检查一下目标 MAC 地址,然后根据策略进行转发。

那交换机是如何知道每个口的电脑的 MAC 地址呢?这需要交换机会学习

电脑 A 要将一个包发送给电脑 B,当这个包到达交换机的时候,一开始交换机也不知道电脑 B 在哪个接口,只能将包转发给除自己外所有的接口,这个时候,交换机会记住,电脑 A 是来自于一个明确的接口的。以后当有包的目的地址是电脑 A 的,直接发送到这个口就可以了。当然,每个机器的 IP 地址会变,所在的接口也可能会变,因而交换机上的学习成果,我们称为 转发表,是有一个过期时间的。

5. 交换机与 VLAN

5.1 环路问题

当多台交换机连接在一起,就形成了一个拓扑结构,随着拓扑结构越来越复杂,这么多网线,绕过来绕过去,不可避免的会出现一些意料不到的情况。其中最常见的问题就是环路问题

在这里插入图片描述

数据包会在里面转来转去,每台机器都会发广播包,交换机转发也会复制广播包,当广播包越来越多的时候,网络就会越来越堵塞,这就需要使用 STP 协议,通过生成最小生成树的算法,将有环路的图变成没有环路的树,从而解决环路问题。

5.2 如何解决广播问题和安全问题

当局域网内机器越来越多,交换机也随之增多,就算交换机比 Hub 智能一些,但是难免有广播的问题,广播消息一大推,性能就下来了。

一个公司有不同的部门,有的部门需要保密,由于都在同一个广播域内,很多包都会在一个局域网里面飘啊飘,碰到会抓包的有心之人,就能抓到这些包,如果没有加密,就能看到这些敏感信息了。

基于以上两种情况,需要考虑划分局域网。

有两种划分方法,一种是 物理隔离,每个部分有单独的交换机,配置单独的子网,这样部门与部门之间的沟通就需要路由器了。但是这样划分无法充分满足部门人数变化的情况,一个部分人可能越来越多,也可能越来越少,交换机口多了浪费,少了又不够用。

另一种方式是 虚拟隔离,就是 VLAN,或者叫 虚拟局域网,我们需要在原来第二层的头上加一个 TAG,里面有一个 VLAN ID,一共 12 位。如果我们买的交换机是支持 VLAN 的,当这个交换机把第二层的头取下来的时候,就能够识别这个 VLAN ID。这样只有相同 VLAN 的包,才会互相转发,不同 VLAN 的包,是看不到的。这样广播问题和安全问题就都能够解决了。

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

6. ICMP 与 ping

6.1 ICMP 协议的格式

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

网络包在异常复杂的网络环境中传输时,常常会遇到各种各样的问题。当遇到问题时,要传出消息来,报告情况,这样才可以调整传输策略。

ICMP 报文是封装在 IP 包里面的,因为传输指令的时候,需要源地址和目标地址

6.2 查询报文类型

主动去查询网络情况怎么样,对应 ICMP 的查询报文类型。ping 就是查询报文,是一种主动请求,并且获得主动应答的 ICMP 协议。所以 ping 发的包也是符合 ICMP 协议格式的,只不过它在后面增加了自己的格式。

对 ping 的主动请求,进行网络抓包,称为 ICMP ECHO REQUEST,同理主动请求的回复,称为 ICMP ECHO REPLY。比起原生的 ICMP ,多了两个字段,一个是 标识符,另一个是 序号,在选项数据中,ping 还会存放发送请求的时间值,来计算往返时间,说明路程长短。

6.3 差错报文类型

当我们正常发数据包时,目标地址出现问题,回复给我们的报文,对应的就是 ICMP 的差错报文类型。

7. 网关

局域网怎么与外网联系 – 路由器

使用路由器,然后配置我们的网卡(DHCP 是可以默认配置的),除了 IP 地址,我们还需要配置 网关

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

如果是同一个网段 ,例如,访问局域网内旁边的电脑,那就没有网关什么事,直接将源地址和目标地址放入 IP 头,然后通过 ARP 获得 MAC 地址,将源 MAC 和 目的 MAC 放入 MAC 头中,发出去就可以了。

如果不是同一个网段 ,例如,要发往局域网之外的网络,这就需要发往默认网关 Gateway。Gateway 的地址一定是和源 IP 地址是一个网段的。往往不是第一个,就是第二个。例如 192.168.1.0/24 这个网段,Gateway 往往会是 192.168.1.1/24 或者 192.168.1.2/24。

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

7.1 静态路由

  • 静态路由,其实就是在路由器上,配置一条一条规则。

8. 路由

8.1 如何配置路由?

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

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

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

可以通过命令来进行配置,核心思想是:根据目的 IP 地址来配置路由。

也可以根据多个参数来配置路由,这称作为 策略路由 ,这些都是静态路由。

8.2 动态路由算法

如果网络环境很复杂并且多变,静态路由配置起来就很麻烦,因此需要动态路由算法。

动态路由算法的本质意义就是 如何在图中找到最短路径的问题,常用的有两种,一种是 Bellman-Ford 算法,另一种就是 Dijkstra 算法。

在计算机网络中,距离矢量路由算法 便是基于 Bellman-Ford 算法的;链路状态路由算法 是基于 Dijkstra 算法的

9. UDP

9.1 TCP 和 UDP 有哪些区别?

  1. TCP 是 面向连接的 ,UDP 是 面向无连接的

    在互通之前,面向连接的协议会先建立连接,例如,TCP 会三次握手,而 UDP 不会,所谓建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性

  2. TCP 提供 可靠交付,通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达;而 UDP 继承了 IP 包的特性,不保证不丢失,不保证顺序到达

  3. TCP 是 面向字节流的 ,发送的时候发的是一个流,没头没尾;UDP 是 基于数据报的,一个一个地发,一个一个地收。

  4. TCP 是可以拥有 拥塞控制 的,会根据网络情况调整自己的行为;UDP 则不会,上边的应用让它发,它就发,不管网络情况怎样。

因此,TCP 其实是一个有状态的服务,它可以记着发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而 UDP 则是无状态服务,数据报发出去就发出去了,不够结果如何。

9.2 UDP 的三大使用场景

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

10. TCP

10.1 TCP 包头格式

在这里插入图片描述

  1. 源端口号与目的端口号:为了确定数据应该发给哪个应用。
  2. 序号:为了解决 乱序 问题,确认哪个包先来,哪个包后来。
  3. 确认序号:解决 不丢包 的问题,发出的包应该有确认,如果没有收到就应该重新发送,直到送达。
  4. 状态位:TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
    1. SYN:发起一个连接
    2. ACK:回复
    3. RST:重新连接
    4. FIN:结束连接
  5. 窗口大小:TCP 要做 流量控制 ,通信双方各声明一个窗口,标识自己当前的处理能力。

流量控制是照顾通信对象

拥塞控制是照顾通信环境

10.2 TCP 三次握手

10.2.1 为什么两次握手不行?

假设网络通路是不可靠的,A 想要与 B 建立连接。当 A 发出一个请求的时候,这个请求杳无音信,这时候有很多种可能,可能是这个包丢了,也可能没有丢,只是绕了弯路,也可能是 B 不想要与 A 建立连接。

A 并不能确认是什么情况,它只有重复发送请求包,这时候 B 这边收到了 A 的请求包,它可能不愿意与 A 建立连接,则 A 会重试一段时间然后放弃,它也可能愿意与 A 建立连接,那 B 就会向 A 发送一个确认包,如果只有两次握手的话,这时候已经结束了, A 应该与 B 成功建立连接了,但是,B 发送的这个确认包也可能会丢,或者这时候恰好 A 掉线了,B 也不能知道这个包的状态,只能保持着连接的状态。这是两次握手不能确认建立连接的原因之一。

或者再看另外一种极端情况,A 与 B 原来建立了连接,并且进行了简单的通信,结束了连接,但是 A 之前发出请求建立连接的时候,由于网络状态不好,重复发了好几次请求,这时候其中一个请求包珊珊来迟,B 自然会认为这是一个正常的请求,于是建立了连接,这样的话,连接就会一直保持着。因此两次握手肯定不行。

10.2.2 为什么不需要四次握手?或更多次握手?

由于网络状态不好,B 也可能会发送多次应答,但只要有一个应答包到达 A,A 就认为连接已经建立了,因为对于 A 来说,它的消息是有来有回的。然后 A 会对 B 的应答包发送应答包,表示 A 已经收到了 B 的应答,而 B 也在等这个消息,只有等到了 A 的应答包,才能确认连接的建立,这样对于 B 来讲,它的消息才算有来有回的。

当然 A 发给 B 的应答包也可能会丢,也可能绕路,甚至 B 挂了,按这样来说,B 要是收到 A 的应答包之后,还应再发一个应答包再发给 A,但是这样下去没底了,所以四次握手是可以的,四十次握手也是可以的,关键是再多次的握手也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了

10.2.3 序号问题

三次握手除了双方建立连接外,主要还是为了沟通 TCP 包的序号问题

A 要告诉 B ,它这边发起的包的序号是从哪个号开始的,B 也同样要告诉 A,它这边发起的包的序号是从哪个号开始的。

为什么不都从 1 开始呢,因为这样往往会出现冲突

例如,A 和 B 建立了连接,A 向 B 发送了 1、2、3 三个包,但是发送 3 这个包的时候,中间丢了,或者绕路了,然后重发 3,这时候 A 掉线了,重连上 B 之后,打算向 B 发送 1、2 这两个包,但是上次绕路的那个 3 又回来了,发给了 B ,B 自然认为这就是下一个包,于是发生了错误。

因而,每个连接都要有不同的序号,这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一。计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就失效了。

10.2.4 三次握手过程

在这里插入图片描述
一开始,客户端和服务端都处于 CLOSED 状态,服务端主动监听某一端口号,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SEND 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK ,之后处于 ESTABLISHED 状态,因为它一收一发成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一收一发成功了。

10.3 四次挥手

在这里插入图片描述
断开的时候,A 发送 FIN ,进入FIN_WAIT_1 状态,B 收到 A 的消息后,发送 FIN 的 ACK,进入 CLOSED_WAIT 状态。A 收到 B 的 ACK 进入 FIN_WAIT_2 状态,当 B 处理完所有数据之后,发送 FIN + ACK ,进入 LAST_ACK 状态。A 在收到 B 的 FIN + ACK 之后,向 B 发送 ACK 包确认收到信息,此时,A 进入 TIME_WAIT 状态,等待 2MSL 时间后,关闭连接,B 在收到 A 的 ACK 之后关闭连接。

10.3.1 为什么要等待 2MSL 的时间?

当 A 收到 B 发送的 FIN + ACK 之后,知道 B 处理完数据了,要断开连接了,所以发送了一个 ACK 给 B,按理说,A 这时候已经可以断开连接了,可是如果这个 ACK 包丢了怎么办,如果 A 已经断开连接了,B 就再也没办法收到 ACK 了,B 也就没办法知道 A 到底有没有收到自己要关闭连接的请求包。所以,TCP 协议要求 A 最后等待一段时间,这个时间要足够长,长到如果 B 没有收到 A 的 ACK 包,B 会重发 FIN + ACK ,然后 A 会重新发送一个 ACK 并且给与足够时间到达 B。

等待的时间是 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间的报文将被丢弃。

因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理它的路由器,值就减一,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒、1 分钟和两分钟等。

10.4 TCP 状态机

  • 将连接建立和连接断开的两个时序图综合起来,就是著名的 TCP 状态机
    在这里插入图片描述

10.5 TCP 是如何保证靠谱的

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

为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

  • 第一部分:发送了并且已经确认的
  • 第二部分:发送了并且尚未确认的
  • 第三部分:没有发送,但是已经等待发送的
  • 第四部分:没有发送,并且暂时还不会发送的

在 TCP 里,接收端会给发送端报一个窗口的大小,叫做 滑动窗口 。这个窗口的大小应该等于第二部分加上第三部分。超过这个窗口的,接收端就处理不过来,就不能发送了。

接收端的缓存里记录的内容要简单一些

  • 第一部分:接收并且确认过的
  • 第二部分:还没接收,但是马上就能接收的
  • 第三部分:还没接收,也没法接收的

10.6 确认与重发机制

  • 对于顺序问题与丢包问题,TCP 有确认与重发的机制,一种方法是 超时重试,另一种方法是 SACK

10.6.1 超时重试

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

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

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

超时重传存在的问题是,超时周期可能相对较长

有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK ,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。

例如,接收方发送 6 送到了,8 也送到了,但是 7 还没来,那肯定就是丢了,于是就发送 6 的 ACK ,要求下一个是 7 。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。

10.6.2 SACK

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

10.7 流量控制问题

在接收端发送对于包的确认中,同时会携带一个窗口的大小。

发送端会根据接收端发送的 ACK 包中携带的窗口大小来调整自己的滑动窗口的大小,以此来实现流量控制。

当滑动窗口为 0 了之后,发送端不会再给接收端发送数据包,而此时接收端也没有可以让自己 ACK 的数据包了,这样两边就不清楚对方的情况了。发送端不清楚接收端那边的数据处理完了没有,接收端也没有办法告诉发送端我处理到哪里了(因为接收端没有可以 ACK 的包了)。这样就陷入僵局了,为了打破这个僵局,当滑动窗口为 0 了之后,发送方会定时发送窗口探测包,询问接收端是否有机会调整窗口的大小。(接收方也得矜持一点,不能有了一个空位,就马上告诉发送方你可以传数据包了,当发送端传了之后,发送端又没窗口了,发送端又要发送窗口探测数据包,这样窗口一会儿没,一会儿有,即使有,窗口的大小也是1、2啥的。这种探测窗口的数据包发的就挺多的,这些包丢没丢还得需要机制去保证,效率就下来了)。一般都是当接收端窗口大小到达一定的大小,或者窗口大小是缓冲区的一半了,才给发送端一个信息,可以更新窗口了,更新窗口的大小为 XXX。

10.8 拥塞控制问题

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

这里有一个公式 发送的未确认的包的数量 <= min(cwnd, rwnd),是拥塞窗口和滑动窗口共同控制发送的速度。

TCP 的拥塞控制目的是在不堵塞、不丢包的情况下,尽量发挥带宽。通道容量 = 带宽 * 往返延迟。如果我们设置发送窗口,使得发送但未确认的包为通道的容量,就能够撑满整个管道。

TCP 的拥塞控制主要来避免两种现象,包丢失超时重传。一旦出现这些现象,就说明发送速度太快了,要慢一点。

但是一开始如何知道速度多快呢?怎么知道应该把窗口调整到多大呢?TCP 对应的策略就是 慢启动

10.8.1 慢启动

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

直到 cwnd 增长到超过 ssthresh(65535个字节),就要慢下来了,改变成每收到一个确认后,cwnd 增加 1/cwnd ,也就是每次增加一个,这样就变成了 线性增长

但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞。拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh 设为 cwnd/2 ,将 cwnd 设为1,重新开始慢启动。但是这种方式太激进了,将一个高速的传输速度一下子停下来,会造成网络卡顿。

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

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

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

总结:

  • 顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的
  • 拥塞控制是通过拥塞窗口来解决的

11. Socket

11.1 基于 TCP 的Socket 程序函数调用过程

  1. TCP 的服务端要先监听一个端口,先调用 bind 函数,给这个 Socket 赋予一个 IP 地址和端口
    • 为什么需要端口?因为当一个网络包来的时候,内核要通过 TCP 头里面的这个端口,来找到这个应用程序,把包给它
    • 为什么需要 IP 地址?可能一台机器会有多个网卡,也就会有多个 IP 地址,可以选择监听所有网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。
  2. 当服务端有了 IP 地址,就可以调用 listen 函数进行监听,这时客户端就可以发起连接了
  3. 客户端通过connect 函数发起连接。先在参数中指明要连接的 IP 地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的 accept 就会返回另一个 Socket。
  4. 连接建立成功之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

注意:监听的 Socket 和真正用来传数据的 Socket 是两个,一个叫作 监听 Socket,一个叫作 已连接 Socket
在这里插入图片描述

11.2 基于 UDP 的 Socket 函数调用过程

对于 UDP 来讲,它是没有连接的,所以不需要三次握手,也就不需要调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 和端口号,因而也需要 bind。UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket ,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom ,都可以传入 IP 地址和端口。

11.3 服务器如何接更多的项目?

11.3.1 多进程方式

相当于自己是一个代理,在那里监听来的请求。一旦建立了一个连接,就会有一个已连接 Socket ,这时候就可以创建一个子进程,然后将基于已连接 Socket 的交互交给这个新的子进程来做。

Linux 中可以使用 fork 函数来创建子进程,是在父进程的基础上完全拷贝一个子进程。在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程。这两个进程刚复制完的时候,几乎一模一样,如果 fork 的返回值是 0 ,则是子进程;如果返回值是其他的整数(子进程的 ID),就是父进程。

接下来,子进程就可以通过这个已连接 Socket 和和客户端进行互通了,当通信完毕,就可以退出进程。父进程可以通过 fork 函数返回的子进程的 ID 来查看子进程是否完成项目,是否需要退出。
在这里插入图片描述

11.3.2 多线程方式

相比于进程来讲,线程要轻量的多。

Linux 中可以通过 pthread_create 创建一个线程,也是调用 do_fork。不同的是,虽然新的线程在 task 列表会新创建一项,但是很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已。

新的线程也可以通过已连接 Socket 处理请求,从而达到并发处理的目的。
在这里插入图片描述

以上基于进程或线程模型的,会有一个 C10K问题 ,新到来一个 TCP 连接,就需要分配一个进程或线程,一台机器无法创建很多进程或线程。C10K 问题就是,一台机器要维护一万个连接,就要创建一万个进程或者线程,操作系统是无法承受的。

10.3.3 IO多路复用,一个线程维护多个 Socket

由于 Socket 是文件描述符,因而某个线程盯的所有的 Socket ,都放在一个文件描述符集合 fd_set 中,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 对应的位都设为 1,表示 Socket 可读或者可写,从而可以进行读写操作,然后再调用 select ,接着盯着下一轮的变化。

10.3.4 epoll(从 “派人盯着” 到 “有事通知” )

select 函数使用轮询的方式来监听所有 Socket,并且使用 select ,能够同时监听的 Socket 数量由 FD_SETSIZE 限制。

epoll 是通过注册 callback 函数的方式,当某个文件描述符发生变化的时候,就会主动通知。

10.3.4.1 select/poll

select 实现多路复用的方式是,将已连接的 Socket 都放到一个 文件描述符集合 ,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式就是通过 遍历 文件描述符集合,当检查到有事件产生后,将此 Socket 标记为可读或可写,接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过 遍历 的方式找到可读或可写的 Socket ,然后再对其进行处理。

select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制,默认最大值为 1024。

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用 线性结构 存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket ,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,性能的损耗会呈指数级增长。

10.3.4.2 epoll

epoll 通过两个方面,很好解决了 select / poll 的问题。

第一点,epoll 在内核里使用 红黑树来跟踪进程所有待检测的文字描述符,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket ,减少了内核和用户空间大量的数据拷贝和内存分配。

第二点,epoll 使用事件驱动的机制,内核里 维护了一个链表来记录就绪事件 ,当某个 socket 有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
在这里插入图片描述
epoll 支持两种事件触发模式,分别是 边缘触发(edge-triggered ET)水平触发(level-triggered LT)

  • 使用边缘触发模式时,当描述符从未就绪变为就绪时,内核通过 epoll 告诉我们,然后它会假设我们已经知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,因此,我们程序要保证一次性将内核缓冲区的数据读取完
  • 使用水平触发模式时,当被监控的 socket 上有可读事件发生时,服务器会不断地对我们进行通知,直到内核缓冲区数据被 read 函数读完才结束

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少系统调用的次数,边缘触发模式一般和非阻塞 IO 搭配使用

epoll 是解决 C10K 问题的利器

来源于:小林 coding《图解系统》

12. HTTP

12.1 HTTP 请求的格式

在这里插入图片描述

12.1.1 第一部分:请求行

方法类型:

  • GET:获取服务器资源
  • POST:主动去告诉服务端一些信息,而非获取
  • PUT:向指定资源位置上传最新内容。(但是 HTTP 的服务器往往是不允许上传文件的,所以 PUT 和 POST 就都变成了要传给服务器东西的方法)
  • DELETE:删除资源

在使用过程中,POST 往往是用来创建一个资源的,而 PUT 往往是用来修改一个资源的

12.1.2 第二部分:首部字段

是以 key-value 的形式保存的

例如,Accept-Charset,表示客户端可以接受的字符集,防止传过来的是另外的字符集,从而导致出现乱码。

Content-Type 是指正文的格式,比如 JSON、image、text。

Cache-control,是用来控制缓存的,当客户端发送的请求中包含 max-age 指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定 max-age 值为 0,那么缓存层通常需要将请求转发给应用集群。

If-Modified-Since 也是关于缓存的,如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回 “304 Not Modified” 的响应,那客户端就不用下载了,也会节省带宽。

12.2 HTTP 返回的构建

在这里插入图片描述

  • 状态码会反映 HTTP 请求的结果,200、404 等,短语会大概说一下原因

首部字段也是以 key-value 的形式保存的

Retry-After 是告诉客户端应该在多长时间以后再次尝试一下。主要用于配合状态码 503 service Unavailable 响应一起发送的时候,表示服务预计不可用的时间,当与重定向响应一起发送的时候,比如 301(Moved Permanently,永久迁移),表示用户代理在发送重定向请求之前需要等待的最短时间。

Content-Type,表示返回的类型,HTML、JSON

12.3 HTTP 请求的流程

  1. 浏览器将域名发送给 DNS 服务器,让它解析为 IP 地址
  2. HTTP 是基于 TCP 协议的,进行三次握手建立连接(目前使用的 HTTP 协议大部分都是 1.1,1.1 协议中默认是开启了 Keep-Alive 的,这样建立的 TCP 连接,就可以在多次请求中复用)
  3. 通过 stream 二进制流的方式传给对方,当然,到了 TCP 层,它会把二进制流变成一个个报文段发送给服务器
  4. 在发送报文段的时候,都需要对方有一个回应,来保证报文可靠地到达了对方。如果没有回应,那么 TCP 这一层会进行重新传输,直到可以到达
  5. TCP 层发送每一个报文的时候,都需要加上自己的地址(源地址)和它的目标地址,将这两个信息放到 IP 头里面,交给 IP 层进行处理
  6. IP 层需要查看目标地址和自己是否是在同一个局域网。如果是,就发送 ARP 协议来请求这个目标地址对应的 MAC 地址,然后将源 MAC 和目标 MAC 放入 MAC 头,发送出去即可;如果不在同一个局域网,就需要发送到网关,还得需要发送 ARP 协议,来获取网关的 MAC 地址,然后将源 MAC 和网关 MAC 放入 MAC 头,发送出去。
  7. 网关收到包发现 MAC 符合,取出目标 IP 地址,根据路由协议找到下一跳的路由器,获取下一跳路由器的 MAC 地址,将包发给下一跳路由器。
  8. 这样路由器一跳一跳终于到达目标的局域网。这个时候,最后一跳的路由器能够发现,目标地址就在自己的某一个出口的局域网上。于是在这个局域网上发送 ARP ,获得这个目标地址的 MAC 地址,将包发出去。
  9. 目标机器发现 MAC 地址符合,就将包收起来;发现 IP 地址符合,根据 IP 头中协议项,知道自己上一层是 TCP 协议,于是解析 TCP 的头,里面有序列号,需要看一看这个序列号是不是我要的,如果是就放入缓存中然后返回一个 ACK,如果不是就丢弃。
  10. TCP 头里面还有端口号,HTTP 的服务器正在监听这个端口号。于是,目标机器自然知道是 HTTP 服务器这个进程想要这个包,于是将包发给 HTTP 服务器。HTTP 服务器的进程看到,开始响应客户端的请求,构造响应报文。
  11. 构造好了返回的 HTTP 报文,还是交给 Socket 去发送,还是交给 TCP 层,让 TCP 将返回的 HTML 也分成一个个小的段,并且保证每个段都可靠到达
  12. 这些段加上 TCP 头后会交给 IP 层,然后把刚才的发送过程反向走一遍。虽然两次不一定走相同的路径,但是逻辑过程是一样的,一直到达客户端
  13. 客户端发现 MAC 地址符合、IP 地址符合,于是就会交给 TCP 层。根据序列号看是不是自己要的报文段,如果是,则会根据 TCP 头中的端口号,发给相应的进程,这个进程就是浏览器
  14. 当浏览器拿到了HTTP 的报文,发现返回 200,一切正常,于是就从正文中将 HTML 拿出来,浏览器对其进行渲染展示。

12.4 HTTP 2.0

原文:HTTP1.0、HTTP1.1 和 HTTP2.0 的区别

12.4.1 HTTP 1.0 和 HTTP 1.1 的一些区别

  1. 缓存处理,在 HTTP 1.0 中主要使用 header 里的 If-Modified-Since、Expires 来做为缓存判断的标准,HTTP 1.1 则引入了更多的缓存控制策略,例如 Entity tag,If-Unmodfied-Since,If-Match,If-None-Match 等更多可供选择的缓存头来控制缓存策略
  2. 带宽优化及网络连接的使用,HTTP 1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP 1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
  3. 错误通知的管理,在 HTTP 1.1 中新增了 24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源当前的状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
  4. Host 头处理,在 HTTP 1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个 IP 地址。HTTP 1.1 的请求消息和响应消息都已支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)
  5. 长连接,HTTP 1.1 支持长连接和请求流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP 1.1 中默认开启 Connection:keep-alive,一定程度上弥补了 HTTP 1.0 每次请求都要创建连接的缺点

12.4.2 HTTP 1.1 的缺点

  1. 纯文本通信,每次通信都要带完整的 HTTP 头部
  2. 如果不使用管道化 pipelining 特性,TCP 的长连接复用是:一个请求响应完毕后,下一个请求才能发送,如果请求阻塞了,后面的请求都会受影响(请求-响应、请求-响应,串行方式使用 TCP 长连接)
  3. 如果使用管道化 pipelining 特性,TCP 的长连接复用是:一个请求发送完毕后,无需等待响应,便可发送下一个请求。但是服务端响应的时候只能按照客户端发送的请求的顺序进行响应,如果第一个请求处理的特别慢,后面的请求即使处理完毕,也要等着,这就是著名的 队头阻塞问题 ,(请求1 - 请求 2 - 请求 3,响应1 - 响应 2- 响应 3,半并行化使用 TCP 长连接,如果响应 1 阻塞,响应 2 、响应 3 即使完成了,也不能在 TCP 连接上传输,要等待响应 1 的完成,响应 1 传输完了,响应 2、响应 3 才能开始传输)
12.4.2.1 使用管道化的限制

原文:白话http队头阻塞

并且使用 HTTP 管道化还有一些限制:

  1. 管道化要求服务端按照请求方发送的顺序返回响应(FIFO),原因很简单,HTTP 请求和响应并没有序号标识,无法将乱序的响应与请求关联起来
  2. 当客户端在支持管道化时需要保持未收到响应的请求,当连接意外中断时,需要重新发送这部分请求。如果这个请求只是从服务器获取数据,那么并不会对资源造成任何影响,而如果是一个提交信息的请求,比如 post 请求,那么可能会造成资源多次提交从而改变资源,这是不允许的。而不会对服务器资源产生影响的请求叫作幂等请求。客户端在使用管道化的时候请求方式必须是 幂等请求

现代浏览器默认都关闭了管道化,并且大部分服务器也是默认不支持管道化的

12.4.2.2 如何解决队头阻塞?

针对每个域名而言,浏览器允许客户端使用 并发长连接,比如 Chrome 是 6 个,也就是页面中如果针对同一个域名有多个 HTTP 请求,Chrome 会针对这个域名建立 6 个 TCP 长连接,在每个长连接里面再去处理 HTTP 请求

例如下面这种情况
在这里插入图片描述

12.4.3 HTTP 2.0 的改进

以下主要来源于小林Coding《图解网络》

12.4.3.1 头部压缩

HTTP 2.0 使用 HPACK 算法,客户端和服务器两端都会维护 “字典”,用长度较小的索引号表示重复的字符串,再用哈夫曼编码压缩数据。

HPACK 算法主要包含三个组成部分:

  • 静态字典
  • 动态字典
  • 哈夫曼编码(压缩算法)

静态表包含 61 组高频出现在头部的字符串和字段,它是写入 HTTP 2.0 框架的,不会变化的,表中包含 index(索引)、Header Name(字段名)、Header Value(索引对应的值)。

其中表中有部分 index 没有对应的 Header Value,因为这些 Value 并不是固定的,而是变化的,这些 Value 都会经过哈夫曼编码后,发送出去。

静态表中只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 index 从 62 起步,会在编码解码时随时更新。

比如,第一次发送时头部中的 user-agent 字段数据有上百个字节,经过哈夫曼编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 index 号 62,那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部,理论上随着同一 HTTP 2.0 连接上发送的报文越来越多,客户端与服务端双方字典积累的越来越多,最终每个头部字段都会变成 1 个字节的 index,这样便避免了大量的冗余数据的传输,大大节约了带宽。

但是动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests 的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP 2.0 连接来释放内存。

12.4.3.2 二进制帧

HTTP 2.0 厉害的地方在于将 HTTP 1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析

HTTP 2.0 将所有的传输信息分割为更小的消息和帧(Frame),常见的帧有 Header 帧,用于传输 Header 内容,再就是 Data 帧,用来传输正文实体,采用二进制编码。
在这里插入图片描述

12.4.3.3 并发传输

HTTP 2.0 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP 1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量

  • 一个 TCP 连接包含一个或者多个 Stream
  • Stream 包含一个或多个 Message ,Message 对应 HTTP 1 中的请求或响应,有 HTTP 头部和包体构成
  • Message 里包含一条或者多个 Frame,Frame 是 HTTP 2.0 的最小单位,以二进制压缩格式存放 HTTP 1 中的内容(头部和包体)

在 HTTP 2.0 的连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而 同一 Stream 内部的帧必须是严格有序的。

客户端和服务器双方都可以建立 Stream ,客户端建立的 Stream ID 必须是奇数号,而服务器建立的 Stream ID 必须是偶数号。

HTTP 2.0 对于每个 Stream 还可以设置不同优先级,比如可以设置服务器先传递 HTML/CSS,再传递图片,以此来提高用户体验。

12.4.3.4 服务器主动推送资源

HTTP 1.1 都是客户端向服务器发起请求后,才能获取到服务器响应的资源。

HTTP 2.0 中服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确的请求,省去了客户端重复请求的步骤。

客户端发起的请求,必须使用的是奇数号的 Stream ,服务器主动的推送,使用的是偶数号 Stream 。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段来告知客户端,接下来会在哪个偶数号 Stream 中发送包体。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流。
在这里插入图片描述

12.3.4.5 HTTP 2.0 的性能瓶颈

现在压力都集中在底层一个 TCP 连接上,TCP 层是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当 “前一个字节数据” 没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这一个字节数据到达时,HTTP 应用层才能从内核中拿到数据,这就是 HTTP 2.0 队头阻塞的问题。

12.5 HTTP 3.0

12.5.1 QUIC 协议的机制

机制一:自定义连接机制

一条 TCP 连接是由四元组标识的,分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时,就需要断开重连。比如说,在移动设备上,从移动网络切换到 WIFI 时,都会导致重连,而建立连接的过程包括 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

QUIC 是以一个 64 位的随机数作为 ID 来标识,而且 UDP 是无连接的,所以当 IP 或者端口变化时,只要 ID 不变,就不需要重新建立连接

机制二:自定义重传机制

在 TCP 中,为了保证可靠性,使用 序号应答 机制,来解决顺序问题和丢包问题,通过 自适应重传算法采样往返时间 RTT,不断调整超时时间,一旦某个包在超时时间内未得到应答,就重新发送。

但是其中存在着采样不准确的问题。例如,发送一个包,序号为 100,发现没有返回,于是再发送一个 100,过一阵返回一个 ACK 101,但是这个包往返时间按哪个算呢,按第一个发送的时间算,时间可能算长了,按第二个发送的时间算,时间了算短了。

QUIC 也有个序列号,是递增的。任何一个序列号的包只发送一次,下次就要加一了。例如,发送一个包,序号是 100,发现没有返回;再次发送的时候,序号就是 101 了;如果返回的是 ACK 100,就是对第一个包的响应,如果返回 ACK 101,就是对第二个包的响应,RTT 计算相对准确。

QUIC 定义了一个 offset 的概念来确保两个包发送的是同样的内容,QUIC 是面向连接的,也就像 TCP 一样,是一个数据流,发送的数据在这个数据流里面有个偏移量 offset,可以通过 offset 查看数据发送到了哪里,这样只要这个 offset 的包没有来,就要重发;如果来了,按照 offset 拼接,还是能够拼成一个流。
在这里插入图片描述

机制三:无阻塞的多路复用

假如某一个流丢了一个 UDP 包,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,知道 QUIC 重传丢失的报文,数据才会交给 HTTP/3。但是其他流的数据报文只要被完整接收,HTTP/3 就可以读取到数据。

所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。

机制四:自定义流量控制

TCP 的流量控制是通过 滑动窗口协议。QUIC 的流量控制也是通过 window_update ,来告诉对端它可以接受的字节数。但是 QUIC 的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个 stream 控制窗口。

QUIC 的 ACK 是基于 offset 的,每个 offset 的包来了,进行缓存,就可以应答,应答后就不会重发,中间的空档会等待到来或者重发即可,而窗口的起始位置为当前收到的最大 offset ,从这个 offset 到当前的 stream 所能容纳的最大缓存,是真正的窗口大小。

另外,还有整个连接的窗口,需要对于所有的 stream 的窗口做一个统计。

对于一个连接上的控制窗口来说:接收窗口 = stream1 接收窗口 + stream2 接收窗口 + ··· + streamN 接收窗口 。
在这里插入图片描述

13. HTTPS

13.1 对称加密

  • 加密和解密的秘钥是相同的,因此对称加密算法要保证安全性的话,秘钥要做好保密

13.2 非对称加密

  • 在非对称加密算法中,加密使用的秘钥和解密使用的秘钥是不相同的。一把是作为公开的公钥,另一把是作为谁都不能给的私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。

13.3 中间人攻击

非对称加密的算法都是公开的,所有人都可以自己生成一对公钥私钥。

当服务端向客户端返回公钥 A1 的时候,中间人将其 替换 成自己的公钥 B1 传送给浏览器。

而浏览器此时一无所知,使用公钥 B1 加密了密钥 K 发送出去,又被 中间人截获,中间人利用自己的私钥 B2 解密,得到密钥 K ,再使用服务端的公钥 A1 加密传送给服务端,完成了通信链路,而服务端和客户端毫无感知。
在这里插入图片描述
出现这一问题的核心原因是 客户端无法确认收到的公钥是不是真的是服务端发来的,为了解决这个问题,互联网引入了一个公信机构,这就是 CA。

13.4 CA 证书

服务端在使用 HTTPS 前,去经过认证的 CA 机构申请颁发一份 数字证书,数字证书里包含有证书持有者、证书有效期、公钥等信息,服务端将证书发送给客户端,客户端校验证书身份和要访问的网站身份确实一致后再进行后续的加密操作。

但是,如果中间人也聪明一点,只改动证书中的公钥部分,客户端依然不能确认证书 是否被篡改,这时就需要一些防伪技术了

使用 私钥 来对信息进行加密作为 数字签名

  1. CA 机构在颁发证书时对证书明文信息进行哈希
  2. 将哈希值用私钥进行 加签,得到数字签名

在 HTTPS 连接建立的过程中,明文数据和数字签名组成证书,传递给客户端

  1. 客户端得到证书,分解成明文部分 Text 和数字签名 Sig1
  2. 用 CA 机构的公钥进行 解签,得到 Sig2(由于 CA 机构是一种公信身份,因此在系统或浏览器中会内置 CA 机构的证书和公钥信息)
  3. 用证书里声明的哈希算法对明文 Text 部分进行哈希,得到 H
  4. 当自己计算得到的哈希值 H 与解签后的 Sig2 相等,表示证书可信,没有被篡改

因为签名是由 CA 机构的私钥生成的,中间人篡改信息后无法拿到 CA 机构的私钥对其再次加密,这样就保证了证书可信

详见文章:《大前端进阶 安全》系列 HTTPS详解(通俗易懂)

13.5 SSL/TLS 的运行机制

问题1:如何保证公钥不被篡改?

  • 如上所述,将公钥放在 数字证书 中。只要证书是可信的,公钥就是可信的

问题2:公钥加密计算量太大,如何减少耗用的时间?

  • 每一次对话(session),客户端和服务器端都生成一个 “对话密钥”,用它来加密信息,由于 “对话密钥” 是对称加密,所以运算速度非常快,而服务器公钥只用于加密 “对话密钥” 本身,这样就减少了加密运算的消耗时间。

13.5.1 客户端发出请求(ClientHello)

首先,客户端先向服务器发出发出加密通信的请求,叫作 ClientHello 请求

主要提供以下信息:

  1. 支持的协议版本,比如 TLS 1.0
  2. 一个客户端生成的随机数,稍后用于生成 “对话秘钥”
  3. 支持的加密方法,比如 RSA 公钥加密
  4. 支持的压缩方法

13.5.2 服务器回应(ServerHello)

服务器收到客户端请求后,向客户端发出回应,这叫作 ServerHello。

包含以下内容:

  1. 确认使用的加密通信协议版本,如 TLS 1.0
  2. 一个服务器生成的随机数,稍后用于生成 “对话秘钥”
  3. 确认使用的加密方法,如 RSA 公钥加密
  4. 服务器证书

13.5.3 客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息:

  1. 一个随机数(Pre-Master-Key)。该随机数用服务器公钥加密,防止被窃听
  2. 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送
  3. 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的 hash 值,用来供服务器校验
13.5.3.1 为什么需要三个随机数?
  • 不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于 SSL 协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性
  • 对于 RSA 密钥交换算法来说,Pre-Master-Key 本身就是一个随机数,再加上 hello 消息中的随机,三个随机通过一个密钥导出器最终导出一个对称密钥
  • Pre-Master 的存在在于 SSL 协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那 pre-master-secret 就有可能被猜出来,那么仅使用 pre-master-secret 作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上 pre-master-secret 三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的可不是一。

13.5.4 服务器最后的回应

服务器在收到客户端的第三个随机数 pre-master key 之后,计算生成本次会话所用的 “会话密钥”。然后,向客户端最后发送下面信息:

  1. 编码通知改变,表示随后的信息都将用双方商定的加密方法和密钥发送
  2. 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的 hash 值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用 “会话密钥” 加密内容。
在这里插入图片描述

来源于:SSL/TLS协议运行机制的概述

13.6 HTTP 和 HTTPS 的区别

  1. 最重要的区别就是 安全性,HTTP 明文传输,不对数据进行加密,安全性较差。HTTPS 的数据传输过程是加密的,安全性较好
  2. 使用 HTTPS 协议需要申请 CA 证书
  3. HTTPS 由于加了一层安全层,建立连接的过程更复杂,也要交换更多的数据,难免影响速度,所以 HTTP 页面响应速度必 HTTPS 快
  4. HTTP 的默认端口是 80,HTTPS 的默认端口是 443

13.7 HTTPS 的缺点

  1. 在相同的网络环境中,HTTPS 相比 HTTP 无论是响应时间还是耗电量都有大幅度上升
  2. HTTPS 的安全是有范围的,在黑客攻击、服务器劫持的情况下几乎起不到作用
  3. HTTPS 需要更多的服务器资源,也会导致成本的升高
  4. 证书要钱

14. DNS

14.1 DNS 解析流程

  1. 电脑客户端会发出一个 DNS 查询报文(比如说查询 a.b.com),其中包含域名信息、类型等,并发给本地域名服务器(本地 DNS)
  2. 本地 DNS 服务器收到来自客户端的请求,先在自己的缓存表中查询是否有这个域名的解析记录,如果没有则将该报文转发到根 DNS 服务器
  3. 根 DNS 服务器注意到 com 前缀,便向本地 DNS 服务器返回 com 对应的顶级域 DNS 服务器的 IP 地址列表
  4. 本地 DNS 向其中一台顶级域 DNS 服务器发送查询报文
  5. 该顶级域服务器注意到 b.com 的前缀,向本地 DNS 服务器返回负责该域名的权威 DNS 服务器的地址
  6. 本地 DNS 服务器向权威 DNS 服务器发送查询报文,权威 DNS 服务器查询后将对应的 IP 地址返回给本地 DNS 服务器
  7. 本地 DNS 在将 IP 地址返回给客户端,客户端和目标建立连接

14.2 负载均衡

14.2.1 内部负载均衡

例如,一个应用要访问数据库,在这个应用里面应该配置这个数据库的域名,而不是 IP 地址,因为一旦这个数据库,因为某种原因,换到了另外一台机器上,而如果有很多个应用都配置了这台数据库的话,一换 IP 地址,就需要将这些应用全部修改一遍。但是如果配置了域名,则只要在 DNS 服务器里,将域名映射为新的 IP 地址,这个工作就完成了,大大简化了运维。

另一方面,我们可以通过配置一些策略,在域名解析的时候,解析出不同服务器的 IP,这次返回第一个,下次返回第二个,这样轮询或带权重的返回 IP 地址,实现负载均衡。

14.2.2 全局负载均衡

为了保证我们的高可用,往往会部署多个机房,每个地方都会有自己的 IP 地址。当用户访问某个域名的时候,这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某些原因挂掉了,只要在 DNS 服务器中,将这个数据中心对应的 IP 地址删除,就可以实现一定的 高可用

另一方面,可以通过引入 全局负载均衡器 来实现用户访问属于相同运营商的距离较近的数据中心。大概流程就是,在 DNS 解析的时候,权威 DNS 服务器返回的并不是用户请求的域名的 IP 地址,而是返回该域名对应的全局负载均衡器的 IP 地址,然后,在负载均衡器上实现一定的策略,来达到负载均衡。
地 DNS 向其中一台顶级域 DNS 服务器发送查询报文
5. 该顶级域服务器注意到 b.com 的前缀,向本地 DNS 服务器返回负责该域名的权威 DNS 服务器的地址
6. 本地 DNS 服务器向权威 DNS 服务器发送查询报文,权威 DNS 服务器查询后将对应的 IP 地址返回给本地 DNS 服务器
7. 本地 DNS 在将 IP 地址返回给客户端,客户端和目标建立连接

14.2 负载均衡

14.2.1 内部负载均衡

例如,一个应用要访问数据库,在这个应用里面应该配置这个数据库的域名,而不是 IP 地址,因为一旦这个数据库,因为某种原因,换到了另外一台机器上,而如果有很多个应用都配置了这台数据库的话,一换 IP 地址,就需要将这些应用全部修改一遍。但是如果配置了域名,则只要在 DNS 服务器里,将域名映射为新的 IP 地址,这个工作就完成了,大大简化了运维。

另一方面,我们可以通过配置一些策略,在域名解析的时候,解析出不同服务器的 IP,这次返回第一个,下次返回第二个,这样轮询或带权重的返回 IP 地址,实现负载均衡。

14.2.2 全局负载均衡

为了保证我们的高可用,往往会部署多个机房,每个地方都会有自己的 IP 地址。当用户访问某个域名的时候,这个 IP 地址可以轮询访问多个数据中心。如果一个数据中心因为某些原因挂掉了,只要在 DNS 服务器中,将这个数据中心对应的 IP 地址删除,就可以实现一定的 高可用

另一方面,可以通过引入 全局负载均衡器 来实现用户访问属于相同运营商的距离较近的数据中心。大概流程就是,在 DNS 解析的时候,权威 DNS 服务器返回的并不是用户请求的域名的 IP 地址,而是返回该域名对应的全局负载均衡器的 IP 地址,然后,在负载均衡器上实现一定的策略,来达到负载均衡。

  • 0
    点赞
  • 2
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

ZhiZDK

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值