image-20231117114707878

阅读的顺序可以不用从头读到尾,你可以根据你想要了解的知识点,通过本站的搜索功能,去看哪个章节的内容就好,可以随意阅读任何章节。

图解网络介绍 | 小林coding (xiaolincoding.com)

基础篇

为什么要有TCP/IP 网络模型?

对于不同设备上的进程间通信,就需要网络通信,而设备是多样性的,所以要兼容多种多样的设备,就协商出了一套通用的网络协议

这个网络协议是分层的,每一层都有各自的作用和职责

TCP/IP 网络模型相比 OSI 网络模型简化了不少,也更加易记,它们之间的关系如下图:

img

img

帧、报文、数据包的差别_报文和帧的区别-CSDN博客

报文、报文段、数据报、数据包、帧的概念区别_数据报和报文的区别-CSDN博客 报文、数据段、分组、帧、PDU概念_pdu报文-CSDN博客

img

网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。

TCP/IP网络模型

应用层

应用层位于传输层之上,主要提供不同终端的进程之间的信息交换服务,它定义了信息交换的格式,消息会交给下一层传输层来传输。

应用层交互的数据单元称为报文。

应用层是工作在操作系统中的用户态,传输层及以下则工作在内核态

常见的应用层协议:

HTTP(超文本传输协议):基于TCP协议,用于传输超文本多媒体内容,主要应用于浏览器和服务器间的通信。

SMTP(简单邮件发送协议):基于TCP,用于发送电子邮件

POP3(邮件接收协议)

FTP(文件传输协议):基于TCP. 不安全,不会在传输时加密数据,SFTP更安全

TeInet(远程登录协议):基于TCP,通过一个终端登录到其他服务器。 所有数据以明文发送,不安全。建议用SSH

SSH(安全的网络传输协议):基于TCP,通过加密认证机制实现安全的远程登录。默认端口是 22。SSH 是一个守护进程,负责实时监听客户端请求,并进行处理

RTP(实时传输协议):基于UDP,也可以支持TCP。 用于端到端的实时数据传输

DNS(域名管理系统):基于UDP,用于解决域名和IP的映射问题。

传输层

负责为不同中断的进程间通信提供通用数据传输服务

应用层将报文传给传输层来传输

常见协议:

TCP(Transmisson Control Protocol,传输控制协议 ):提供 面向连接 的,可靠 的数据传输服务。

UDP(User Datagram Protocol,用户数据协议):简单到只负责发送数据包,不保证数据包是否能抵达对方,但它实时性相对更好,传输效率也高

应用需要传输的数据可能会非常大,如果直接传输就不好控制,因此当传输层的数据包大小超过 MSS(TCP 最大报文段长度) ,就要将数据包分块,这样即使中途有一个分块丢失或损坏了,只需要重新发送这一个分块,而不用重新发送整个数据包。在 TCP 协议中,我们把每个分块称为一个 TCP 段TCP Segment)。

img

当设备作为接收方时,传输层则要负责把数据包传给应用,但是一台设备上可能会有很多应用在接收或者传输数据,因此需要用一个编号将应用区分开来,这个编号就是端口

由于传输层的报文中会携带端口号,因此接收方可以识别出该报文是发送给哪个应用。

网络层

传输层可能大家刚接触的时候,会认为它负责将数据从一个设备传输到另一个设备,事实上它并不负责。

实际场景中的网络环节是错综复杂的,中间有各种各样的线路和分叉路口,如果一个设备的数据要传输给另一个设备,就需要在各种各样的路径和节点进行选择,而传输层的设计理念是简单、高效、专注,如果传输层还负责这一块功能就有点违背设计原则了。

传输层不负责实际的数据传输,而是作为应用间通信的媒介,**网络层**负责实际上的数据传输,需要决定传输的路径和节点,将数据从一个设备传输到另一个设备。

img

网络层最常使用的是 IP 协议(Internet Protocol),IP 协议会将传输层的报文作为数据部分,再加上 IP 包头组装成 IP 报文,如果 IP 报文大小超过 MTU(以太网中一般为 1500 字节)就会再次进行分片,得到一个即将发送到网络的 IP 报文。

img

子网掩码

用于将某个IP地址划分成[网络号]和[主机号]两部分。子网掩码是一个32位地址,用于屏蔽IP地址的一部分以区别网络标识和主机标识

子网掩码其实就表示前多少位表示子网的网段

网络号:标识该IP地址属于哪个子网

主机号:标识统一子网下的不同主机

结构分析:

img

将该子网掩码的二进制由24个1和8个0组成,8个0表示该子网掩码划分出的子网容量为256(2的8次方),也就是说192.168.1.0-255都在同一个子网中

当然上面是默认子网掩码,实际情况可以根据子网需要的容量,设置不同的掩码来划分子网

知道了子网掩码,该怎么计算出网络地址和主机地址呢?

将 10.100.122.2 和 255.255.255.0 进行按位与运算,就可以得到网络号,如下图:

img

将 255.255.255.0 取反后与IP地址进行进行按位与运算,就可以得到主机号。

为什么要划分子网

  1. 减少广播域:广播是指在网络中传播信息的过程,广播域是指广播信息可以到达的范围。划分子网可以减少广播域,从而减少网络流量。

  2. 提高网络性能:将网络空间分层划分,相比于扁平的空间结构方便网络寻址(通过IP找到对应主机)

  3. 分层更好管理

寻址过程

1.通过IP和子网掩码拿到网络号,根据网络号找到子网

2.通过IP和子网掩码拿到主机号,根据主机号找到对应设备

IP协议

IP是一种 「不可靠」的 「端到端」的数据包 「传输服务」,主要实现两个功能:数据传输 和 数据分片。

IP协议根据「IP地址」将数据传输到指定的目标主机

为了方便寻址和层次化构造网络,每个IP地址分为网络号和主机号两个部分

路由

IP的数据传输功能除了寻址以外,还有路由功能

实际场景中,两台设备并不是用一条网线连接起来的,而是通过很多网关、路由器、交换机等众多网络设备连接起来的,那么就会形成很多条网络的路径,因此当数据包到达一个网络节点,就需要通过路由算法决定下一步走哪条路径。

路由器寻址工作中,就是要找到目标地址的子网,找到后进而把数据包转发给对应的网络内。

IP地址的网络号

所以,IP 协议的寻址作用是告诉我们去往下一个目的地该朝哪个方向走,路由则是根据「下一个目的地」选择路径。寻址更像在导航,路由更像在操作方向盘

网络接口层

img

以太网就是一种在子网内,把附近的设备连接进行通讯的技术。

在以太网进行通讯要用到 MAC 地址。

MAC 头部是以太网使用的头部,它包含了接收方和发送方的 MAC 地址等信息,我们在网络层会通过 ARP 协议获取对方的 MAC 地址。

所以说,网络接口层主要为网络层提供「链路级别」传输的服务,负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标识网络上的设备。

有了IP地址,为什么还需要MAC地址

有了 IP 地址,为什么还要用 MAC 地址? - 知乎 (zhihu.com)

巨强的讲解,带你了解网络拖布结构和网络传输过程

概要:因为一开始是先有的MAC后有的IP,

MAC地址用于静态标识设备,在小网络内效率更高,IP地址用于动态标识设备,便于动态组网和分发,在大网络中效率更高

如果设备A向设备F发送一个数据包(已知源IP和目标IP),会经历哪些过程?

1.A通过子网掩码计算,A于F是否在同一个子网内

如果在同一子网内,A 先ARP拿到目标MAC,交换机根据MAC选择对应的端口传输到F

如果不再同一子网内,A通过ARP找到默认网关MAC地址,用交换机将数据包转发给默认网关(路由器)

2.路由器查路由表,发给下一跳的端口

3.一直跳直到跳到路由表内有目标IP的路由器,该路由器ARP一下拿到对应的MAC,把mac号封装到数据包里,然后通过路由表IP和端口的映射,将数据包通过端口发送到对应的交换机

4.交换机根据mac地址表匹配到对应目标F的端口,通过端口把数据包发送给F

ps:从始至终这个数据包的两个 IP 地址都是不变的,只有 MAC 地址在不断变(数据链路层头部)

每个设备都会有唯一的IP和MAC

涉及到的三张表分别是

  • 交换机中有 MAC 地址表用于映射 MAC 地址和它的端口

  • 路由器中有路由表用于映射 IP 地址(段)和它的端口

  • 电脑和路由器中都有 arp 缓存表用于缓存 IP 和 MAC 地址的映射关系

这三张表是怎么来的

  • MAC 地址表是通过以太网内各节点之间不断通过交换机通信,不断完善起来的。

  • 路由表是各种路由算法 + 人工配置逐步完善起来的。

  • arp 缓存表是不断通过 arp 协议的请求逐步完善起来的

从浏览器输入网址到网站响应

简单的网络模型

1.浏览器解析URL,确定服务器域名和文件路径,DNS域名解析查询服务器域名对应的IP地址

2.获取到IP后,通过Http协议栈将请求封装成Http报文

2.1 TCP协议栈将HTTP报文切分添加TCP头部传输给IP协议栈

2.2 IP协议在网络包头部加上IP头部

2.3 arp一下在IP头部前加上MAC头部

2.4,将处理后的网络包发给网卡驱动

3.网卡驱动给网络包开头加上报头和起始帧分界符,末尾加上帧校验序列,发给网卡,网卡将网络包转为电信号通过网线发送出去

4.数据包首先到达交换机,交换机根据MAC地址表,找到对应端口将包传过去,如果目的IP属于该交换机所在内网,会传给对应设备,如果属于外网,会传给交换机连接的路由器

5.路由器查询路由表找到下一跳的IP地址,根据目的IP通过ARP获得下一跳的MAC地址,放到包内,通过端口把包转发到下一个路由器

6.层层转发后,到达目标IP的子网路由器,路由器arp获得目的MAC地址,通过端口转发给交换机,交换机通过MAC地址表找到对应的端口,将包传给目标设备的网卡

7.客户端接收数据包后,通过HTTP反向协议栈层层扒开数据包,先对比MAC,再对比IP,再看TCP头,获得端口号,根据端口号找到对应的HTTP进程,HTTP进程收到请求发现是要查询网页,把网页信息封装到HTTP响应报文,再通过HTTP协议栈,层层封装上TCP,IP,MAC头,发给客户端。

DNS域名解析

DNS 服务器专门用来保存域名和IP的映射

域名的层级关系

DNS 中的域名都是用句点来分隔的,比如 www.server.com,这里的句点代表了不同层次之间的界限

在域名中,越靠右的位置表示其层级越高

域名的层级关系类似一个树状结构:

  • 根 DNS 服务器(.)

  • 顶级域 DNS 服务器(.com)

  • 权威 DNS 服务器(server.com)

域名解析过程

1.客户端发起DNS请求,发给本地DNS服务器,询问www.server.com 的 IP 是啥

2.本地DNS收到请求,查看缓存中有无,有的话直接返回,没有就会去询问它的根域名服务器

3.根DNS将请求转给对应的顶级域名服务器,顶级域名服务器转给权威域名服务器

4.权威域名服务器将查询到的IP地址返回给本地DNS,本地DNS再返回给客户端

域名解析的工作流程

协议栈

HTTP 协议栈(HTTP protocol stack)是一组协议层次结构,用于在客户端和服务器之间传输和处理 HTTP 报文

底层原理上,HTTP 协议栈是在操作系统内核中实现的

在传输发送过程中,HTTP 报文会从应用层传输到传输层,然后传输到网络层和数据链路层,最终传输到物理层进行传输。在接收方收到 HTTP 报文后,它会通过反向的协议栈进行解析和处理,并返回相应的响应报文。

img

应用程序(浏览器)通过调用 Socket 库,将HTTP报文请求发到协议栈,委托协议栈工作。协议栈的上半部分有两块,分别是负责收发数据的 TCP 和 UDP 协议(这俩是传输层协议啊,TCP是需要连接,但是连接是为了可靠的数据传输),这两个传输协议会接受应用层的委托执行收发数据的操作。

协议栈的下面一半是用 IP 协议控制网络包收发操作,在互联网上传数据时,数据会被切分成一块块的网络包,而将网络包发送给对方的操作就是由 IP 负责的。

IP 下面的网卡驱动程序负责控制网卡硬件,而最下面的网卡则负责完成实际的收发操作,也就是对网线中的信号执行发送和接收操作。

TCP

TCP包头格式

看看 TCP 报文头部的格式:

TCP 包头格式

首先,源端口号目标端口号是不可少的,如果没有这两个端口号,数据就不知道应该发给哪个应用。

接下来有包的号,这个是为了解决包乱序的问题。

还有应该有的是确认号,目的是确认发出去对方是否有收到。如果没有收到就应该重新发送,直到送达,这个是为了解决丢包的问题。

接下来还有一些状态位。例如 SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。

还有一个重要的就是窗口大小。TCP 要做流量控制,通信双方各声明一个窗口(缓存大小),标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。

除了做流量控制以外,TCP还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。

TCP 传输数据之前,要先三次握手建立连接

三次握手目的是保证双方都有发送和接收的能力

如何查看 TCP 的连接状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

TCP 分割数据

如果 HTTP 请求消息比较长,超过了 MSS 的长度,这时 TCP 就需要把 HTTP 的数据拆解成一块块的数据发送,而不是一次性发送所有数据。

MTU 与 MSS

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节。

  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度。

数据会被以 MSS 的长度为单位进行拆分,拆分出来的每一块数据都会被放进单独的网络包中。也就是在每个被拆分的数据加上 TCP 头信息,然后交给 IP 模块来发送数据。

数据包分割

TCP 报文生成

TCP 协议里面会有两个端口,一个是浏览器监听的端口(通常是随机生成的),一个是 Web 服务器监听的端口(HTTP 默认端口号是 80, HTTPS 默认端口号是 443)。

在双方建立了连接后,TCP 报文中的数据部分就是存放 HTTP 头部 + 数据,组装好 TCP 报文之后,就需交给下面的网络层处理。

至此,网络包的报文如下图。

TCP 层报文

IP

IP 包头格式

我们先看看 IP 报文头部的格式:

IP 包头格式

在 IP 协议里面需要有源地址 IP目标地址 IP

  • 源地址IP,即是客户端输出的 IP 地址;

  • 目标地址,即通过 DNS 域名解析得到的 Web 服务器 IP。

因为 HTTP 是经过 TCP 传输的,所以在 IP 包头的协议号,要填写为 06(十六进制),表示协议为 TCP。

假设客户端有多个网卡,就会有多个 IP 地址,那 IP 头部的源地址应该选择哪个 IP 呢?

查路由表选择网卡

当存在多个网卡时,在填写源地址 IP 时,就需要判断到底应该填写哪个地址。这个判断相当于在多块网卡中判断应该使用哪个一块网卡来发送包。

这个时候就需要根据路由表规则,来判断哪一个网卡作为源地址 IP。

在 Linux 操作系统,我们可以使用 route -n 命令查看当前系统的路由表。

路由表

举个例子,根据上面的路由表,我们假设 Web 服务器的目标地址是 192.168.10.200

路由规则判断

  1. 首先先和第一条目的子网掩码(Genmask)进行 与运算,得到结果为 192.168.10.0,但是第一个条目的 Destination192.168.3.0,两者不一致所以匹配失败。

  2. 再与第二条目的子网掩码进行 与运算,得到的结果为 192.168.10.0,与第二条目的 Destination 192.168.10.0 匹配成功,所以将使用 eth1 网卡的 IP 地址作为 IP 包头的源地址。

那么假设 Web 服务器的目标地址是 10.100.20.100,那么依然依照上面的路由表规则判断,判断后的结果是和第三条目匹配。

第三条目比较特殊,它目标地址和子网掩码都是 0.0.0.0,这表示默认网关,如果其他所有条目都无法匹配,就会自动匹配这一行。并且后续就把包发给路由器,Gateway 即是路由器的 IP 地址。

MAC

生成了 IP 头部之后,接下来网络包还需要在 IP 头部的前面加上 MAC 头部

在 MAC 包头里需要发送方 MAC 地址和接收方目标 MAC 地址,用于在以太网中确定传输的两点。

如何获得发送/接收方MAC?

发送方的MAC由网卡生产时写入的,是网卡的唯一标识

接收方的MAC需要先通过IP寻址找到对应的子网,在子网中通过ARP协议找到对应的MAC

但是我们一开始是不可能拿到目标设备的MAC的,不在一个子网中,所以MAC是实时更新的,我们只要根据目标IP每次找到下一跳的路由器MAC,在这个寻址的过程中渐渐逼近目标IP所在子网的路由器,再arp找到设备。 这个过程我们每一跳都要更新网络包的MAC包头,通过arp获得下一跳的MAC

ARP

ARP协议(地址解析协议)是根据IP获取MAC地址的一个*协议

在 TCP/IP 模式中 ARP 协议属于网络层,因为ARP协议属于TCP/IP协议簇

在 OSI 参考模型中 ARP 协议属于数据链路层,因为按照 OSI 参考模型,数据链路层封装 IP 报文时,需要通过 ARP 获取链路层目的地址,添加到报文头部

机制

ARP 协议会在以太网中以广播的形式,对以太网所有设备通信询问这个IP是哪个设备的,匹配上的设备会回复自己的MAC地址

如果对方和自己处于同一个子网中,那么通过上面的操作就可以得到对方的 MAC 地址。然后,我们将这个 MAC 地址写入 MAC 头部,MAC 头部就完成了。

在发包时:

  • 先查询 ARP 缓存,如果其中已经保存了对方的 MAC 地址,就不需要发送 ARP 查询,直接使用 ARP 缓存中的地址。

  • 而当 ARP 缓存中不存在对方 MAC 地址时,则发送 ARP 广播查询。

出口 — 网卡

网卡驱动获取网络包之后,会将其复制到网卡内的缓存区中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列; 然后网卡会将网卡驱动包装的包转为电信号通过网线发送出去

数据包

  • 起始帧分界符是一个用来表示包起始位置的标记

  • 末尾的 FCS(帧校验序列)用来检查包传输过程是否有损坏

交换机

交换机根据 MAC 地址表查找 MAC 地址,然后将网络包发送到相应的端口

交换机接收传来的网络包,先校验包末尾的 FCS,如果没问题则放到缓冲区。

然后查询一下这个包的接收方 MAC 地址是否已经在 MAC 地址表中有记录了,有记录的话就转发网络包到对应端口

交换机的 MAC 地址表主要包含两个信息:

  • 一个是设备的 MAC 地址,

  • 另一个是该设备连接在交换机的哪个端口上。

交换机的 MAC 地址表

当 MAC 地址表找不到指定的 MAC 地址会怎么样?

这种情况下,交换机无法判断应该把包转发到哪个端口,只能将包转发到所有端口上,无论该设备连接在哪个端口上都能收到这个包。然后只有相应的接收者才接收包返回响应包给交换机,交换机将它的地址写入MAC地址表以便下次寻址

路由器

网络包经过交换机之后,现在到达了路由器,并在此被转发到下一个路由器或目标设备。(离开原来的子网前往下一个子网)

路由器和交换机是有区别的。

  • 因为路由器是基于 IP 设计的,俗称三层网络设备,路由器的各个端口都具有 MAC 地址和 IP 地址;

  • 交换机是基于以太网设计的,俗称二层网络设备,交换机的端口不具有 MAC 地址。

路由器工作原理

路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方;同时还具有 IP 地址,从这个意义上来说,它和计算机的网卡是一样的。

1.当转发包时,首先路由器端口会接收到目标MAC是自己MAC地址(确认是发给自己)的包,

2.去掉包开头的 MAC 头部。MAC 头部的作用就是将包送达下一跳地址,每送达一次就得删掉再更新为下一跳的MAC

3.路由器查询路由表判断转发目标

3.1进行路由匹配,根据包的目的IP查询路由表,找到相匹配的记录,如果匹配不到选择默认路由

3.2 根据路由表项的网关列 判断对方的地址,如果是一个IP,则说明未到达终点需要继续转发,如果为空,说明已到达重点

3.2 知道了下一跳的IP地址后,要通过 ARP 协议根据 IP 地址查询 MAC 地址,并将包的目的MAC更新

4.将网络包转换成电信号通过端口发出去

发送出去的网络包会通过交换机到达下一个路由器。经过层层转发,网络包到达了最终的目的地。

ps:【在网络包传输的过程中,源 IP 和目标 IP 始终是不会变的,一直变化的是 MAC 地址,因为需要 MAC 地址在以太网内进行两个设备之间的包传输。

互相扒皮 —— 服务器 与 客户端

网络分层模型

数据包抵达服务器后,服务器会先扒开数据包的 MAC 头部,查看是否和服务器自己的 MAC 地址符合,符合就将包收起来。

接着继续扒开数据包的 IP 头,发现 IP 地址符合,根据 IP 头中协议项,知道自己上层是 TCP 协议。

于是,扒开 TCP 的头,里面有序列号,需要看一看这个序列包是不是我想要的,如果是就放入缓存中然后返回一个 ACK,如果不是就丢弃。TCP头部里面还有端口号, HTTP 的服务器正在监听这个端口号。

于是,服务器自然就知道是 HTTP 进程想要这个包,于是就将包发给 HTTP 进程。

服务器的 HTTP 进程看到,原来这个请求是要访问一个页面,于是就把这个网页封装在 HTTP 响应报文里。

HTTP 响应报文也需要穿上 TCP、IP、MAC 头部,不过这次是源地址是服务器 IP 地址,目的地址是客户端 IP 地址。

穿好头部衣服后,从网卡出去,交由交换机转发到出城的路由器,路由器就把响应数据包发到了下一个路由器,就这样跳啊跳。

最后跳到了客户端的城门把守的路由器,路由器扒开 IP 头部发现是要找城内的人,于是又把包发给了城内的交换机,再由交换机转发到客户端。

读者问:“请问公网服务器的 Mac 地址是在什么时机通过什么方式获取到的?我看 arp 获取Mac地址只能获取到内网机器的 Mac 地址吧?”

在发送数据包时,如果目标主机不是本地局域网,填入的MAC地址是路由器,也就是把数据包转发给路由器,路由器一直转发下一个路由器,直到转发到目标主机的路由器,发现 IP 地址是自己局域网内的主机,就会 arp 请求获取目标主机的 MAC 地址,从而转发到这个服务器主机。

转发的过程中,源IP地址和目标IP地址是不会变的(前提:没有使用 NAT 网络的),源 MAC 地址和目标 MAC 地址是会变化的。

网络模型

为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。

每一层负责的职能都不同,如下:

  • 应用层,负责给应用程序提供统一的接口;

  • 表示层,负责把数据转换成兼容另一个系统能识别的格式;

  • 会话层,负责建立、管理和终止表示层实体之间的通信会话;

  • 传输层,负责端到端的数据传输;

  • 网络层,负责数据的路由、转发、分片;

  • 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;

  • 物理层,负责在物理网络中传输数据帧;

由于 OSI 模型实在太复杂,提出的也只是概念理论上的分层,并没有提供具体的实现方案。

事实上,我们比较常见,也比较实用的是四层模型,即 TCP/IP 网络模型,Linux 系统正是按照这套网络模型来实现网络协议栈的。

TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层,每一层负责的职能如下:

  • 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;

  • 传输层,负责端到端的通信,比如 TCP、UDP 等;

  • 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;

  • 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;

TCP/IP 网络模型相比 OSI 网络模型简化了不少,也更加易记,它们之间的关系如下图:

img

不过,我们常说的七层和四层负载均衡,是用 OSI 网络模型来描述的,七层对应的是应用层,四层对应的是传输层。

Linux网络协议栈

img

从上图的的网络协议栈,你可以看到:

  • 应用程序需要通过系统调用,来跟 Socket 层进行数据交互

  • Socket 层的下面就是传输层、网络层和网络接口层;

  • 最下面的一层,则是网卡驱动程序和硬件网卡设备;

Linux接收网络包的流程

当有网络包到达时,会通过DMA技术,让网卡直接将网络包写入指定的内存地址,然后网卡向CPU发起硬件中断,cpu接收硬件中断请求后,调用中断处理函数,先屏蔽中断,再发起软中断处理耗时工作,然后恢复屏蔽的中断;

硬件中断处理函数会做如下的事情:

  • 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。

  • 接着,发起「软中断」,然后恢复刚才屏蔽的中断。

硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了。

为什么需要屏蔽中断

在数据包来的特别频繁时,那么就会触发非常多的中断,会导致 CPU 一直没完没了的处理中断,其他任务可能无法继续前进,从而影响系统的整体效率。

为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll 的方法来轮询数据。

网络协议栈

首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法则丢弃,合法则会找出该网络包的上层协议的类型,比如是 IPv4,还是 IPv6,接着再去掉帧头和帧尾,然后交给网络层。

到了网络层,则取出 IP 包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。

传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识,找出对应的 Socket,并把数据放到 Socket 的接收缓冲区。

最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。

至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。

img

Linux 发送网络包的流程

如上图的右半部分,发送网络包的流程正好和接收流程相反。

首先,应用程序会系统调用 Socket 发送数据包的接口,申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。(应用层)

接下来,网络协议栈从 Socket 发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。

传输层:对 sk_buff 填充 TCP 头。

网络层:选取路由(确认下一跳的 IP)、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片,通过ARP协议获得下一跳的MAC地址。处理完这些工作后会交给网络接口层处理。

网络接口层: 对 sk_buff 填充帧头和帧尾,接着将 sk_buff 放到网卡的发送队列中。

触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。

当数据发送完成以后,其实工作并没有结束,因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。

最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff 。

发送网络数据的时候,涉及几次内存拷贝操作?

第一次,调用发送数据的系统调用的时候,内核会申请一个内核态的 sk_buff 内存,将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区。

第二次,在使用 TCP 传输协议的情况下,从传输层进入网络层的时候,每一个 sk_buff 都会被克隆一个新的副本出来。副本 sk_buff 会被送往网络层,等它发送完的时候就会释放掉,然后原始的 sk_buff 还保留在传输层,目的是为了实现 TCP 的可靠传输,等收到这个数据包的 ACK 时,才会释放原始的 sk_buff 。

第三次,当 IP 层发现 sk_buff 大于 MTU 时才需要进行。会再申请额外的 sk_buff,并将原来的 sk_buff 拷贝为多个小的 sk_buff。

为什么全部数据包只用一个结构体来描述呢

这里提一下,sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。

你可能会好奇,为什么全部数据包只用一个结构体来描述呢?协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率。

于是,为了在层级之间传递数据时,不发生拷贝,只用 sk_buff 一个结构体来描述所有的网络包,那它是如何做到的呢?是通过调整 sk_buff 中 data 的指针,比如:

  • 当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部。

  • 当要发送报文时,创建 sk_buff 结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值来增加协议首部。

你可以从下面这张图看到,当发送报文时,data 指针的移动过程。

img

HTTP篇

提纲

HTTP 基本概念

HTTP 是什么?

HTTP 是超文本传输协议,也就是HyperText Transfer Protocol。

是一个在专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

HTTP 常见的状态码有哪些?

2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。

  • 200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。

  • 204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。

  • 206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。

3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向

  • 301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。

  • 302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。

301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。

  • 304 Not Modified」不缓存重定向,也就是告诉客户端可以继续使用缓存资源。

4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。

  • 400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。

  • 403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。

  • 404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。

5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。

  • 500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。

  • 501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。

  • 502 Bad Gateway」服务器连接超时,可能是由于服务器连接太多,无法正常响应

  • 503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思

HTTP 常见字段有哪些?

Host 字段

客户端发送请求时,用来指定服务器的域名。Host: www.A.com

Content-Length 字段

服务器在返回数据时,会有 Content-Length 字段,表明本次回应的数据长度。

//大家应该都知道 HTTP 是基于 TCP 传输协议进行通信的,而使用了 TCP 传输协议,就会存在一个“粘包”的问题,HTTP 协议通过设置回车符、换行符作为 HTTP header 的边界,通过 Content-Length 字段作为 HTTP body 的边界,这两个方式都是为了解决“粘包”的问题

Connection 字段

Connection 字段最常用于客户端要求服务器使用「HTTP 长连接」机制,以便其他请求复用。

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

HTTP 长连接

HTTP/1.1 版本的默认连接都是长连接,但为了兼容老版本的 HTTP,需要指定 Connection 首部字段的值为 Keep-Alive

Connection: Keep-Alive

开启了 HTTP Keep-Alive 机制后, 连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接,一直持续到客户端或服务器端提出断开连接。

Content-Type 字段

Content-Type 字段用于服务器回应时,告诉客户端,本次数据是什么格式。

Content-Type: text/html; Charset=utf-8

上面的类型表明,发送的是网页,而且编码是UTF-8。

客户端请求的时候,可以使用 Accept 字段声明自己可以接受哪些数据格式。

Accept: */*

上面代码中,客户端声明自己可以接受任何格式的数据。

Content-Encoding 字段

Content-Encoding 字段说明数据的压缩方法。表示服务器返回的数据使用了什么压缩格式

Content-Encoding: gzip

上面表示服务器返回的数据采用了 gzip 方式压缩,告知客户端需要用此方式解压。

客户端在请求时,用 Accept-Encoding 字段说明自己可以接受哪些压缩方法。

Accept-Encoding: gzip, deflate

HTTP报文结构

请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

img

  • 请求行(Request Line):

    • 方法:如 GET、POST、PUT、DELETE等,指定要执行的操作。

    • 请求 URI(统一资源标识符):请求的资源路径,通常包括主机名、端口号(如果非默认)、路径和查询字符串。

    • HTTP 版本:如 HTTP/1.1 或 HTTP/2。

    请求行的格式示例:GET /index.html HTTP/1.1

  • 请求头(Request Headers):

    • 包含了客户端环境信息、请求体的大小(如果有)、客户端支持的压缩类型等。

    • 常见的请求头包括HostUser-AgentAcceptAccept-EncodingContent-Length等。

  • 空行

    • 请求头和请求体之间的分隔符,表示请求头的结束。

  • 请求体(可选):

    • 在某些类型的HTTP请求(如 POST 和 PUT)中,请求体包含要发送给服务器的数据。

GET 与 POST

GET 和 POST 有什么区别?

GET 的语义是从服务器获取指定的资源,GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制

get是只读的操作

POST 的语义是根据(报文body)对指定的资源做出处理。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,而且浏览器不会对 body 大小做限制。

post是新增或提交数据的操作

GET 和 POST 方法都是安全和幂等的吗?

先说明下安全和幂等的概念:

  • 在 HTTP 协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。

  • 所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。

如果从 RFC 规范定义的语义来看:

  • GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。、

  • POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等

但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:

  • 可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。

  • 可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。

不能说get传数据相比post传数据更容易泄露:

HTTP传输内容是明文的,抓个包就能看到body里的数据

HTTP缓存技术

对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,那么下次就直接读取本地的数据,不必在通过网络获取服务器的响应了

避免发送 HTTP 请求的方法就是通过缓存技术

HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

什么是强制缓存?

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

  • Cache-Control, 是一个相对时间;

  • Expires,是一个绝对时间;

如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control 的优先级高于 Expires

Cache-Control 来实现强缓存。具体的实现流程如下:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 Cache-Control,Cache-Control 中设置了过期时间大小;

  • 浏览器再次请求访问服务器中的该资源时,会先通过请求资源的时间与 Cache-Control 中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器;

  • 服务器再次收到请求后,会再次更新 Response 头部的 Cache-Control。

什么是协商缓存?

当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

img

上图就是一个协商缓存的过程,所以协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存

协商缓存可以基于两种头部来实现。

第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

  • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;

  • 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

  • 响应头部中 Etag:唯一标识响应资源;

  • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

第一种实现方式是基于时间实现的,第二种实现方式是基于一个唯一标识实现的,相对来说后者可以更加准确地判断文件内容是否被修改,避免由于时间篡改导致的不可靠问题。

当使用 ETag 字段实现的协商缓存的过程:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;

  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:

    • 如果没有过期,则直接使用本地缓存;

    • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;

  • 服务器再次收到请求后,

    会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较

    • 如果值相等,则返回 304 Not Modified,不会返回资源

    • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;

  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

HTTP特性

HTTP/1.1 的优点有哪些?

「简单、灵活和易于扩展、应用广泛和跨平台」。

1. 简单

HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。

2. 灵活和易于扩展

HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充

同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化,比如:

  • HTTPS 就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层;

  • HTTP/1.1 和 HTTP/2.0 传输协议使用的是 TCP 协议,而到了 HTTP/3.0 传输协议改用了 UDP 协议。

3. 应用广泛和跨平台

互联网发展至今,HTTP 的应用范围非常的广泛,同时天然跨平台,任何开发语言都可以实现基于HTTP进行网络通信

HTTP/1.1 的缺点有哪些?

「无状态、明文传输」

1. 无状态双刃剑

无状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

无状态的坏处,既然服务器没有记忆能力,它在完成有关联性的操作时会麻烦。

2. 明文传输双刃剑

明文意味着在传输过程中的信息,是可方便阅读的,比如 Wireshark 抓包都可以直接肉眼查看,为我们调试工作带了极大的便利性。

但是这正是这样,HTTP 的所有信息都暴露在了光天化日下,相当于信息裸奔

3. 不安全

HTTP 比较严重的缺点就是不安全:

  • 通信使用明文(不加密),内容可能会被窃听。比如,账号信息容易泄漏,那你号没了。

  • 不验证通信方的身份,因此有可能遭遇伪装。比如,访问假的淘宝、拼多多,那你钱没了。

  • 无法证明报文的完整性,所以有可能已遭篡改。比如,网页上植入垃圾广告,视觉污染

HTTP 的安全问题,可以用 HTTPS 的方式解决,也就是通过引入 SSL/TLS 层,使得在安全上达到了极致。

HTTP/1.1 的性能如何?

HTTP 协议是基于 TCP/IP,并且使用了「请求 - 应答」的通信模式,所以性能的关键就在这两点里。

1. 长连接

早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了通信开销。

为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销

持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

当然,如果某个 HTTP 长连接超过一定时间没有任何数据交互,服务端就会主动断开这个连接。

短连接与长连接

2. 管道网络传输

HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。

即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

管道网络传输

但是服务器必须按照 接收请求的顺序 响应这些管道化请求

如果服务端在处理 A 请求时耗时比较长,那么后续的请求的处理都会被阻塞住,这称为「队头堵塞」。

所以,HTTP/1.1 管道解决了请求的队头阻塞,但是没有解决响应的队头阻塞

ps:管道化实际上没有开启

TIP

注意!!!

实际上 HTTP/1.1 管道化技术不是默认开启,而且浏览器基本都没有支持,所以后面所有文章讨论 HTTP/1.1 都是建立在没有使用管道化的前提。大家知道有这个功能,但是没有被使用就行了。

3. 队头阻塞

「请求 - 应答」的模式会造成 HTTP 的性能问题。为什么呢?

因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」,好比上班的路上塞车。

由于HTTP1.1管道化技术默认不开启且浏览器不支持,所以HTTP1.1还是有队头阻塞问题

队头阻塞

HTTP与HTTPS

HTTP 与 HTTPS 有哪些区别?

  • HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。

  • HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。

  • 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。

  • HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。

HTTPS 解决了 HTTP 的哪些问题?

HTTP 由于是明文传输,所以安全上存在以下三个风险:

  • 窃听风险,比如通信链路上可以获取通信内容,用户号容易没。

  • 篡改风险,比如强制植入垃圾广告,视觉污染,用户眼容易瞎。

  • 冒充风险,比如冒充淘宝网站,用户钱容易没。

HTTP 与 HTTPS 网络层

HTTPS 在 HTTP 与 TCP 层之间加入了 SSL/TLS 协议,可以很好的解决了上述的风险:

  • 信息加密:交互信息无法被窃取

  • 校验机制:无法篡改通信内容,篡改了就不能正常显示

  • 身份证书:通过安全证书证明淘宝是真的淘宝网

HTTPS 是如何解决上面的三个风险的?

  • 混合加密的方式实现信息的机密性,解决了窃听的风险。

  • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。

  • 将服务器公钥放入到数字证书中,解决了冒充的风险。

混合加密

非对称加密:使用两把密钥,一把叫做私有密钥,用于解密,另一把叫做公开密钥,用于加密。私有密钥不能让其他任何人知道,而公开密钥则可以随意发布,任何人都可以获得,都可以用公钥加密,而解密只有私钥持有者能做

非对称加密的问题:无法证明公开密钥就是接收方公开的那个密钥,而不是被攻击者调包的密钥,如果攻击者给用户一个假的公钥,用户根据假的公钥发请求被攻击者拦截,用假公钥对应的私钥解密,用户信息就泄露了

解决:数字证书认证机构(CA)颁发证书,保证公钥的来源可靠

混合加密 在通信建⽴前采⽤⾮对称加密的⽅式交换「会话秘钥」,后续就不再使⽤⾮对称加密。在通信过程中全部使⽤对称加密的「会话秘钥」的⽅式加密明⽂数据

HTTP通过混合加密的方式可以保证信息的机密性,同时保证了性能。

对称加密性能好,但密钥必须保密,不够安全

非对称加密安全,但是性能差

摘要算法 + 数字签名

用于校验数据完整性(用于验证证书可靠性)

为了保证传输的内容不被篡改,我们需要对内容计算出一个「指纹」,然后同内容一起传输给对方。

对方收到后,先是对内容也计算出一个「指纹」,然后跟发送方发送的「指纹」做一个比较,如果「指纹」相同,说明内容没有被篡改,否则就可以判断出内容被篡改了。

那么,在计算机里会用摘要算法(哈希函数)来计算出内容的哈希值,也就是内容的「指纹」,这个哈希值是唯一的,且无法通过哈希值推导出内容

img

通过哈希算法可以确保内容不会被篡改,但是并不能保证「内容 + 哈希值」不会被中间人替换,因为这里缺少对客户端收到的消息是否来源于服务端的证明

那为了避免这种情况,计算机里会用非对称加密算法来解决,共有两个密钥:

  • 一个是公钥,这个是可以公开给所有人的;

  • 一个是私钥,这个必须由本人管理,不可泄露。

这两个密钥可以双向加解密的,比如可以用公钥加密内容,然后用私钥解密,也可以用私钥加密内容,公钥解密内容。

流程的不同,意味着目的也不相同:

  • 公钥加密,私钥解密。这个目的是为了保证内容传输的安全,因为被公钥加密的内容,其他人是无法解密的,只有持有私钥的人,才能解密出实际的内容;

  • 私钥加密,公钥解密。这个目的是为了保证消息不会被冒充,因为私钥是不可泄露的,如果公钥能正常解密出私钥加密的内容,就能证明这个消息是来源于持有私钥身份的人发送的。

一般我们不会用非对称加密来加密实际的传输内容,因为非对称加密的计算比较耗费性能的。

所以非对称加密的用途主要在于通过「私钥加密,公钥解密」的方式,来确认消息的身份,我们常说的数字签名算法,就是用的是这种方式,不过私钥加密内容不是内容本身,而是对内容的哈希值加密

img

私钥是由服务端保管,然后服务端会向客户端颁发对应的公钥。如果客户端收到的哈希值密文,能被公钥解密,就说明该消息是由服务器发送的。

img

摘要算法用来校验证书可靠性

数字证书

通过一个权威机构认证服务器身份,防止伪造公私钥冒充窃取用户信息

HTTPS 如何建立连接?

TODO:待完善

SSL/TLS 协议基本流程:

  • 客户端向服务器索要并验证服务器的公钥。

  • 双方协商生产「会话秘钥」。

  • 双方采用「会话秘钥」进行加密通信。

前两步也就是 SSL/TLS 的建立过程,也就是 TLS 握手阶段。

TLS 的「握手阶段」涉及四次通信,使用不同的密钥交换算法,TLS 握手流程也会不一样的,现在常用的密钥交换算法有两种:RSA 算法 (opens new window)ECDHE 算法 (opens new window)

HTTPS 连接建立过程

1. ClientHello

首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。发送客户端随机数

2. SeverHello

服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello 发送服务端随机数和服务端证书(内有都五端的公钥)

3.客户端回应

客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性

如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送信息

服务器和客户端有了三个随机数后,用双方协商的加密算法,各自生成本次通信的「会话秘钥」

4. 服务器的最后回应

向客户端发送最后的信息:握手结束通知,并告诉客户端一会都用会话密钥加密通信

就是要验证数字证书,同时使用非对称加密保证颁发会话密钥过程的安全性

至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。

HTTPS 的应用数据是如何保证完整性的?

TLS 在实现上分为握手协议记录协议两层:

  • TLS 握手协议就是我们前面说的 TLS 四次握手的过程,负责协商加密算法和生成对称密钥,后续用此密钥来保护应用程序数据(即 HTTP 数据);

  • TLS 记录协议负责保护应用程序数据并验证其完整性和来源,所以对 HTTP 数据加密是使用记录协议;

TLS 记录协议主要负责消息(HTTP 数据)的压缩,加密及数据的认证,过程如下图:

img

具体过程如下:

  • 首先,消息被分割成多个较短的片段,然后分别对每个片段进行压缩。

  • 接下来,经过压缩的片段会被加上消息认证码(MAC 值,这个是通过哈希算法生成的),这是为了保证完整性,并进行数据的认证。通过附加消息认证码的 MAC 值,可以识别出篡改。与此同时,为了防止重放攻击,在计算消息认证码时,还加上了片段的编码。

  • 再接下来,经过压缩的片段再加上消息认证码会一起通过对称密码进行加密。

  • 最后,上述经过加密的数据再加上由数据类型、版本号、压缩后的长度组成的报头就是最终的报文数据。

记录协议完成后,最终的报文数据将传递到传输控制协议 (TCP) 层进行传输。

HTTP/1.1、HTTP/2、HTTP/3 演变

HTTP/1.1 相比 HTTP/1.0 提高了什么性能?

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。

  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

但 HTTP/1.1 还是有性能瓶颈:

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;

  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;

  • 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;HTTP1.1只解决了请求的队头阻塞,没有解决响应的队头阻塞

  • 没有请求优先级控制;

  • 请求只能从客户端开始,服务器只能被动响应。

HTTP/2 做了什么优化?

HTTP/2 协议是基于 HTTPS 的,所以 HTTP/2 的安全性也是有保障的。

HTT/1 ~ HTTP/2

那 HTTP/2 相比 HTTP/1.1 性能上的改进:

  • 头部压缩

  • 二进制格式

  • 并发传输

  • 服务器主动推送资源

1. 头部压缩

HTTP/2 会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分

这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

2. 二进制格式

HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)

HTTP/1 与 HTTP/2

这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率

3. 并发传输

我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

而 HTTP/2 就很牛逼了,引出了 Stream 概念,一个TCP连接可以有多个stream传消息,到服务端按照streamID组装起来,我们只需要保证stream内有序就行,不同stream可以并发乱序发送消息

img

从上图可以看到,1 个 TCP 连接包含多个 Stream,Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成。Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体)。

针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应

比如下图,服务端并行交错地发送了两个响应: Stream 1 和 Stream 3,这两个 Stream 都是跑在一个 TCP 连接上,客户端收到后,会根据相同的 Stream ID 有序组装成 HTTP 消息。

img

4、服务器推送

HTTP/2 还在一定程度上改善了传统的「请求 - 应答」工作模式,服务端不再是被动地响应,可以主动向客户端发送消息。

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

客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:

img

如上图右边部分,在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。

HTTP/2 有什么缺陷?

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

img

举个例子,如下图:

img

图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 packet 3 在网络中丢失了,即使 packet 4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 packet 3 重传后,接收方的应用层才可以从内核中读取到数据,这就是 HTTP/2 的队头阻塞问题,是在 TCP 层面发生的。

所以,一旦发生了丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的所有的 HTTP 请求都必须等待这个丢了的包被重传回来

HTTP/3 做了哪些优化?

前面我们知道了 HTTP/1.1 和 HTTP/2 都有队头阻塞的问题:

  • HTTP/1.1 中的管道( pipeline)虽然解决了请求的队头阻塞,但是没有解决响应的队头阻塞,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后, 才能处理下一个请求,这属于 HTTP 层队头阻塞。

  • HTTP/2 虽然通过多个请求复用一个 TCP 连接解决了 HTTP 的队头阻塞 ,但是一旦发生丢包,就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。

HTTP/2 队头阻塞的问题是因为 TCP,所以 HTTP/3 把 HTTP 下层的 TCP 协议改成了 UDP!

HTTP/1 ~ HTTP/3

UDP 发送是不管顺序,也不管丢包的,所以不会出现像 HTTP/2 队头阻塞的问题。大家都知道 UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输。

QUIC 有以下 3 个特点。

  • 无队头阻塞

  • 更快的连接建立

  • 连接迁移

1、无队头阻塞

QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。

QUIC 有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。

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

img

2、更快的连接建立

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。

HTTP/3 在传输数据前虽然需要 QUIC 协议握手,但是这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。

但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是 QUIC 内部包含了 TLS(复用了握手过程),它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,如下图:

TCP HTTPS(TLS/1.3) 和 QUIC HTTPS

3、连接迁移

基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。

TCP 四元组

那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接。而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

读者问:“ 我看文中 TLS 和 SSL 没有做区分,这两个需要区分吗?”

这两实际上是一个东西。

SSL 是洋文 “Secure Sockets Layer” 的缩写,中文叫做「安全套接层」。它是在上世纪 90 年代中期,由网景公司设计的。

到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS(是 “Transport Layer Security” 的缩写),中文叫做 「传输层安全协议」。

很多相关的文章都把这两者并列称呼(SSL/TLS),因为这两者可以视作同一个东西的不同阶段。

TCP篇

img

TCP基本认识

TCP 头格式有哪些?

我们先来看看 TCP 头的格式,标注颜色的表示与本文关联比较大的字段,其他字段不做详细阐述。

TCP 头格式

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1

  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。

  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。

  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

为什么需要 TCP 协议? TCP 工作在哪一层?

IP 层是「不可靠」的,不解决网络包的丢包,乱序问题

OSI 参考模型与 TCP/IP 的关系

如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

什么是 TCP ?

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

img

  • 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

  • 可靠的:TCP可以保证一个HTTP报文按序完整到达接收端

  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃

什么是 TCP 连接?

简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括 Socket、序列号和窗口大小称为连接。

img

所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端达成上述三个信息的共识。

  • Socket:由 IP 地址和端口号组成

  • 序列号:用来解决乱序问题等

  • 确认序列号:确认没丢包

  • 窗口大小:用来做流量控制

如何唯一确定一个 TCP 连接呢?

TCP 四元组可以唯一的确定一个连接,四元组包括如下:

  • 源地址

  • 源端口

  • 目的地址

  • 目的端口

TCP 四元组

源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。

源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程

UDP 和 TCP 有什么区别呢?分别的应用场景是?

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。

UDP 协议真的非常简,头部只有 8 个字节(64 位),UDP 的头部格式如下:

UDP 头部格式

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。

  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。

  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP 包。

TCP 和 UDP 区别:

1. 连接

  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。

  • UDP 是不需要连接,即刻传输数据。

2. 服务对象

  • TCP 是一对一的两点服务,即一条连接只有两个端点。

  • UDP 支持一对一、一对多、多对多的交互通信

3. 可靠性

  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。

  • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议,具体可以参见这篇文章:如何基于 UDP 协议实现可靠传输?(opens new window)

4. 拥塞控制、流量控制

  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。

  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

5. 首部开销

  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。

  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

6. 传输方式

  • TCP 是流式传输,没有边界,但保证顺序和可靠。

  • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

7. 分片不同

  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。

  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

TCP 和 UDP 应用场景:

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;

  • HTTP / HTTPS;

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNSSNMP 等;

  • 视频、音频等多媒体通信;

TCP 和 UDP 可以使用同一个端口吗?

答案:可以的

传输层的「端口号」的作用,是为了区分同一个主机上不同应用程序的数据包。

传输层有两个传输协议分别是 TCP 和 UDP,在内核中是两个完全独立的软件模块。

因此,TCP/UDP 各自的端口号也相互独立,如 TCP 有一个 80 号端口,UDP 也可以有一个 80 号端口,二者并不冲突。

TCP 连接建立

#TCP 三次握手过程是怎样的?

TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。三次握手的过程如下图:

TCP 三次握手

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态

第一个报文 —— SYN 报文

  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。

第二个报文 —— SYN + ACK 报文

  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。

第三个报文 —— ACK 报文

  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。

  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。

一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

1.服务端listen监听端口

2.客户端发起第一次握手,将初始序列号放入报文,向服务端发送SYN报文

3.服务端收到后,将自己的初始SYN序列号放到此次报文的序列号部分,和客户端发来的SYN序列号加1放到此次报文的确认序列号中,返回给客户端SYN和ACK(第二次握手)

4.客户端收到SYN和ACK之后,将服务端的序列号+1放到此次报文的确认序列号中,返回给服务端(第三次握手),然后进入ESTABLISHED状态

5.服务端收到ACK后,也进入established状态,三次握手结束

为什么是三次握手?

“因为三次握手才能保证双方具有接收和发送的能力后再建立链接。”“:

第一次握手服务端确定对方发送正常,自己接收正常,

第二次握手客户端确定自己发送正常,自己接收正常,对方发送正常,对方接收正常

第三次握手服务端确定对方接收正常,自己发送正常

这回答是没问题,但这回答是片面的,并没有说出主要的原因。

接下来,以三个方面分析三次握手的原因:

  • 阻止重复历史连接的初始化(主要原因)

  • 同步双方的初始序列号

  • 保证双方具有接收和发送的能力

原因一:避免历史连接

我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

三次握手避免历史连接

看看三次握手是如何阻止历史连接的

客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。

  • 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。

  • 服务端收到 RST 报文后,就会释放连接。

  • 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。

上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接

如果是两次握手连接,就无法阻止历史连接,为什么

如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接

如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效历史链接,造成不必要的资源浪费。这些历史连接是可以发送数据的,只有在收到 RST 报文后,才会断开连接。(白白分配资源创建了连接,发送了数据,造成不必要的浪费)

要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。

所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。

两次握手无法阻止历史连接

原因二:同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个「序列号」

序列号是可靠传输的一个关键因素,能够保证数据包不重复、不丢弃和按序传输。

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

四次握手与三次握手

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。

而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。。

小结

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?

主要原因有两个方面:

  • 为了防止历史报文被下一个相同四元组的连接接收(主要方面);

  • 为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

不同的序列号可以一定程度上避免历史报文被下一个相同四元组的连接接收。

接下来,详细说说第一点。

假设每次建立连接,客户端和服务端的初始化序列号都是从 0 开始:

img

过程如下:

  • 客户端和服务端建立一个 TCP 连接,在客户端发送数据包被网络阻塞了,然后超时重传了这个数据包,而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文。

  • 紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接;

  • 在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱。

可以看到,如果每次建立连接,客户端和服务端的初始化序列号都是一样的话,很容易出现历史报文被下一个相同四元组的连接接收的问题

每次初始化序列号不一样很大程度上能够避免历史报文被下一个相同四元组的连接接收,注意是很大程度上,并不是完全避免了(因为序列号会有回绕的问题,所以需要用时间戳的机制来判断历史报文,

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

我们先来认识下 MTU 和 MSS

MTU 与 MSS

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节;

  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;

如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?

当 IP 层有一个超过 MTU 大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 传输层。

这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传

因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。

当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」。

因此,可以得知由 IP 层进行分片传输,是非常没有效率的。

所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。

握手阶段协商 MSS

经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率

第一次握手丢失了,会发生什么?

1.客户端向服务器发送SYN报文第一次握手,等待服务端的SYN-ACK报文

2.等待超时,客户端超时重传SYN报文

3.重试超过次数,客户端不再等待,断开连接

第二次握手丢失了,会发生什么?

1.客户端向服务器发送SYN报文,等待服务端的SYN-ACK报文

2.服务端发送SYN-ACK报文给客户端,但却由于网络阻塞迟迟无法送达。服务端等待第三次握手

3.客户端等待超时,重传SYN报文。 服务端等待第三次握手超时,重传SYN-ACK报文

4.重试超过次数后,客户端断开连接。 重试超过次数,服务端断开连接

第三次握手丢失了,会发生什么?

1.客户端向服务端发送ACK报文第三次握手,由于网络阻塞没有送达

2.服务端超时未收到第三次握手,重传SYN-ACK报文

3.重试超过次数,服务端断开连接

什么是 TCP 半连接和全连接队列

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;

  • 全连接队列,也称 accept 队列;

正常流程

TCP四次挥手

TCP 四次挥手过程是怎样的?

天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。

双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图:

客户端主动关闭连接 —— TCP 四次挥手

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。

  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。

  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。

  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。

  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态

  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。

  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

为什么挥手需要四次?

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。

  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

实际上就像是把三次握手的第二次握手拆成两步了,一步ACK,一步FIN

原因是 当受到客户端FIN报文时,服务端通常有些数据还没处理/发送完,需要收个尾

什么时候三次挥手

当FIN到来,服务端没有数据要发送,且开启了 【TCP 延迟确认机制】

什么是TCP延迟确认机制

ACK通常只有包头,没有响应数据,为了解决 ACK 传输效率低问题,所以就衍生出了 TCP 延迟确认

  • 当有响应数据要发送时,ACK 会随着响应数据一起立刻发送给对方

  • 当没有响应数据要发送时,ACK 将会延迟一段时间,以等待是否有响应数据可以一起发送

img

挥手丢失数据

第一次挥手丢失:

如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发超过次数的话,客户端断开连接

第二次挥手丢失:

ps:单ACK报文是不会重传的

如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数,关闭连接。

第三次挥手丢失:

1.当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

2.进程主动调用 close 函数来触发服务端发送 FIN 报文。同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。(在这步网络阻塞,FIN发不过去)

3.如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发超过次数后,等待一段时间还是没有ACK服务端就断开连接

ps:当客户端收到第二次挥手,客户端就会处于 FIN_WAIT2 状态等待FIN,由于第一次挥手时客户端调用了close关闭连接,所以如果 60s内没收到FIN,客户端会关闭连接

img

第四次挥手丢失:

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态(TIME_WAIT 状态会持续 2MSL ,2MSL后客户端关闭连接),在TIME_WAIT如果再次收到第三次挥手(FIN),就会重试时长。,

服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发超出次数等待一会服务端就会断掉连接。

img

TIME_WAIT

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了

TIME_WAIT 等待 2 倍的 MSL, 2MSL时长 这其实是相当于至少允许报文丢失一次,不能连续丢包两次。

比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

为什么需要 TIME_WAIT 状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;

  • 保证「被动关闭连接」的一方,能被正确的关闭;

原因一:防止历史连接中的数据,被后面相同四元组的连接错误的接收

序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?

TIME-WAIT 时间过短,收到旧连接的数据报文

如上图:

  • 服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。

  • 接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 SEQ = 301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。

为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。(拖死延迟的数据包,防止数据包送到了下一个连接里)

原因二:保证「被动关闭连接」的一方,能被正确的关闭,防止报错

TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,CLOSE状态的客户端接收到FIN会返回RST错误报文

服务端收到这个 RST 并将其解释为一个错误(Connection reset by peer),这对于一个可靠的协议来说不是一个优雅的终止方式。

为了防止这种情况出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK,如果服务端没有收到 ACK,那么就会触发 TCP 重传机制,服务端会重新发送一个 FIN,这样一去一来刚好两个 MSL 的时间。

TIME-WAIT 时间过短,没有确保连接正常关闭

TIME-WAIT 时间正常,确保了连接正常关闭

客户端在收到服务端重传的 FIN 报文时,TIME_WAIT 状态的等待时间,会重置回 2MSL。

time_wait 状态的影响:

  • TCP 连接中,「主动发起关闭连接」的一端,会进入 time_wait 状态

  • TCP 端口数量,上限是 6.5w

  • 占用端口资源, time_wait 状态下,TCP 连接占用的端口,无法被再次使用,大量 time_wait 状态存在,会导致新建 TCP 连接会出错

  • 占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等

.解决办法:

  • 允许 time_wait 状态的 socket 被重用(只适用客户端,也就是连接发送方)

  • 当系统中处于 TIME_WAIT 的连接一旦超过参数值时,系统将后面的 TIME_WAIT 连接状态重置

  • 调用close后,会立该发送一个RST标志给对端,来跳过四次挥手

TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT

服务器出现大量 TIME_WAIT 状态的原因有哪些?

首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。

问题来了,什么场景下服务端会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接

  • 第二个场景:HTTP 长连接超时

  • 第三个场景:HTTP 长连接的请求数量达到上限

第一个场景:HTTP 没有使用长连接

我们先来看看 HTTP 长连接(Keep-Alive)机制是怎么开启的。

在 HTTP/1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的 header 中添加:

Connection: Keep-Alive

然后当服务器收到请求,作出回应的时候,它也被添加到响应中 header 里:

Connection: Keep-Alive

这样做,TCP 连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个 TCP 连接。这一直继续到客户端或服务器端提出断开连接。

从 HTTP/1.1 开始, 就默认是开启了 Keep-Alive,现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。

如果要关闭 HTTP Keep-Alive,需要在 HTTP 请求或者响应的 header 里添加 Connection:close 信息,也就是说,只要客户端和服务端任意一方的 HTTP header 中有 Connection:close 信息,那么就无法使用 HTTP 长连接的机制。

关闭 HTTP 长连接机制后,每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接

请求和响应的双方都可以主动关闭 TCP 连接。(根据大多数 Web 服务的实现,不管哪一方禁用了 HTTP Keep-Alive,都是由服务端主动关闭连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。)

当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive,因为任意一方没有开启 HTTP Keep-Alive,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT 状态的连接。

第二个场景:HTTP 长连接超时

HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。为了避免资源浪费的情况,一般超链接都会超时关闭。如果客户端超时没有发送新的请求,服务端就会主动关闭连接

可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务端接收到,以至于 HTTP 长连接超时。

HTTP 长连接超时

第三个场景:HTTP 长连接的请求数量达到上限

Web 服务端通常会有个参数,来定义一条 HTTP 长连接上最大能处理的请求数量,当超过最大限制时,就会主动关闭连接。 如果达到这个参数设置的最大值时,则 nginx 会主动关闭这个长连接,那么此时服务端上就会出现 TIME_WAIT 状态的连接。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISH 状态,占用着系统资源。

方案一 :TCP保活

,每隔一个时间间隔,发送一个探测报文,果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡

方案二:

我们可以自己在应用层实现一个心跳机制,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。

TCP可靠性传输

为了实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。

那么,TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。

今天,将重点介绍 TCP 的重传机制、滑动窗口、流量控制、拥塞控制。

img

重传机制

TCP 实现可靠传输的方式之一,是通过序列号与确认应答。在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。

TCP 针对数据包丢失的情况,会用重传机制解决。

接下来说说常见的重传机制:

  • 超时重传

  • 快速重传

  • SACK

  • D-SACK

超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传

TCP 会在以下两种情况发生超时重传:

  • 发送方数据包丢失

  • 接收方返回的ACK丢失

超时重传的两种情况

超时时间应该设置为多少呢?

我们先来了解一下什么是 RTT(Round-Trip Time 往返时延),从下图我们就可以知道:

RTT

RTT 指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

计算RTP:

由于网络会波动,超时重传时间 RTO 的值」应该是一个动态变化的值。

估计往返时间,通常需要采样以下两个:

  • 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。

  • 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。

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

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传

TCP采用的是累计确认机制,即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)

如图所示,报文段1成功接收并被确认ACK 2,接收端的期待序号为2,当报文段2丢失,报文段3失序到来,与接收端的期望不匹配,接收端重复发送冗余ACK 2。 这里写图片描述

这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK2,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。

快速重传的工作方式是基于冗余ACK机制,当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

快速重传解决了超市周期过长的问题,但它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。

举个例子,假设发送方发了 6 个数据,编号的顺序是 Seq1 ~ Seq6 ,但是 Seq2、Seq3 都丢失了,那么接收方在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方,但是发送方并不清楚这连续的 ACK2 是接收方收到哪个报文而回复的, 那是选择重传 Seq2 一个报文,还是重传 Seq2 之后已发送的所有报文呢(Seq2、Seq3、 Seq4、Seq5、 Seq6) 呢?

  • 如果只选择重传 Seq2 一个报文,那么重传的效率很低。因为对于丢失的 Seq3 报文,还得在后续收到三个重复的 ACK3 才能触发重传。

  • 如果选择重传 Seq2 之后已发送的所有报文,虽然能同时重传已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的报文是已经被接收过了,对于重传 Seq4 ~Seq6 折部分数据相当于做了一次无用功,浪费资源。

为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK 方法

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment), 选择性确认

这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,知道了这些信息,就可以只重传丢失的数据。(这种方式是基于快速重传的优化)

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

选择性确认

滑动窗口

引入窗口概念的原因

我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。

这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。 后面的数据包必须等前一个被应答ACK了再发送

这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低

为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,窗口内中间 若有 ACK 丢失,可以通过「下一个确认应答进行确认」

用滑动窗口方式并行处理

图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答

窗口大小由哪一方决定?

TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。、

发送方的滑动窗口

我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

img

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。(发的速度比ACK的速度快)

可用窗口耗尽

在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

32 ~ 36 字节已确认

程序是如何表示发送方的四个部分的呢?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

SND.WND、SND.UN、SND.NXT

向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

接收方的滑动窗口

接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:

  • #1 + #2 是已成功接收并确认的数据(等待应用进程读取);

  • #3 是未收到数据但可以接收的数据;

  • #4 未收到数据并不可以接收的数据;

接收窗口

发送和接受窗口大小一般不调整,当接受速度比TCP处理速度快太多时,防止缓存过载,服务端会告诉客户端更改窗口大小

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

流量控制的实现:

TCP 通过让接收方指明希望的发送窗口大小来进行流量控制。

目的:

避免发送方的的数据填满接收方的缓存

不做流量控制的窗口间数据交互

下面举个栗子,为了简单起见,假设以下场景:

  • 客户端是接收方,服务端是发送方

  • 假设接收窗口和发送窗口相同,都为 200

  • 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响

流量控制

根据上图的窗口数据交互,说明下每个过程:

  1. 客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。

  2. 服务端收到请求报文后,发送确认报文和 80 字节的数据,于是可用窗口 Usable 减少为 120 字节,同时 SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321。

  3. 客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节,RCV.NXT 也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。

  4. 服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法再继续发送数据。

  5. 客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节,RCV.NXT 也就指向 441,接着发送确认报文给服务端。

  6. 服务端收到对 80 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 321,于是可用窗口 Usable 增大到 80。

  7. 服务端收到对 120 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 441,于是可用窗口 Usable 增大到 200。

  8. 服务端可以继续发送了,于是发送了 160 字节的数据后,SND.NXT 指向 601,于是可用窗口 Usable 减少到 40。

  9. 客户端收到 160 字节后,接收窗口往右移动了 160 字节,RCV.NXT 也就是指向了 601,接着发送确认报文给服务端。

  10. 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

操作系统缓冲区与滑动窗口的关系

前面的流量控制例子,我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整

那操作系统的缓冲区,是如何影响发送窗口和接收窗口的呢?

我们先来看看第一个例子。

当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化。

考虑以下场景:

  • 客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为 360

  • 服务端非常的繁忙,当收到客户端的数据时,应用层不能及时读取数据。

img

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变,这个内容后面会说,这里先简单提一下。

我们先来看看第二个例子。

当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。

img

所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。(还没来得及通知发送方窗口缩了,发送方就再次发送,把缓存冲爆了)

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。

窗口关闭

在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口关闭潜在的危险

接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。

窗口关闭潜在的危险

这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

TCP 是如何解决窗口关闭时,潜在的死锁现象呢?

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接发送方收到接收方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

窗口探测

  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持

糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。

,要解决糊涂窗口综合症,就要同时解决下面两个问题就可以了:

  • 让接收方不通告小窗口给发送方

  • 让发送方避免发送小数据

怎么让接收方不通告小窗口呢?

接收方通常的策略如下:

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

怎么让发送方避免发送小数据呢?

发送方通常的策略如下:

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS

  • 条件二:收到之前发送数据的 ack 回包;

只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。

拥塞控制

为什么要有拥塞控制呀,不是有流量控制了吗?

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重

所以,TCP 不能忽略网络上发生的事,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

拥塞控制,控制的目的就是避免「发送方」的数据冲垮整个网络。

什么是拥塞窗口?和发送窗口有什么关系呢?

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

(为了在「发送方」调节所要发送数据的量。)

我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

那么怎么知道当前网络是否出现了拥塞呢?

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

拥塞控制有哪些控制算法?

拥塞控制主要是四个算法:

  • 慢启动

  • 拥塞避免

  • 拥塞发生

  • 快速恢复

慢启动

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?

慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。

慢启动算法

可以看出慢启动算法,发包的个数是指数性的增长

那慢启动涨到什么时候是个头呢?

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • cwnd < ssthresh 时,使用慢启动算法。

  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法

前面说道,当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。

一般来说 ssthresh 的大小是 65535 字节。

那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

接上前面的慢启动的栗子,现假定 ssthresh8

  • 当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长。

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了「拥塞发生算法」。

拥塞发生

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传

  • 快速重传

发生超时重传的拥塞发生算法

当发生了「超时重传」,则就会使用拥塞发生算法。

这个时候,ssthresh 和 cwnd 的值会发生变化:

  • ssthresh 设为 cwnd/2

  • cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)

也就是说发生超时重传后,就会把慢启动门限设置为当前拥塞窗口大小的一半,

然后把拥塞窗口大小归0,重新从慢启动开始增长

缺点:突然降速太多了,会造成网络卡顿。

发生快速重传的拥塞发生算法

还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;

  • ssthresh = cwnd;更新慢启动门限

  • 进入快速恢复算法

快速恢复

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

正如前面所说,进入快速恢复之前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;

  • ssthresh = cwnd;

然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);

  • 重传丢失的数据包;

  • 如果再收到重复的 ACK,那么 cwnd 增加 1;

  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

快速恢复算法的变化过程如下图:

快速重传和快速恢复

也就是没有像「超时重传」一夜回到解放前,而是还在比较高的值,后续呈线性增长。

IP篇

IP 基础知识全家桶

前菜 —— IP 基本认识

IP 在 TCP/IP 参考模型中处于网络层

网络层的主要作用是:实现主机与主机之间的通信,也叫点对点(end to end)通信。

IP 的作用

网络层与数据链路层有什么关系呢?

有的小伙伴分不清 IP(网络层) 和 MAC (数据链路层)之间的区别和关系。

其实很容易区分,在上面我们知道 IP 的作用是主机之间通信用的,而 MAC 的作用则是实现「直连」的两个设备之间通信,而 IP 则负责在「没有直连」的两个网络之间进行通信传输。

换句话说,MAC是负责某个区间内的通信传输,IP是负责将数据包从源地址发到最终目的地址

IP 的作用与 MAC 的作用

数据包内 源IP地址和目标IP地址在传输过程中是不会变化的(前提:没有使用 NAT 网络),只有源 MAC 地址和目标 MAC 一直在变化。

主菜 —— IP 地址的基础知识

IP 地址(IPv4 地址)由 32 位正整数来表示,IP 地址在计算机是以二进制的方式处理的。

而人类为了方便记忆采用了点分十进制的标记方式,也就是将 32 位 IP 地址以每 8 位为组,共分为 4 组,每组以「.」隔开,再将每组转换成十进制。

点分十进制

IP地址最大值是2的32次方,也就是43亿

实际上,IP 地址并不是根据主机台数来配置的,而是以网卡。像服务器、路由器等设备都是有 2 个以上的网卡,也就是它们会有 2 个以上的 IP 地址。

每块网卡可以分配一个以上的IP地址

因此,让 43 亿台计算机全部连网其实是不可能的

可能有的小伙伴提出了疑问,现在不仅电脑配了 IP 地址, 手机、IPad 等电子设备都配了 IP 呀,照理来说肯定会超过 43 亿啦,那是怎么能够支持这么多 IP 的呢?

因为会根据一种可以更换 IP 地址的技术 NAT,使得可连接计算机数超过 43 亿台。 NAT 技术后续会进一步讨论和说明

IP 地址的分类

互联网诞生之初,IP 地址显得很充裕,于是计算机科学家们设计了分类地址

IP 地址分类成了 5 种类型,分别是 A 类、B 类、C 类、D 类、E 类。

IP 地址分类

上图中黄色部分为分类号,用以区分 IP 地址类别。

如果主机号全0,IP地址代表仅网络号指向的那个网段,该IP代表该子网本身;如果主机号全1,IP地址代表网络号指向的全部主机,IP地址代表广播地址

广播地址用于什么?

广播地址用于在同一个链路中相互连接的主机之间发送数据包

广播地址可以分为本地广播和直接广播两种。

  • 在本网络内广播的叫做本地广播。例如网络地址为 192.168.0.0/24 的情况下,广播地址是 192.168.0.255 。因为这个广播地址的 IP 包会被路由器屏蔽,所以不会到达 192.168.0.0/24 以外的其他链路上。

  • 在不同网络之间的广播叫做直接广播。例如网络地址为 192.168.0.0/24 的主机向 192.168.1.255/24 的目标地址发送 IP 包。收到这个包的路由器,将数据转发给 192.168.1.0/24,从而使得所有 192.168.1.1~192.168.1.254 的主机都能收到这个包(由于直接广播有一定的安全问题,多数情况下会在路由器上设置为不转发。) 。

ToDO:无分类地址CIDR

image-20231120235002742

公有 IP 地址与私有 IP 地址

在 A、B、C 分类地址,实际上有分公有 IP 地址和私有 IP 地址。

img

平时我们办公室、家里、学校用的 IP 地址,一般都是私有 IP 地址。因为这些地址允许组织内部的 IT 人员自己管理、自己分配,而且可以重复。

公有 IP 地址是有个组织统一分配的,并且公有 IP 地址基本上要在整个互联网范围内保持唯一。

公有 IP 地址与私有 IP 地址

公有 IP 地址由谁管理呢?

私有 IP 地址通常是内部的 IT 人员管理,公有 IP 地址是由 ICANN 组织管理,中文叫「互联网名称与数字地址分配机构」。

IANA 是 ICANN 的其中一个机构,它负责分配互联网 IP 地址,是按州的方式层层分配。

img

IP 地址与路由控制

IP地址的网络地址这一部分是用于进行路由控制。

路由控制表中记录着网络地址与下一步应该发送至路由器的地址。在主机和路由器上都会有各自的路由器控制表。

在发送 IP 包时,首先要确定 IP 包首部中的目标地址,再从路由控制表中找到与该地址具有相同网络地址的记录,根据该记录将 IP 包转发给相应的下一个路由器。如果路由控制表中存在多条相同网络地址的记录,就选择相同位数最多的网络地址,也就是最长匹配。

下面以下图的网络链路作为例子说明:

IP 地址与路由控制

  1. 主机 A 要发送一个 IP 包,其源地址是 10.1.1.30 和目标地址是 10.1.2.10,由于没有在主机 A 的路由表找到与目标地址 10.1.2.10 相同的网络地址,于是包被转发到默认路由(路由器 1

  2. 路由器 1 收到 IP 包后,也在路由器 1 的路由表匹配与目标地址相同的网络地址记录,发现匹配到了,于是就把 IP 数据包转发到了 10.1.0.2 这台路由器 2

  3. 路由器 2 收到后,同样对比自身的路由表,发现匹配到了,于是把 IP 包从路由器 210.1.2.1 这个接口出去,最终经过交换机把 IP 数据包转发到了目标主机

环回地址是不会流向网络

环回地址是在同一台计算机上的程序之间进行网络通信时所使用的一个默认地址。

计算机使用一个特殊的 IP 地址 127.0.0.1 作为环回地址。与该地址具有相同意义的是一个叫做 localhost 的主机名。使用这个 IP 或主机名时,数据包不会流向网络

IP 分片与重组

每种数据链路的 MTU 之所以不同,是因为每个不同类型的数据链路的使用目的不同。使用目的不同,可承载的 MTU 也就不同。

其中,我们最常见数据链路是以太网,它的 MTU 是 1500 字节。

那么当 IP 数据包大小大于 MTU 时, IP 数据包就会被分片。

经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行,路由器是不会进行重组的。

假设发送方发送一个 4000 字节的大数据报,若要传输在以太网链路,则需要把数据报分片成 3 个小数据报进行传输,再交由接收方重组成大数据报。

分片与重组

在分片传输中,一旦某个分片丢失,则会造成整个 IP 数据报作废(如果分片丢失,有可能丢的刚好是包含TCP头的,所以不管如何得作废整个报文),所以 TCP 引入了 MSS 也就是在 TCP 层进行分片不由 IP 层分片,那么对于 UDP 我们尽量不要发送一个大于 MTU 的数据报文

IPv6 基本认识

IPv4 的地址是 32 位的,大约可以提供 42 亿个地址,但是早在 2011 年 IPv4 地址就已经被分配完了。

但是 IPv6 的地址是 128 位的

但 IPv6 除了有更多的地址之外,还有更好的安全性和扩展性,说简单点就是 IPv6 相比于 IPv4 能带来更好的网络体验。

但是因为 IPv4 和 IPv6 不能相互兼容,所以不但要我们电脑、手机之类的设备支持,还需要网络运营商对现有的设备进行升级,所以这可能是 IPv6 普及率比较慢的一个原因。

TODO://

TODO:NAT