计算机网络

计算机网络的基础知识,以及一部分开发必备网络知识
计算机网络
前置基础
数据包交换原则
数据包:独立的,包含将数据送到目的地必要信息的数据集合
数据包交换:独立的处理任何一个到来的数据包,在对应链路空闲时将该数据包发送出去。
流:一系列属于相同起点与终点的数据
数据包交换不需要流的状态,如果流是有状态的,那会给交换机带来巨大的管理压力,大量需要管理的状态也会拖慢网络的速度。同时,如果某一端出现了问题,也不会导致无限重连,给网络带来巨大的压力,因为流不需要记录数据包是否抵达,也就不需要为数据丢失负责。
分层原则
在计算机网络中,按照TCP/IP分层方式,将其分为了五层。这种分层原则在很多地方都有应用,在正式开始介绍网络之前,先了解一下分层原则的含义。
层(layer)代表的是功能组件,每一层都借助下层提供的服务以及自身带有的处理,为上层提供定义良好的服务。层与层之间的交互也是沿着层级划分顺序的传递。
分层的价值:
- 模块化:他将系统分解为更小更易管理的模块
- 定义良好的服务:每一层都为上层提供定义良好的服务
- 重用:上层可以借助下层提供的服务进一步实现功能
- 关注点分离:每一层都可以专注于自身工作,可以独立发展
- 持续优化:由于每一层之间的交互都只涉及简单的数据传输,因此可以很轻易的做出变动或更改。
封装原则
互联网中各层级的协议多种多样,为了继续遵循关注点分离以及方便数据包的解析,将数据包按照层级进行一层一层的封装,在原有的载荷上添加本层需要传递的数据包的头和尾。
封装的体现就在每一层拿到传来的数据之后都把它当作有效载荷 (一坨要传递的数据)而不需要去管数据使用的协议,传递的信息等等,只需要在此基础上添加自己要传递的信息即可。思想上类似Composite模式,用递归的方式去处理层级关系。
输入URL到网页显示期间的操作
HTTP
浏览器首先要做的是将输入的域名解析为IP地址,这里使用到了DNS域名解析服务。
它的大致流程为,浏览器向DNS域名服务器发送一个请求,查询指定域名对应的IP地址,DNS Server使用循环或递归查询方式获取到对应的IP并返回给浏览器。浏览器获取到地址后,会将这个地址缓存起来,这样之后请求就不用再请求DNS Server了。
浏览器获取到IP地址后就可以开始打包数据了,浏览器借助URL确定服务器地址以及目标文件名,这些信息会被用来生成HTTP请求信息。
协议栈
接下来数据包就会被交给OS中的协议栈
协议栈的内部分为几个部分,分别承担不同的工作。上下关系是有一定的规则的,上面的部分会向下面的部分委托工作,下面的部分收到委托的工作并执行。
TCP
通过协议栈,可以确定接下来由TCP处理这个数据包
TCP的详细解释看下面的内容
如果TCP发现报文的数据长度过长(也就是HTTP打包完交给它的数据,不包括TCP头部),超过了MSS,就会进行分片处理,每个分片都会打上TCP头信息。
IP
传输层更多的作用是确定数据如何被发送以及如何被接收,真正的传输工作交给网络层来实现。网络层的主要协议就是IP协议。
IP协议里会指定源IP与目的IP,同时指明协议号,表示这个数据是要按照哪个传输层协议进行传输。
IP协议最重要的一个功能就是路由,每个主机中都会存储一个路由表,其中记录了各个目的地址的转发规则。路由表是通过主机自主学习生成的,主机每收到一个数据包,都会看一眼它从哪里来的,这样就可以知道之后收到相同网段的数据要往哪里发送了。如果主机发现路由表中没有存储对应网段应该转发到哪个路由,就会发到一个默认路由去。
MAC
MAC协议是以太网内的协议,这里的以太网也可以理解为局域网,它负责某个网段内部的数据传输。MAC协议需要知道发送方MAC地址与接收方MAC地址。
当某个机器不知道接收方的MAC时,就轮到ARP协议出场了。它会在当前网段广播询问指定IP的MAC地址,接收方在收到这条广播之后就会将自己的MAC地址发送给发送方。发送方在收到之后,将它存储到ARP缓存里,然后将数据发送过去。
网卡
网络包只是存储在内存中的一组数据,要想发送给其他主机,需要利用网卡,将数字信号转换为电信号,然后才能在网线上传输
交换机
交换机是用于将网络包完好无损的发送到目的地,它工作在链路层,是一个二层网络设备。
交换机在发现电信号到达网络接口后,会将电信号转换为数字信号,然后通过末尾的FCS校验错误,如果没有问题就放到缓冲区去。
交换机内部也有一个MAC地址和端口的映射表,它会根据数据包的MAC地址来判断接下来要往哪个端口转发。如果目标MAC从来没有被记录,那么交换机会将这个数据包发送到自己的所有端口上去。
路由器
路由器在收到数据包之后,会检查MAC地址,看这是不是发送给自己的数据包,如果不是,就直接丢弃,否则放入缓冲区。在完成包接收操作之后,路由器会去掉包开头的MAC头部,因为MAC只用于单个网段,在完成一跳之后,它的使命就完成了。接下来路由器会根据自己的路由表决定要把数据发送到哪里去。
首先根据路由表的网关列判断对方的地址:
- 如果网关是个IP,代表这个数据包要到另一个网段去,需要路由器继续转发。
- 如果网关为空,代表已经找到了目标地址
在知道目标IP之后,就可以使用ARP协议获得对方的MAC地址,然后将数据发送出去,数据会到达另一个交换机,并在中转之后到达下一个路由器。接下来路由器会再做一遍相同的操作,直到找到目标。
运输层
运输层的主要职责是在应用程序与应用程序之间传输数据。
TCP
当两个应用程序通过TCP进行交互时,他会在二者之间建立一条双向连接。在连接的两端,TCP会维护各维护一个状态机来跟踪当前连接的状态。
TCP连接的建立与拆除
TCP建立连接的步骤
- A向B发送一条数据,其中包含SYN字段,表示A希望与B进行连接
- B接收到数据,向A返回确认,并表示B希望与A进行连接
- A接收到连接请求,返回确认,连接建立。
TCP拆除连接的步骤
- A向B发送FIN信号,表示要断开与B的连接
- B接收到信号后不再从连接中获取新数据,返回一个ACK
- A接收到确认后关闭A到B的连接,但B可能还有数据要传输,因此B到A的连接还没有被关闭。
- B完成数据传输后,向A发送FIN信号,关闭B到A的连接
- A收到信号,返回确认,连接正式关闭。
TCP服务模型
属性 | 表现 |
---|---|
字节流 | 可靠的字节流传输服务 |
可靠传输 | 1. 发送确认来确保 2. 使用检验和发现传输错误的数据 3. 使用序列号可以发现没有 4. 流量控制,避免淹没目的主机,同时也能有效利用带宽 |
顺序 | 数据都是按序传输到应用程序 |
拥塞控制 | √ |
TCP报文结构
Source/Destination port:源端口,目的端口,用于标注报文来自/去往主机的哪一个应用程序
Sequence:当前报文携带的数据的首个字节,在整个数据中的位置
Acknowledgment Sequence:已经确定收到了#字节前的部分,期待从#开始之后的数据内容
HLEN:报文头部大小
Checksum:检验和,检验报文是否在传输过程中损坏
PSH:告诉连接的另一端,立即发送数据而不要等待接收到指定数量的数据一起发送。
TCP三次握手的原因
阻止重复历史连接的初始化
三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。现在假设客户端向服务器发起连接,附带的序列号为seq1,但是在服务器接收到请求之前,客户端宕机了,恢复之后,客户端重新发起了一次建立连接请求。服务端首先接收到的大概率是seq1的连接请求,之后服务端响应客户端,此时服务端根据响应回来的ack判断该连接是否是一个历史连接,如果是就向服务器发送RST中止连接。
现在考虑一下如果只有两次握手会导致什么,服务端在接收到请求之后直接响应客户端并建立连接,发送数据,但对客户端来说,这是一个历史连接,并不生效。这就导致网络资源被白白浪费。而三次握手给了发起方一个判断请求是否有效的机会,可以有效防止资源的浪费。同步双方初始序列号
序列号是可靠传输的一个关键因素:
(1) 接收方可以去除掉重复数据
(2) 接收方可以根据数据包的序列号按序接收
(3) 可以标识发送出去的数据包中,哪些是对方成功接收到的
第二三次握手的作用就是确保双方已经知道对方的序列号,自己接下来发送的数据要按照什么序列号来传输。避免资源浪费
第三次握手还有一个作用就是让服务器知道客户端已经收到了自己发出去的数据,完成了连接的建立。如果只有两次握手,那么服务端就不清楚客户端究竟有没有收到自己的数据,因此对于每一个SYN都只能主动建立一个连接。如果客户端发送的SYN被阻塞,重复多次发送SYN,就会导致服务端建立多个连接,浪费资源。
TCP每次建立连接的初始化序列号不同的原因
title: 主要原因
- 防止历史报文被下一个相同四元组的连接接收
- 防止黑客伪造的相同序列号的TCP报文被对方接收
具体场景:
客户端和服务端建立一个TCP连接,客户端向服务端发送了一个数据包。如果客户端与服务端中有一方宕机,在重启后重新建立TCP连接。现在假设每次初始序列号不变,若在上一次连接期间发送的数据包此刻姗姗来迟,可能会由于它恰好在接收窗口内而被接收方接收过去引发混乱。
这也同样是TCP断开连接时在最后等待一段时间的原因:让本次连接的所有数据包都消失在网络当中。
TCP分片的意义
title: 基本术语
MTU:一个网络包的最大长度,一般为1500字节。包含IP头部,TCP头部以及数据
MSS:除去IP和TCP头部之后,一个网络包容纳的TCP数据的最大长度
IP分片操作:当IP层发现有一个大小超过MTU的数据要发送,他就会将数据进行分片发送。目标主机的IP层接收之后重新组装再交付给上层。
这样做看似非常完美,但IP协议是个尽力而为协议,他不会进行超时重传。也就是如果有一个IP分片丢失,对于上层的TCP来说就是有一个TCP报文段没有收到,它会重传整个TCP报文段,这显然是非常低效的。
因此如果我们将分片的任务交给TCP,它确保每一个报文段中搭载的数据不会超过MSS(这相当于保证了IP层不会分片)。那么即使发生丢包,也是以MSS为单位重传。
TCP断开连接
- 客户端准备关闭链接,发送一个首部
FIN
标志位为1的报文,之后进入FIN_WAIT_1
状态 (表明客户端已经没有数据要发送了) - 服务端收到报文后,向客户端发送ACK应答,进入
CLOSE_WAIT
状态 - 客户端收到ACK响应后进入
FIN_WAIT_2
状态 (服务端表示已经接受到请求,但可能还有数据要处理和发送) - 服务端处理完数据之后,向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态 (服务端处理完毕,向客户端表示自己这边可以关闭连接了) - 客户端收到服务器的FIN之后,回应一个ACK,进入
TIME_WAIT
状态 - 服务端接受到ACK之后,进入CLOSE状态,至此服务端已经完成连接的关闭
- 客户端经过2MSL时间之后,自动进入CLOSE状态
MSL:报文最大生存时间
TIME_WAIT等待2倍MSL的原因是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后优惠向对方发送响应。一来一回需要等待2倍时间。
PS:客户端在接收到重复FIN之后会重置2MSL
TIME_WAIT存在的原因
只有主动发起关闭的一方才会有TIME_WAIT状态 他主要有两个目的:
防止历史连接的数据被之后相同四元组的连接接受
保证被动关闭连接的一方能被正确关闭 TIME_WAIT可以保证最后的ACK能被接收方接收到并帮助它正确关闭。如果发送方不进行等待,直接关闭连接,一旦ACK丢失,被动关闭方就会尝试重传FIN,由于发送方连接已关闭,它会返回一个RST给被动关闭方,而在被动关闭的一方眼里,这显然是一个错误信息。这种关闭方式并不应该出现在一个可靠的协议中。 客户端接收到重复FIN会重置TIME_WAIT也是为了保证服务端正确接收到了ACK。
TCP重传
TCP目前具有的重传机制: - 超时重传 - 快速重传 - SACK - D-SACK
快速重传
快速重传不像超时重传那样以时间为驱动,而是以数据为驱动。当接收方接收到发送方发来的数据之后,会将自己期待的下一个数据的起始序列号返回给发送方。如果发送方收到了三个同样的ACK,就可以判断数据包在网络中丢失了,需要重传。 快速重传虽然解决了超时的问题,但是他依旧无法解决重传多少数据包的问题,因为发送方只能判断出从某个序号开始有数据丢失,不知道丢失了多少。 为了解决这个问题,又引入了SACK的概念
SACK
SACK,选择性确认。这种方式需要在TCP头部的选项字段里加一个SACK的字段,它会将接收端
已接受到
的数据的信息发送给发送方,发送方根据这个判断出哪些数据接收到了,哪些数据没收到。
D-SACK
主要使用了SACK来告诉发送方有哪些数据被重复接收 如果ACK丢失了,发送方在超时之后会重传第一个数据包。接收方收到后会发现数据是重复收到的,于是返回SACK = 重复数据序号范围。如果ACK的值大于SACK,就代表这个SACK是D-SACK。这样发送方就知道了数据没有丢,是ACK丢失了。 如果由于网络延时导致触发了数据重传,冗余数据到达服务端后,它也会将重复数据的范围返回过去。由于客户端在之前已经接收到了那部分冗余数据的ACK,因此他可以判断出数据包是网络延迟了。 D-SACK好处: - 发送方可以了解到发出去的包是丢失了,还是接收方的ACK包丢了 - 可以了解到是否发送方的数据包被网络延迟了 - 可以了解到网络中是否把发送方的数据包复制了
滑动窗口
TCP中每发送一个数据都要进行一次ACK。只有当上一个数据包确认收到了才发送下一个。这样做的缺点是效率比较低,如果RTT很长,通信的效率就会非常低。 因此TCP引入了滑动窗口这个概念,即使在往返时间较长的情况下,他也不会降低网络通信的效率。 窗口的本质其实就是一个缓冲区,发送方主机在等到ACK之前,必须在缓冲区中保留已发送的数据,如果收到ACK,数据就可以从缓冲区清除。而即使中途有ACK丢失了,也可以通过之后接收到的ACK确认数据是否收到。
发送方滑动窗口
接收方滑动窗口
窗口关闭
定义:窗口大小为0时,就会阻止发送方给接收方传递数据,直到窗口变为非0为止。
窗口关闭的潜在风险
接收方的窗口大小信息会跟着ACK报文发送给发送方。如果发生了窗口关闭,在接收端完成数据处理之后,向发送端发送一个ACK,表示自己可以继续接收数据。假如这个ACK丢失了,就会导致发送端一直在等待接收端处理完数据,而接收端一直在等待发送端传输数据。
为了解决这个问题,TCP为每一个连接设有一个持续定时器,只要TCP连接一方收到零窗口通知,就会启动持续计时器。如果持续计时器超时,就会发送窗口探测报文,对方在确认这个探测报文时,给出自己的接收窗口大小。
糊涂窗口综合症
如果发送方的发送速度比接收方处理数据的速度快,就会导致发送方窗口越来越小。最后如果每当接收方稍微腾出一点空间,发送方就急着继续发送,就会导致网络资源利用率非常低,也就是糊涂窗口综合症。 它的解决思路也非常简单: - 接收方不通告小窗口给发送方 当窗口大小小于MSS和缓存空间一半的最小值时,就会通告发送方窗口为0,阻止发送方继续发数据。 - 发送方避免发送小数据 使用Nagle
算法,该算法的思路是延时处理,只有满足以下两个条件中的任意一个条件,才可以发送数据: - 等待窗口大小>=MSS,数据大小>=MSS - 收到之前发送的数据的ACK PS:只有上述两个方案同时满足,才能避免糊涂窗口综合症。假如接收方仍旧会通告小窗口给发送方,那么Negla算法的第二个条件仍然会被满足,也依旧会发送小数据。
拥塞控制
拥塞控制存在的目的是,当网络负载较大时,避免发送方持续高速输出导致网络负载进一步加重。 因此为了调节发送方要发送的数据量,引入了拥塞窗口这个概念,从此,发送窗口的大小就是拥塞窗口与接收窗口的最小值。
慢启动
规则:发送方每收到一个ACK,拥塞窗口大小就会+1。当拥塞窗口大小达到慢启动门限(ssthresh)之后,就会停止慢启动算法。
拥塞避免算法
规则:每当收到一个ACK,cwnd就增加1/cwnd 拥塞避免算法将原本慢启动的指数级增长变成了线性增长。
拥塞发生
一旦发生了超时重传,就会启动拥塞发生算法,此时会将ssthresh设置为cwnd/2 同时将cwnd重置为1。 接下来就重新开始慢启动,不过这种方式较为激进,会造成网络卡顿。
而当发生了快速重传,则是选用另一套逻辑。因为既然ACK还能顺利抵达那就代表网络中的阻塞情况并不是特别严重,此时将cwnd设为cwnd/2,ssthresh=cwnd,并进入快速恢复算法。
快速恢复
- 首先将cwnd设为ssthresh+3(接收到了3个ACK)
- 然后重传丢失的数据包
- 如果收到重复的ACK,将cwnd增加1
- 如果收到新数据的ACK,将cwnd设置为第一步中的ssthresh值。这说明D-SACK的数据全部都已经收到,恢复过程已结束,可以再次进入拥塞避免状态。
快速恢复图解
TCP快速建立连接
在Linux3.7版本中,提供了TCP Fast Open功能,该功能可以减少TCP连接的时延。 具体流程: 1. 在第一次建立连接时,服务器将一个加密过的Cookie跟着SYN+ACK传给客户端。其他流程和常规握手相同,因此最少仍需要2RTT 2. 在之后的请求中,客户端在发起SYN的同时携带上Cookie,服务端根据Cookie中的内容解析出TCP相关的信息,这样就可以直接跳过握手过程
快速连接建立过程
TCP是面向字节流的协议
我们通常说TCP是面向字节流的协议,UDP是面向报文的协议,这是因为OS对二者的发送方的机制不同。 首先来说说UDP为什么是面向报文的协议。当用户通过UDP传输消息时,OS不会对消息进行拆分,因此发出去的报文中的数据部分就是完整的用户信息,也就是说每个UDP报文就是一个用户消息的边界。通俗来说就是,报文是UDP发送数据的最基本的单位,接收方以报文为单位接受数据。 接下来讲讲TCP为什么是面向字节流的协议。和UDP不同,TCP有分片机制,也就是说消息可能会被拆分成多条TCP报文,对接收方来说就无法了解到接收到的报文是之前报文的延续还是一个新的报文。因此,用户消息和TCP报文之间并非一一对应的关系,所以说TCP是面向字节流的协议。
解决粘包
面向字节流也就带来了粘包这个问题,这个问题要交给上层的应用层来解决。
- 固定长度的消息 这种解决方法非常好理解,发送和接收都设置一个固定长度,这样应用程序就可以成功解析出传递的数据。
- 特殊字符作为边界 面向字节流的一大问题就是无法明确表示用户消息的边界,因此我们可以自定义一个用户消息的边界,一旦读取到特殊字符,就认为读完一个完整消息。典型示例就是HTTP
- 自定义消息结构 最后一种方式,也就是TCP使用的,自定一个消息结构,由包头和数据组成,包头大小固定(让接收方能明确接收到包头中的提示信息)。同时包头中的信息可以指明后续数据的大小。
UDP
UDP只会获取需要传输的数据,然后交给网络层
UDP不像TCP那样保证可靠传输,一般由不需要可靠传输,或是拥有私有传输机制的应用使用。
UDP报文结构
Length:UDP数据报中数据的长度
Checksum:UDP中的检验和会用到IP层的一部分信息,虽然这会破坏分层原则,但也可以帮助UDP发现数据报是否传输到错误的目的地。
UDP服务模型
属性 | 表现 |
---|---|
无连接服务 | UDP不会建立连接,数据包也会以任意的顺序出现,因此如果应用程序在意数据的顺序就需要进行重排序 |
不可靠传输 | 1. 没有传输确认 2. 没有手段发现传输信息的错误或丢失 3. 没有流量控制 |
网络层
DHCP
DHCP是接入网络的主机动态获取自己的IP地址的协议,它的流程如下:
客户端首先发送DHCP发现报文,因为它并不清楚DHCP服务端在哪里,因此它会使用UDP广播通信,它使用的目标地址是255.255.255.255:67,源IP地址是0.0.0.0:68。DHCP客户端将这个IP数据报传输给链路层,然后链路层将它广播到网络中的所有设备。
DHCP服务器收到发现报文后,使用DHCP提供报文做出响应。这里面提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 IP 地址租用期。仍然是将这个报文发送到广播地址
DHCP客户端收到多个DHCP提供报文后,从里面选择一个服务器,发送DHCP请求报文进行响应,回显配置参数,表示自己就要这个门牌号了。
最后服务器用DHCP ACK报文进行响应,客户端收到之后就可以在租用期内使用这个IP了
NAT
由于IPv4的地址数量有限,因此诞生了像是NAT一类用于缓解地址消耗的协议。NAT的基本思想就是将内网机器的IP+端口映射到公网IP的不同端口上。下面用一张图来表示:
图中有两个客户端 192.168.1.10 和 192.168.1.11 同时与服务器 183.232.231.172 进行通信,并且这两个客户端的本地端口都是 1025。
此时,两个私有 IP 地址都转换 IP 地址为公有地址 120.229.175.121,但是以不同的端口号作为区分。
因此,只要生成这样一张转换表,就可以让内网主机全都使用一个IP与外部通信。这张表中的记录会在首次建立TCP连接时建立,又会在连接结束后删除
不过NAT也有一定的缺陷:
外部无法与内网机器进行主动交互,因为它根本不知道内网机器的IP映射
地址转换会带来一定的性能开销
一旦NAT路由器重启,所有TCP连接都要重置。
开发相关网络知识
HTTP
HTTP(超文本传输协议):一个专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
状态码
状态码 | 含义 |
---|---|
1xx | 提示信息,表示目前是协议处理的中间状态 |
2xx | 成功,报文已被收到并被正确处理 |
3xx | 重定向,资源位置发生变动,需要客户端重新发送请求 |
4xx | 客户端错误,请求报文有误,服务器无法处理 |
5xx | 服务器错误,服务器在处理请求时内部发生错误 |
「200 OK」是最常见的成功状态码,表示一切正常。如果是非
HEAD
请求,服务器返回的响应头都会有 body 数据。「204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
「206 Partial Content」是应用于 HTTP 分块下载或断点续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。
「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
「302 Found」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
301 和 302 都会在响应头里使用字段 Location
,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。
「304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。
「400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
「403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。
「500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
「501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
「502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
「503 Service Unavailable」表示服务器当前很忙,暂时无法响应客户端,类似“网络服务正忙,请稍后重试”的意思。
GET和POST
GET的语义:从服务器获取指定的资源,GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符
POST的语义:根据请求负荷对指定的资源做出处理
安全与幂等
安全:请求方法不会破坏服务器上的资源
幂等:多次执行相同的操作,结果都是相同的
由于GET方法是只读操作,因此它是安全且幂等的,无论发起多少次请求,服务器上的资源都不会受到影响。所以GET请求的数据可以被缓存
POST则是新增或提交数据的操作,会修改服务器上的资源,因此它是不安全的。同时多次请求也会创建多个资源,因此它也不是幂等的。基于此,POST请求一般是不会被缓存的。
不过以上都是基于语义得出的结论,在实际操作中仍旧要看对应方法请求的目的。
HTTP缓存
^3b171b
强制缓存
只要浏览器判断缓存没有过期,就直接使用本地缓存,决定权在浏览器这边。
强制缓存利用如下两个响应头来实现
- Cache-Control:一个相对时间,表示资源有效期 优先级高
- Expires:一个绝对时间,表示资源的过期时间
强制缓存的实现流程如下:
- 当浏览器首次请求服务器资源时,服务器会在返回该资源的同时,在Response头上添加Cache-Control,其中设置了过期时间大小。
- 浏览器在此请求访问服务器中相同的资源时,会先比较当前时间与设定的缓存过期时间,如果没有过期就使用缓存。
- 服务器再次收到请求,代表本地缓存已经过期,会更新响应头的Cache-Control
协商缓存
在上面的HTTP状态码介绍中,有一个状态码为304,这代表服务器告诉浏览器,这个资源没有更新,可以直接使用本地缓存。这种通过服务端告知客户端是否可以使用缓存的方式就是协商缓存。
协商缓存的实现方式
- 请求头中携带
If-Modified-Since
字段,内容一般是本地缓存中对应资源的获取时间,服务器接收到请求后发现请求带有该字段,就会将本地缓存的更新时间与服务器端对应资源的最后一次更新时间进行比较,如果服务器端的资源在本地缓存获取时间之后有进行更新,就会返回最新资源;否则返回响应304告诉浏览器使用缓存 - 请求头中携带
If-None-Match
字段,内容是本地缓存中对应资源的Etag(唯一标识响应资源)。服务端接收到请求后会去比对指定资源目前的Etag,如果不一致就代表资源被更新了,返回最新资源,否则304。
如果服务器同时响应了Etag和Last-Modified两个字段,此时使用Etag的优先级更高。
reason: - 即使没有修改文件内容,文件的最后修改时间也可能发生改变,这时如果使用Last-Modified会导致客户端认为文件已经发生修改,从而重新请求。
- 可能有些文件是在秒级以内修改的,
If-Modified-Since
能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次 - 部分服务器无法精确获取文件最后修改时间
ps:协商缓存的两个字段都要配合强制缓存的Cache-control字段使用,只有在强制缓存没有命中的时候才会发起带有协商缓存的请求
HTTPS
- HTTP的信息是明文传输的,HTTPS在TCP和HTTP网络层之间加入了SSL/TLS安全协议,使得报文能够加密传输
- HTTP在三次握手之后就可以进行报文传输,HTTPS还需要进行SSL/TLS的握手过程,才会开始加密报文传输
- HTTPS需要向CA(证书权威机构)申请数字证书来保证服务器的身份可信
SSL/TLS很好的解决了HTTP的安全风险:
- 信息加密:交互信息无法被窃取 借助混合加密实现
- 校验机制:无法篡改通信内容,篡改了就不能正常显式 利用摘要算法为数据生成独一无二的指纹,用于校验数据完整性,解决篡改风险
- 身份证书:证明访问的网站货真价实
混合加密
非对称加密:使用公钥来加密明文,然后使用对应的私钥来解密密文
HTTPS采用对称加密和非对称机密结合的混合加密方式:
- 在通信建立前采用非对称加密(公钥和私钥组成,公钥任意分发而私钥保密,解决密钥交换问题,但速度较慢)的方式交换会话密钥
- 通信过程中全部使用对称加密(运算速度快,密钥必须保密,无法做到安全的密钥交换)
摘要算法+数字签名
为了保证数据不会在传输过程中被修改,我们需要对内容计算出一个指纹,然后同内容一起传输给接收方。接收方收到之后,根据获取到的数据再计算一次指纹,如果和传过来的指纹相同,代表数据没有被篡改。
一般来说,会使用摘要算法(哈希函数)计算出内容的哈希值,这个值是唯一的且无法根据哈希值推导出内容。
不过要注意的是,哈希算法可以保证内容不被篡改,但不能保证内容和哈希值都没有被人替换过,究其原因是客户端无法判断接收到的消息是否来自服务端。
要想解决这个问题就又要提到非对称加密了,非对称加密一般有两种用法:
- 公钥加密,私钥解密。目的是为了保证内容传输的安全,因为被公钥加密的信息,只有使用私钥才能解读出实际的内容。
- 私钥加密,公钥解密。目的是为了保证消息不会被冒充,只有收取到被私钥加密的信息,公钥才能正确解密。而私钥又被服务端持有,因此相当于是表明这条信息来自服务端。
这里我们使用私钥加密,公钥解密的方式来确保哈希值不会被中间人替换,保证消息来自服务端且没有被篡改。
数字证书
到目前为止,我们利用混合算法保证内容传输过程的安全性,用摘要算法和数字签名保证了消息传输的完整性以及消息来源的正确性。但还缺少身份验证环节,数字签名保证了消息来自服务端,但没有保证这个消息一定来自我们期望的那个服务端。
也就是说,如果我们伪造一个公钥给客户端,然后用自己的私钥加密信息发送给客户端,由于客户端手中的公钥和我们的私钥匹配,他就能因此解析出信息。从而让客户端误认为我们是货真价实的服务端。
在HTTPS中,每个网站都会把他们的公钥注册到CA,CA会用私钥给这些公钥做一个数字签名,然后将网站的信息,公钥和数字签名打包成一个数字证书发送给网站。这样,每当客户端要发起请求时,都会先拿着这个数字证书到CA去验证证书是否合法,如果CA能用自己的公钥解析成功,就代表这个证书确实是被注册过的,然后将这个网站的公钥交给客户端,客户端就可以利用这个公钥解析出信息的内容
HTTPS建立连接的流程
SSL/TLS协议基本流程:
- 客户端向服务器索要并验证服务器的公钥
- 双方协商生成会话密钥
- 双方采用会话密钥进行加密通信
详细流程:
ClientHello
客户端向服务器发送加密通信请求
在这一步,客户端主要发送以下信息:
(1) 客户端支持的SSL/TLS协议版本
(2) 客户端生成的随机数,后续会用于生成会话密钥
(3) 客户端支持的密码套件列表ServerHello
服务端收到请求后做出响应,响应包括如下信息:
(1) 确认SSL/TLS协议版本,如果浏览器不支持,就关闭加密通信
(2) 服务器生成的随机数,用于后续生成会话密钥
(3) 确认密码套件列表
(4) 服务器的数字证书客户端回应
客户端收到回应之后,先通过浏览器或OS中的CA公钥,校验服务器的数字证书的真实性。如果证书没有问题,客户端就会取出证书中的服务器公钥,用它来加密报文信息。报文中包含:
(1) 一个随机数(pre-master key)
(2) 加密通信算法改变通知,告诉服务器后续的报文信息都会用会话密钥加密
(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。同时把之前发送的所有内容做一个摘要用来供服务端校验。
到此为止,客户端和服务端都有了三个随机数,接着就用双方协商的加密算法,计算出本次通信的会话密钥
- 服务端回应
服务端收到三个随机数之后,通过协商的加密算法计算出会话密钥。然后向客户端发送信息:
(1) 加密通信算法改变通知,表示随后的信息都会使用会话密钥加密通信
(2) 服务端握手结束通知,同时把之前发送的内容做一个摘要交给客户端进行校验。
客户端校验数字证书的流程
签发证书:
- CA将持有者的公钥,用途,颁发者,有效时间等信息打包,然后进行哈希计算得到一个Hash值。
- 接着将Hash值用CA私钥加密,生成证书签名
- 最后将签名添加在证书上生成数字证书
校验证书:
- 用相同的哈希算法计算数字证书内容的哈希值
- 用CA公钥解密数字签名,获取到哈希值。
- 比较两个哈希值是否相同,如果相同,代表数字证书有效
保证应用数据完整性
TLS在实现上分为握手协议和记录协议:
- TLS握手协议负责协商加密算法和生成对称密钥来保护应用程序数据
- TLS记录协议负责保护应用数据并验证其完整性和来源,因此对HTTP数据加密是使用的记录协议。
具体过程:
- 将消息分割成多个较短的片段,然后分别对每个片段进行压缩
- 然后压缩的片段会被加上MAC值,这个MAC值用来识别数据的完整性,同时防止篡改。此外为了防止重放攻击,还加上了片段的编码。
- 接下来,压缩的片段和MAC值会一起通过对称加密进行加密
- 最后加上由数据类型,版本,长度组成的报文头部
HTTP/1.1 HTTP/2 HTTP/3
HTTP/1.1
HTTP/1.1相比HTTP/1.0的改进:
- 使用长连接的方式改善了频繁建立连接的开销
- 支持管道传输,发起请求之后不必等待响应,可以直接发送下一个请求,减少整体响应时间
HTTP/1.1的性能瓶颈
- 请求/响应头未经压缩就发送,首部信息越多延迟越大。只能压缩body部分
- 首部冗长导致每次互相发送相同的首部造成巨大的浪费
- 服务器按顺序处理请求(客户端只有在收到上一个请求的响应之后才会发送下一个请求),如果先到达的请求处理很慢,会导致客户剩余的请求无法处理(队头阻塞)。
- 没有请求优先级控制
- 请求只能从客户端开始,服务端只能被动响应
HTTP/2
HTTP/2协议基于HTTPS,因此它的安全性是可以保证的
相比于HTTP/1.1,HTTP/2在以下方面做出了改进:
头部压缩
如果同时发送多个请求,且这些请求的头部基本相同,HTTP2就会压缩头,消除掉重复的部分。
它的具体实现就是HPACK
算法:客户端和服务端同时维护一张头部表,所有字段都会存入该表,下次要发送同样的字段时就可以直接发送索引号。二进制格式
HTTP/2全面采用二进制格式传输数据,头信息和数据体都是二进制并且统称为帧。虽然这大幅降低了报文的可读性,但却加快了计算机的处理速度,计算机可以直接解析二进制报文。并发传输
由于HTTP/1.1的实现是基于请求-响应模型的。在同一个连接中,HTTP完成一个事务才能开始处理下一个事务,这就有可能导致队头阻塞。
而在HTTP2中引入了Stream的概念,多个Stream可以复用一条TCP连接。1个TCP连接可以包含多个Stream,Stream里可以包含一个或多个Message(Message对应请求/响应报文)。而在一个Message中,又有多个帧(存放报文各部分的信息)。
针对不同的HTTP请求用独一无二的StreamID来区分,接收端可以通过StreamID来组装HTTP消息。这就使得Stream的帧可以乱序发送,也就代表HTTP2可以并行的发送请求/接收响应。服务器推送
HTTP/2还在一定程度上改善了传统的请求-应答模式。服务器可以主动地向客户端发送消息。
客户端的服务器双方都可以建立Stream,规定客户端建立的StreamID必须是奇数,服务端建立的StreamID必须是偶数。
乍一听可能感觉这个功能没有什么价值,但现在想象一下,如果我们希望请求一个页面,在HTTP/1.1中我们首先要请求html文件,但只请求一个html文件显然是不够的,再加上css和js文件才能构成我们平日使用的网页。这就需要客户端发送多个请求,消息往返次数很多。但在HTTP2中,服务端发现客户端请求了一个网页,在返回html文件的同时,主动将对应的css和js文件返回过去,这就大大提高了通信效率。
HTTP/3
HTTP/2看上去已经非常完美了,但实际上,他仍然存在队头阻塞的问题,只不过问题的根源在于TCP层。HTTP/2用流的方式让服务端不至于因为处理某个特定请求速度过慢而导致无法接收到其他请求。但如果某个请求在传输过程中发生了丢包,就会导致TCP触发超时重传,此时丢失的包之后发送的所有数据包都会被存储在TCP的缓冲区,应用层无法接收到数据。也就是说,如果发生了丢包,那么之后传输的数据,哪怕不属于同一个流也会被TCP关在缓冲区里。
HTTP/3为了解决这个问题,直接将运输层的协议改成了基于UDP的QUIC协议。由于UDP的发送是无序且不可靠的,因此不会因为丢包而发生阻塞。而基于UDP的QUIC协议又实现了类似TCP的可靠传输功能。
QUIC具备如下特点:
无队头阻塞
QUIC也有类似HTTP/2多路复用的概念,同时QUIC也有自己的一套机制来保证传输的可靠性。当某个流发生丢包时,只会阻塞这个流,其他流不会收到影响。因此QUIC连接上的流是相互独立的,即使发生丢包也不会影响其他流。更快的连接建立
对于HTTP/1 和 HTTP/2,TCP和TLS是分层的,他们分别属于内核实现的传输层、openssl库实现的表示层。所以他们非常难以合并在一起。
HTTP/3在传输数据前虽然需要QUIC协议握手,但这个握手过程只需要1RTT,目的是为了确认双方的连接ID。同时QUIC内部包含了TLS,因此他会在自己的帧里携带TLS记录。因此只要1个RTT就可以建立连接同时完成密钥协商。连接迁移
由于基于TCP的HTTP协议是通过(源地址,源端口,目的地址,目的端口)这个四元组确定一条TCP连接的,当网络发生变化时,就必须断开连接然后重新建立连接。期间发生的TCP三次握手,SSL四次握手的时延以及TCP慢启动的减速都会降低用户体验。
但对于QUIC来说,由于他确定连接的方式是通过连接ID,所以即使网络环境发生变化,只要连接上下文(连接ID,TLS密钥等)不变,就可以快速复用原连接,消除重连的成本。
HTTP/1.1优化
优化思路:
- 尽量避免发送HTTP请求
- 在需要发送HTTP请求时,尽量减少请求次数
- 减少HTTP响应的数据大小
第一种思路可以通过缓存实现,具体的介绍在HTTP缓存一节[[#^3b171b]]
减少HTTP请求次数
切入点:
- 减少重定向请求次数
由于服务器上的资源可能由于迁移或维护等等原因,将资源转移到另一个URL后,客户端不知情,仍旧请求原先的URL。这时服务端就需要通过302状态码和Location头部告诉客户端资源已经迁移,客户端转而向新的URL发起请求。
因此,如果重定向请求过多,就会导致客户端为请求一个资源发起多次请求,大大拉低网络性能。此外,客户端的请求一般需要先经过代理服务器然后到达源服务器,这就导致重定向请求在网络中的传递次数翻了一倍。
为了减少重定向的请求次数,我们可以让代理服务器帮助客户端去请求重定向之后的URL,而不是将302的响应交给客户端让它重发一次响应。并且,在代理服务器知晓了重定向规则之后,就可以将客户端给的旧URL地址映射到新的URL地址上,这样就可以直接略过重定向这一步骤。
合并请求
由于HTTP/1.1是请求响应模型,如果一个请求未收到响应就会导致阻塞。因此为了防止单个请求的阻塞,浏览器一般会同时发起多个请求,每个请求都是不同的TCP连接。因此如果我们能够合并请求,就可以减少TCP连接的数量,省去TCP握手和慢启动过程的耗时。
具体的做法包括:
(1) 部分网页会包含大量的小图片,图标。对于这种资源,单开一个TCP连接显然是非常浪费的。对于这种情况,我们可以将这些小图片合成一张大图片,浏览器用一次请求获取这张大图然后根据CSS数据切割大图片
(2) 将js、css等资源打包成大文件
(3) 将图片的二进制数据使用base64编码后,以URL的形式直接嵌入HTML文件,跟随HTML一起发送。延迟发送请求
请求网页的时候,没必要把全部资源都获取到,而是只获取当前用户所看到的页面资源,当用户向下滑动页面的时候,再向服务器获取接下来的资源,这样就达到了延迟发送请求的效果。
- 标题: 计算机网络
- 作者: Zephyr
- 创建于 : 2022-10-09 21:17:31
- 更新于 : 2023-03-13 15:32:43
- 链接: https://faustpromaxpx.github.io/2022/10/09/network/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。