网络的基本概念

106 阅读28分钟

我们想访问一个网站,需要经历以下复杂的流程:

但其实真实过程远远不止这么多步,看完这篇文章,你可能有不一样的理解。

OSI 七层模型

七层模型,亦称OSI(Open System Interconnection)。参考模型是国际标准化组织(ISO)制定的一个用于计算机或通信系统间互联的标准体系,一般称为OSI参考模型或七层模型。

而在实际使用中,我们往往称之为五层,最顶上三层可以合并为应用层。 七层模型是倒着看的,下层则是为了上层提供服务的

IP 地址和 MAC 地址

我们想实现两个网络间的通信,最终靠的是网卡,网卡在生产的过程中,就有自己的一个身份标识 MAC 地址(原则上唯一,不考虑一些手段修改的情况)。 也就是说,MAC 地址就像我们的身份证一样,我们只有一个身份证,怎么去找这个人呢,送快递也不能只拿身份证信息吧,所以我们还需要一个地址,在这里,ip(分为 ipv4 和 ipv6) 就是地址,我可以通过 ip 地址找到对面那个人,让他把 MAC 地址给我,然后通过两边的 MAC 地址交换数据。

ipv4 地址是一个 32 位的二进制数,通常被分割为 4 个 "8 位二进制数", 也就是 4 个字节,ipv4 中通常采用 "点分十进制" 表示成 "A.B.C.D" 的形式,其中 A,B,C,D 都是 255 以内的 10 进制整数(十进制的 255 就是 8 个二进制的 1),所以 ipv4 中大概能提供 32 个二进制位的 ip 地址,大概有 43 亿个,这样产生的 ip 地址数量很快就不够用了,因为当时设计 ip 地址的时候,哪儿知道今天有这么多计算机呀,于是 ipv6 诞生了,ipv6 是 8 个 "16 位二进制数"。

在一个子网中第一个和最后一个为保留地址,所以在一般情况局域网可使用的 IP 为 1 - 254。

MAC 地址表、 Arp 缓存表和路由表

敲黑板,这地方还是比较重要的。

MAC 地址表详解 (MAC 地址 --> 端口)

说到 MAC 地址表,就不得不说一下交换机的工作原理了,因为交换机是根据 MAC 地址表转发数据帧的。在交换机中有一张记录着局域网主机 MAC 地址与交换机接口的对应关系的表,交换机就是根据这张表负责将数据帧传输到指定的主机上的。

交换机在接收到数据帧以后,首先、会记录数据帧中的源 MAC 地址和对应的接口到 MAC 表中,接着、会检查自己的 MAC 表中是否有数据帧中目标 MAC 地址的信息,如果有则会根据 MAC 表中记录的对应接口将数据帧发送出去(也就是单播),如果没有,则会将该数据帧从非接受接口发送出去(也就是广播)。

如下图:详细讲解交换机传输数据帧的过程:

  1. 主机 A 会将一个源 MAC 地址为自己,目标 MAC 地址为主机 N 的数据帧发送给交换机。
  2. 交换机收到此数据帧后,首先将数据帧中的源 MAC 地址和对应的接口(接口为 f01) 记录到MAC地址表中。
  3. 然后交换机会检查自己的 MAC 地址表中是否有数据帧中的目标 MAC 地址的信息,如果有,则从 MAC 地址表中记录的接口发送出去,如果没有,则会将此数据帧从非接收接口的所有接口发送出去(也就是除了 f01 接口)。
  4. 这时,局域网的所有主机都会收到此数据帧,但是只有主机B收到此数据帧时会响应这个广播,并回应一个数据帧,此数据帧中包括主机 B 的 MAC 地址。
  5. 当交换机收到主机 B 回应的数据帧后,也会记录数据帧中的源 MAC 地址(也就是主机 B 的 MAC 地址),这时,再当主机 A 和主机 B 通信时,交换机根据 MAC 地址表中的记录,实现单播了。

ARP 缓存表详解 (IP --> MAC 地址)

上面我们讲解了交换机的工作原理,知道交换机是通过 MAC 地址通信的,但是我们是如何获得目标主机的 MAC 地址呢?这时我们就需要使用 ARP 协议了,在每台主机中都有一张 ARP 表,它记录着主机的 IP 地址和 MAC 地址的对应关系。

ARP协议:ARP协议是工作在网络层的协议,它负责将 IP 地址解析为 MAC 地址。

如下图:详细讲解ARP的工作原理。

  1. 如果主机 A 想发送数据给主机 B,主机 A 首先会检查自己的 ARP 缓存表,查看是否有主机 B 的 IP 地址和 MAC 地址的对应关系,如果有,则会将主机 B 的 MAC 地址作为源 MAC 地址封装到数据帧中。如果没有,主机 A 则会发送一个 ARP 请求信息,请求的目标 IP 地址是主机 B 的 IP 地址,目标 MAC 地址是 MAC 地址的广播帧(即 FF-FF-FF-FF-FF-FF),源 IP 地址和 MAC 地址是主机 A 的 IP 地址和 MAC 地址。

  2. 当交换机接受到此数据帧之后,发现此数据帧是广播帧,因此,会将此数据帧从非接收的所有接口发送出去。

  3. 当主机 B 接受到此数据帧后,会校对 IP 地址是否是自己的,并将主机 A 的 IP 地址和 MAC 地址的对应关系记录到自己的 ARP 缓存表中,同时会发送一个 ARP 应答,其中包括自己的 MAC 地址。

  4. 主机 A 在收到这个回应的数据帧之后,在自己的 ARP 缓存表中记录主机 B 的 IP 地址和 MAC 地址的对应关系。而此时交换机已经学习到了主机 A 和主机 B 的 MAC 地址了。

路由表(寻址过程)

路由器负责不同网络之间的通信,它是当今网络中的重要设备,可以说没有路由器就没有当今的互联网。在路由器中也有一张表,这张表叫路由表,记录着到不同网段的信息。路由表中的信息分为直连路由和非直连路由。

直连路由:是直接连接在路由器接口的网段,由路由器自动生成。

非直连路由:就是不是直接连接在路由器接口上的网段,此记录需要手动添加或者是使用动态路由。

路由表中记录的条目有的需要手动添加(称为静态路由),有的测试动态获取的(称为动态路由)。直连路由属于静态路由。

路由器是工作在网络层的,在网络层可以识别逻辑地址。当路由器的某个接口收到一个包时,路由器会读取包中相应的目标的逻辑地址的网络部分,然后在路由表中进行查找。如果在路由表中找到目标地址的路由条目,则把包转发到路由器的相应接口,如果在路由表中没有找到目标地址的路由条目,那么,如果路由配置有默认路由,就根据默认路由的配置转发到路由器的相应接口;如果没有配置默认路由,则将该包丢弃,并返回不可到达的信息。这就是路由器寻址的过程。

如下图:详细介绍路由器的工作原理(注:这里没有体现 NAT 的作用,旨在说明路由的寻址流程)

  1. HostA 在网络层将来自上层的报文封装成 IP 数据包,其中源 IP 地址为自己,目标 IP 地址是 HostB,HostA 会用本机配置的 24 位子网掩码与目标地址进行“按位与”运算,得出目标地址与本机不是同一网段(非同一局域网下),因此发送 HostB 的数据包需要经过网关路由 A 的转发。
  2. HostA 通过 ARP 请求获取网关路由 A 的 E0 口的 MAC 地址,并在链路层将路由器 E0 接口的 MAC 地址封装成目标 MAC 地址,源 MAC 地址是自己。
  3. 路由器 A 从 E0 可接收到数据帧,把数据链路层的封装去掉,并检查路由表中是否有目标 IP 地址网段(即 192.168.2.2 的网段)相匹配的的项,根据路由表中记录到 192.168.2.0 网段的数据把比特流发送给下一跳地址 10.1.1.2 (当然中间可能经过好多个路由),因此数据在路由器 A 的 E1 口重新封装,此时,源 MAC 地址是路由器 A 的 E1 接口的 MAC 地址,封装的目标 MAC 地址则是路由器 2 的 E1 接口的 MAC 地址。
  4. 路由 B 从 E1 口接收到数据帧,同样会把数据链路层的封装去掉,对目标 IP 地址进行检测,并与路由表进行匹配,此时发现目标地址的网段正好是自己 E0 口的直连网段,路由器 B 通过 ARP 广播,获知 HostB 的 MAC 地址,此时数据包在路由器 B 的 E0 接口再次封装,源 MAC 地址是路由器 B 的 E0 接口的 MAC 地址,目标 MAC 地址是 HostB 的 MAC 地址。封装完成后直接从路由器的 E0 接口发送给 HostB。
  5. 此时 HostB 才会收到来自 HostA 发送的数据。

总结:路由表负责记录一个网络到另一个网络的路径(不停更换源 MAC 地址和目标 MAC 地址),因此路由器是根据路由表工作的。

局域网下的通信

比如,我局域网下有三台机器,我们知道,主机之间的通信是通过网卡的,也就是需要知道对方的 MAC 地址,而我们必然不能直接知道别的网卡的 MAC 地址,所以我们需要路由器这一层,局域网内所有的电脑都要连接到路由器上,并由路由器通过 dhcp 动态分配 ip。

  1. 当我们的 MAC1 去找 MAC2 的 ip 时,先去找路由器,路由器判断是否同一个局域网内的,通过子网掩码判断是局域网内的 ip 段(这里暂且讨论局域网)。
  2. 查找 ARP 缓存表,没有记录则广播该 ip 地址,通过目标主机 MAC2 的应答帧收到目标主机的 MAC 地址, 在 ARP 缓存表记录该 ip --> MAC 地址映射。
  3. 路由器查找 MAC 地址表,没有记录则广播该 MAC 地址,通过目标主机 MAC2 的应答帧收到目标主机的端口号,在 Mac 地址表记录该 MAC 地址 --> 端口映射。
  4. 只要访问一次,下次通信直接通过缓存中的 ip 找到 MAC 地址 再找到端口号传输数据帧即可。

路由器和交换机

交换机:维护 MAC 地址表(交换机端口对应的 MAC 地址),不关心 ip 地址,核心就是交换数据,交换效率高 路由器:分为 lan 和 wan 两种端口(也有这两种口的 MAC 地址表),在不连接 wan 口的情况下,路由器可以看成是交换机。

网关

网关(Gateway) 又称网间连接器、协议转换器。 TCP/IP 中规定两个子网不能直接通信(通过子网掩码来区分两个设备是否是同一个子网),我们从内网访问到外网属于两个不同的子网。路由器就充当了网关的角色。

我们设想一个场景,比如 A 和 B 局域网都有一个 192.168.1.16 的 ip 地址,这时候访问外网,外网返回数据,如果直接返回给 192.168.1.16,这不就乱套了么。

这时候,我们突发奇想,如果能使用局域网路由器的 ip 帮我们发送,不就好了么, NAT(网络地址转换) 就是干这件事儿的,路由器会将源 IP 进行 NAT 转换,也就是将主机的 ip 转成对应公网 ip 去访问。

那问题又来了,假设服务端返回了响应数据,到了我的局域网路由器,怎么知道是哪一台主机发出的请求呢,所以,我们在 NAT 转换 ip 的时候需要给每个主机生成一个标识进行区分,这就是端口号(虚拟出的端口号),用于区分不同主机。

这样数据回来后,我们在对公网 ip 再进行一次 SNAT 反转换,拿到对应的主机的端口号去传输数据。

DNS

DNS 是 Domain Name Service 的缩写, DNS 服务器进行域名和与之对应的 IP 地址转换的 服务器,这里先来区分几个概念:

  • 顶级域名:一个点,比如 .com,baidu.com 都是顶级域名
  • 二级域名 .com.cn,三级域名 www.baidu.com.cn , 有多少个点就是几级域名

DNS 查找会先从根域名查找,而且它是基于 UDP 的(快)。

访问过程:我们访问 www.baidu.com,会先通过服务器查找离自己最近的根服务器,通过根服务器找到 .com 服务器,将 ip 返回给 DNS 服务器, DNS 服务器会继续向此 ip 发送请求,去查找对应 .com 下 .baidu 对应的 ip ,直到获取最终的 ip 地址。缓存到 DNS 服务器上,注意,这中间发送了多次 DNS 查找的请求,如果使用基于 TCP,必然会慢特别多。

TCP 协议

tcp 传输控制协议: 可靠、面向连接的协议,传输效率低 (在不可靠的 IP 层上建立可靠的传输层)。TCP提供全双工服务,即数据可在同一时间双向传播。数据是无序地(因为可能丢包,不能保证有序)在网络间传递,接收方需要有一种算法在接受到数据后恢复原有的顺序。

tcp 在包装的时候,会把我们的数据增加一个报文头。

tcp 报文头:

  1. tcp 传输(传输层)是不需要封装 ip 地址的,只要有端口号即可,ip 是在网络层添加的,一个是源端口,一个是目标端口,如上端口最大为 16 位,最大为 16 个 1,也就是 65535
console.log(0b1111111111111111) // 65535
  1. 序列号和确认号分别代表着数据分段后的标识和接收到的标识,32 位大概是 4 个 g,换而言之就是 tcp 传输大文件会先对 4 个 g 的内容做处理(分段,传输),传输完毕后再处理剩下的,直至传输完毕。
  2. 描述 4 位的首部长度,4 位最大表示 15,也就是 15 的首部长度,根据 RFC791 定义,首部长度的单位是 32 位的字长(一字长4字节),所以得出结论,tcp 首部最大为 15 * 4 = 60 字节,不过典型的IP数据报不使用首部中的选项,因此典型的 IP 数据报首部长度是 20 字节。
  3. 6 个保留位,目前没用到的占位(说不定未来会用)。
  4. 6 个标识的字符: @1 URG(紧急): 代表传输包不想要了,比如 ctrl + c 取消访问了 @2 ACK(确认): ACK = 1 表示确认收到 @3 PSH(推送): 表示数据已经发出去了 @4 RST(复位): 当 RST = 1 时,表明 TCP 连接中出现了严重错误。例如主机崩溃或者其他原因,必须释放连接。然后再重新建立连接 @5 SYN(同步): 在连接建立时用来进行同步的信号,当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文。对方若同意建立连接,则在响应的报文段中使 SYN = 1 , ACK = 1。 @6 FIN(终止): 用来释放一个连接,当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并释放 TCP 连接,这个字段只有在 TCP 的四次挥手时候才会被置为 1.
  5. 滑动窗口,16 位,最大为 16 个 1,也就是 65535。
  6. 检验和,检验(报文的)范围包括首部和数据两部分。在计算检验和时,都要在TCP报文段的前面添加 12 字节的伪首部。检验和有什么用?这关系到报文的正确性问题。为了保证TCP连接上传输的数据的正确性,即从发送方传送到接收方没有出现错误,用检验和来确定数据传输过程是否发生了错误。
  7. 紧急指针,只有 URG 标志位被设置时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。

可以看到,一个 tcp 段就至少有 20 个字节的 tcp 头的开销,所以如果每次传递一个比较小的数据,tcp 传输的过程中会进行粘包(优化)

wireshark 抓包 && 三次握手四次挥手

这里我们借助 wireshark 来抓网络包观察,先来创建客户端和服务端。

新建 client.js 创建客户端

const net = require('net');
const socket = new net.Socket();

socket.connect(8999, 'localhost');

socket.on('connect', function(data) {
    socket.write('在吗'); // 可写流
});

socket.on('data', function(data) { // 可读流
  console.log(`from server: ${ data.toString() }`)
})

socket.on('error', function(error) {
    console.log(error);
});

新建 server.js 创建服务端

const net = require('net');

const server = net.createServer(function(socket){
    socket.on('data',function (data) { // 可读流
        socket.write('你好呀'); // 可写流
        console.log(`from client: ${ data.toString() }`)
    });

    socket.on('end',function () {
      console.log('客户端关闭') 
    })
})

server.on('error',function (err) {
    console.log(err);
})

server.listen(8999);

又能读又能写,这就是双工流。

打开 wireshark,选择 loopback

筛选端口为 8989 的 TCP 连接

启动 server 服务,并使用客户端访问

node server.js
node.client.js

可以看到,wireshark 上一下出现了多条 tcp 访问的记录。

**三次握手:**1-3行。 **数据传输:**5-8行。

**四次挥手:**客户端主动断开(请忽略端口号,手残起了两下服务,客户端虚拟端口号变了):

三次握手(SYN, ACK)

  1. SYN(同步):第 1 次握手,发送同步包给服务端,seq = 0, (客户端请求连接)
  2. SYN & ACK(确认):第 2 次握手,返回确认包给客户端,seq = 0,SYN = 1,ACK = 1 (确认收到你的连接请求)
  3. ACK(确认):第 3 次握手,发送确认包给服务端,seq = 1,ACK = 1 (收到服务端的回应,开始连接)

以上阶段没有提及 SYN,因为抓包也没有显示 SYN 的值,可以理解它代表的就是请求连接的一个状态, 第一次握手和第二次握手都为 1,第三次握手为 0

传输包标识(flag)传输方向seqACK
[SYN] 发送连接请求client --> server0-
[SYN, ACK] 收到,
发送连接请求
server --> client01 (ACK + 1)
[ACK] 收到client --> server11

数据传输(PSH)

抓包图中的,标识为 [PSH, ACK] 的连接,可以看到一个是 55434 -> 8999, 一个是 8999 -> 55434。

  1. PSH(推送) & ACK:客户端向服务端发送 len = 6 的数据("在吗"),seq = 1,ACK = 1 (客户端 -> 服务端)
  2. ACK:服务端收到后,给客户端一个 len = 0 的 ACK 消息,seq = 1,ACK = 7 (确认收到你的消息)
  3. PSH(推送) & ACK:服务端向客户端发送 len = 9 的数据("你好呀"),seq = 1,ACK = 7 (服务端 -> 客户端)
  4. ACK:客户端收到后,给客户端一个 len = 0 的 ACK 消息,seq = 7,ACK = 10 (确认收到你的消息)
传输包标识(flag)传输方向传输字节数seqACK
[PSH, ACK] 发送消息client --> server611
[ACK] 确认收到server --> client017 (服务端接收了 6 + 1 个字节)
[PSH, ACK] 发送消息server --> client917
[ACK] 确认收到client --> server07 (序号从 7 开始)10

四次挥手

  1. FIN(终止) & ACK:客户端向服务端发起终止请求,seq = 1,ACK = 1 (客户端 -> 服务端)
  2. ACK:服务端收到后,给客户端一个 len = 0 的 ACK 消息,seq = 1,ACK = 2 (确认收到你的消息,但是我可能还有一些事情要处理,等我通知)
  3. FIN(终止) & ACK:服务端向客户端发送发起终止请求,seq = 1,ACK = 2 (可以断开了)
  4. ACK:客户端收到后,给客户端一个 len = 0 的 ACK 消息,seq = 2,ACK = 2 (收到你的消息,断开连接)
传输包标识(flag)传输方向seqACK
[FIN, ACK] 发送断开请求client --> server11
[ACK] 确认收到server --> client12
[FIN, ACK] 收到,
发送断开请求
server --> client12
[ACK] 收到client --> server22

总结

  • TCP是双工的,所以握手需要3次。保证双方达成一致
  • 断开链接时,发送(FIN)时另一方需要马上回复 (ACK), 但此时不能立即关闭 (可能有未发送完的数据,还有一些准备断开的操作),所以等待确定可以关闭时再发送 (FIN),所以握手需要四次

TCP 滑动窗口(传输速率控制 + 丢包重传)

tcp 又叫传输控制协议,可见它不仅能传输数据,还能控制传输速率,这里面就有个滑动窗口的概念,旨在确认每次发送多少个序号范围(seq)的包给接收方,发送端有发送缓存区,接收端有接收缓存区,要发送的数据都放到发送者的缓存区内等待发送,在建立连接时,接收端会告诉发送端自己的窗口大小(rwnd), 每次接收端收到数据后都会再次确认(rwnd)大小,如果值为 0 (接收方缓存区写满了),停止发送数据,并发送窗口探测包,持续监测窗口大小。

比如 A 想和 B 通信,A 中的数据假如说在 tcp 层某段上有 10 个字节,而在 A 和 B 建立连接时(三次握手时接收方发送的 ACK 包)或者上一段数据发送完毕时,B 会告诉 A 自己的窗口大小(win),也就是说我现在能接受最大窗口的数据。

下图中发送方和接收方都经过了缓存区(长条代表缓存区),且发送方到接收方之间还经过了网络层、数据链路层和物理层。

需要注意以下几点:

  1. 每次发送成功,接收方都会告诉发送方窗口大小。
  2. 每个窗口首部包发送完成,窗口才后移,但是后移多少位得看当前发送完了哪些包,比如图上 1 发送完,窗口要往后移一位,此时 2 包丢了,但是 3 和 4 包全部发送成功了,这时候等到 2 包超时重新发送完毕(这里没有考虑快重传),窗口左边界会直达 5 包。
  3. 直到接收方缓存区窗口为 0,需要等待接收方的应用层读取缓存区来消费数据,这时候会定时发送嗅探包,等待 ACK 中的 win 字段不为 0,继续发送。

TCP 粘包

上述窗口还有一个问题,因为我每个 tcp 报文至少有 20 字节的报文头,而我滑动窗口为 4,要并发的发送四次 tcp 消息,这导致了一个问题,我每次只发送 1 字节的数据,但是报文就多了 20 字节,这是极大的浪费,所以 tcp 通过一个算法,实现了粘包的效果。

Nagle 算法的基本定义是任意时刻,最多只能有一个未被确认的小段 (TCP内部控制),那么粘到多大为止呢,这就涉及到另一个算法。
Cork 算法当达到 MSS (Maximum Segment Size )值时统一进行发送,一般用这个值 (此值就是帧的大小(一般1500字节) - ip 头 - tcp 头 = 1460个字节内容),但是帧大小在不同网络下是可 变的,所以这个值也不是固定的。

什么意思呢,就是说,比如发 1 包的时候,tcp 发现它很小,就不会马上发送它,而是把整个窗口的 n 个包打包起来发送(具体看粘包后大小)。

注意:粘包和分段不是一个概念:

粘包是指多次发送的包被合并在一次去发送。 分段是指一个包的不同内容被分成多次发送,每次的数据为一段,比如窗口大小是 64 k,而段是 1500 字节,那么不加控制的话,一次粘包会发送 53.7 段数据,而加上 MSS 限制,那么就按 1460 字节为一次粘包结果去发送,一个窗口内发送多次罢了(发足 64k)。

TCP 慢启动,拥塞避免、快重传、快恢复

假设接收方窗口大小是无限的,接收到数据后就能发送 ACK 包,那么传输数据主要是依赖于网络带宽,带宽的大小是有限的,如果超过带宽限制,可能就丢包啦(比如游戏延迟)。

所以我们想到,这种理想情况下,是不是也要控制下发送的频率呢,于是早期的 tahoe 算法诞生了。

tahoe 版本(慢启动,拥塞避免)

慢启动:是传输控制协议使用的一种拥塞控制机制。慢启动也叫做指数增长期。慢启动是指每次TCP接收窗口收到确认时都会增长。增加的大小就是已确认段的数目。这种情况一直保持到要么没有收到一些段,要么窗口大小到达预先定义的阈值。如果发生丢失事件,TCP 就认为这是网络阻塞,就会采取措施减轻网络拥挤。

慢启动的慢体现在不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。

拥塞避免:一旦发生丢失事件或者到达阈值,TCP就会进入线性增长阶段。这时,每经过一个 RTT 窗口增长一个段,这种算法叫做拥塞避免算法。

下图关键指标描述: cwnd: 拥塞窗口大小(其实也就是当前窗口的大小,坐标系的 y 轴),窗口大到一定程度,就发不了了,要丢包了(宽带最大承受极限窗口阀值)。 sshthresh: 慢启动的阀值,代表我达到多少以后,需要把窗口增长变小(超过此阀值,传输效率增长明显降低)。 RTT: 代表从发送到确认的时间。

比如我问妈妈要钱,刚开始时成倍成倍增加的要钱(慢开始),妈妈也答应的很爽快,到达一个心理预期后(sshthresh,感觉再要太多要挨揍了,),开始缓慢的增加(拥塞避免算法),一直到再加妈妈也不给了(听不到)的时候(丢包了,cwnd 到达阀值),此时立刻再从最低开始要,并把心理预期根据刚刚妈妈给不了的阀值去折半(sshthresh = cwnd / 2),继续上述流程,多次这样折半处理后,最后网络就达到了一个平衡的过程。

这个版本有个问题,一旦丢包了,要等待超时才知道丢包了,然后 win 重新从 一个较低值开始去增长,这样传输的性能肯定是不理想的,所以有了下面这个 Reno 算法。

Reno 版本(增加快重传、快恢复)

快重传:要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期(不用等待超时)。

1 包达到后,接收方回应 ACK 收到啦,然后经常迷路的 2 包又迷路了,此时 3 包发送成功,接收方就会回复:2 包呢? 4 包发送成功,接收方接着问:2 包呢? 5 包发送成功(假设窗口为 5),接收方又问:2 包呢,此刻发送方发现 2 包被传呼了三次,立即重发 2 包,并在 2 包发送成功后,ACK 回复 5 包收到了。

快恢复:快重传配合使用的还有快恢复算法,当触发快重传(丢包了),就把 ssthresh 门限减半。但是接下去并不执行慢开始算法(不从 0 开始计算窗口),然后将 cwnd(当前的发送窗口) 设置为 ssthresh 大小(相当于也减半啦),再执行拥塞避免算法(线性增长)。

比如我问妈妈要钱,刚开始时成倍成倍增加的要钱(慢开始),妈妈也答应的很爽快,到达一个心理预期后(sshthresh,感觉再要太多要挨揍了),开始缓慢的增加(拥塞避免算法),一直到再加妈妈也不给了(听不到)的时候,继续要三次,然后直接把心理预期值降为当前要钱的一半(sshthresh = cwnd / 2),然后要的钱也减为刚才要的一半(cwnd / 2),以此触发线性增长的拥塞避免算法。

http 版本区别

  • http 0.9 在传输过程中,没有请求头和请求体,服务器响应没有返回头信息,内容采用 ASCII 字符流蓝进行传输 HTML
    • 不能区分请求类型,只能传输 html
  • http 1.0 增加了请求头和响应头,实现多类型数据传输这种串行的传输,劣势有二:
    • 每次都要重新进行 tcp 三次握手,比较慢
    • 串行传输,一个传输完成,另一个才能开始传输
  • http 1.1,现在最常用的 http 版本。
    • keepa-live:默认开启持久链接(keep-alive),在一个 TCP 链接上可以传输多个 http 请求
    • 单域名 6 链接:采用管线化的方式(分域名传输,每个域名能维护 6 个 TCP 持久链接,比如多个 cdn)
    • 存在队首阻塞:队首阻塞,虽然客户端可以并行发送多个请求,服务器端并行接收了多个请求,但由于要遵守 http 1.1 的规则,先接收到的请求需要先发送响应,造成了阻塞问题
    • 增加 cookie:为无状态的 http 增加了 cookie 和一些安全机制
  • http 2.0
    • 多路复用:因为 1.1 中的 6 个 TCP 连接都有 慢启动,多个 tcp 竞争带宽,队首阻塞等缺陷,这样 http2 就采用多路复用的机制,一个域名只开一个 TCP 长链接,我们可以通过一个 TCP 传输多个数据(通过二进制分帧层实现),基于流,把不同类型的数据在一个 TCP 管道上进行分帧传输,服务端接收到之后按序号重组,收到请求的数据包信息。不过多路复用本质上只是缓解了 tcp 层的队首阻塞(tcp 连接没有那么多了),但是并不能从本质上解决不同域名的队首阻塞。(一共只能 6个 network 线程)
    • 服务器主动推送消息:之前的 TCP 发送消息包机制,都是一问一答,无法做到服务器主动给浏览器推送消息,http2 实现了它。
    • 头部压缩:通过 hpyck 算法,通过一个映射表把头部给映射为一个数字(压缩)
  • http 3.0 为了解决 tcp 队首阻塞问题,采用 QUIC 协议。QUIC 协议是基于 UDP 的(目前:支持和部署是最大的问题)
    • 不可靠的 udp:udp 不会创建 tcp 链接,也不保证消息可靠
    • 过往优化全部作废:以前对 tcp 所有的优化,全部作废了
  • HTTP 明文传输,在传输过程中会经历路由器、运营商等环节,数据有可能被窃取或篡改(安全问题),所以后来有了 https,这里暂且不展开。