计算机网络
计算机网络的分层模型
OSI七层模型
- 应用层:计算机上的网络应用,传输应用报文,如HTTP,SMTP
- 表示层:用于处理交换信息的表示格式(处理语法和处理语义),格式变换,数据加密解密,数据压缩等功能
- 会话层:向表示层建立连接,并传输有序的数据,也就是建立同步SYN
- 运输层:负责两个进程的通信,即端到端,如TCP UDP
- 网络层:讲分组从源端传到目的端,注重传输过程中的路径选择,如IP
- 链路层:讲网络层的数据包封装成帧,如CSMA
- 物理层:以比特流的方式传输
上面4层是端到端的,也就是关注的是数据从源主机交付到目的主机,而不管每步是怎么传输的
下面3层是点到点的,关注数据在传输过程中下一步是怎么走的,也就是路由是如何转发的
TCP/IP四层模型
- 应用层(应用层+表示层+会话层)
- 运输层
- 网络层
- 网络接口层(物理层+链路层)
五层参考模型
- 应用层
- 运输层
- 网络层
- 链路层
- 物理层
UDP
特点
- 无连接状态:UDP无需任何准备就可以传输数据,也没有引入建立连接的时延。并且UDP也不维护连接状态
- 精确控制发送数据的时机:应用程序将数据传给UDP,UDP可以立即将数据封装为UDP报文并传递给网络层,而TCP引入了拥塞机制来控制发送方的频率。
报文结构:
UDP首部仅四个部分
- 源端口号:2字节
- 目的端口号:2字节
- 长度:2字节
- 校验和:2字节
UDP的校验和:应用数据的所有16比特字之和的反码
TCP详解
特征:工作在运输层,为应用层序提供可靠传输服务,确保网络包无损坏,无间隔,非冗余,按序排列
TCP首部的控制位字段部分
- ACK=1 表明确认字段有效
- RST=1 表示TCP连接出现严重错误(比如期望确认号与实际收到的确认号不同),必须释放连接,再重新建立连接
- SYN=1 表示这是一个连接请求或者连接接受报文
- FIN=1 表示此段数据的发送端已经发送完毕最后一个数据段,并要求释放连接
TCP的有限状态机
三次握手:
- 一开始,客户端和服务端都处于
CLOSED
状态。先是服务端主动监听某个端口,处于LISTEN
状态 - 客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。 - 服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。 - 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于ESTABLISHED
状态。第三次握手是可以携带数据的,前两次握手是不可以携带数据的 - 服务端收到客户端的应答报文后,也进入
ESTABLISHED
状态。
SYN :Synchronize Sequence Numbers 即握手信号
为什么需要三次握手
- 避免历史连接:由于网络阻塞导致或者客户端宕机导致发出两个SYN不同的连接报文,若只有两次握手,服务端会在收到第一次SYN报文后就进入ESTABLISHED状态发送数据,这就是历史连接。而真正有效的连接其实是第二次,所以第一次发送的数据就白白浪费了。如果有三次握手,相当于是给了服务器一个缓冲确认机会,确保没有历史连接。
- 同步双方序列号:最少三次握手,才能确保双方的初始序列号都被确认收到,从而建立可靠的同步
- 避免资源浪费:如果只有两次握手,服务器在收到SYN报文后就进入ESTABLISHED状态发送数据,而发送的数据不一定能保证能被客户端收到,所以三次握手可以确保双方都可以正确接收数据,避免浪费
为什么每建立一个新的连接都需要重新指定初始化序列号
如果没有重新指定序列号而是使用与上次连接一样的序列号,那么因为网络阻塞而延迟到达的历史报文就可能被新建立的连接正常接收,从而接收到错误的消息。计算机会基于一个算法来随机生成序列号,这个序列号会根据计时器递增,所以基本不可能出现“撞序列号”的情况。
为什么要为TCP设立MSS?
MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度
IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。所以如果分片只依靠IP数据报的MTU,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。由 IP 层进行分片传输,是非常没有效率的。为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。
握手过程中的超时重传
- 第一次握手丢失:如果客户端迟迟收不到SYN-ACK报文,证明第一次的握手报文丢失,需要进行超时重传,重传的 SYN 报文的序列号都是一样的,使用指数后退机制,每次超时重传时间是上一次的2倍,最后一次的等待时间是32秒,之后客户端会断开连接
- 第二次握手丢失:如果服务器收到了第一次握手报文,但是服务器发送的SYN-ACK报文丢失了。此时就会出现一个比较复杂的情况:客户端会觉得自己第一次握手丢失,服务器因为收不到第三次握手会认为自己的第二次握手丢失了。所以双方都会触发重传,如果其中一方的重传次数超过最大次数,就会断开连接。
- 第三次握手丢失:当客户端发出最后一个ACK报文后,就会进入ESTABLISHED状态。但是当服务器迟迟没有收到时,就会触发重传,重传第二次握手报文,直到握手成功或者超过最大次数。ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文,所以这里是由服务器来重传而不是客户端
四次挥手
- 客户端打算关闭连接,此时会发送一个 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
报文给客户端来表示同意现在关闭连接
简而言之就是在FIN_WAIT2状态下,客户端仍然需要处理网路中未收取完的数据,所以需要多确认一次来确保双方都已经完成数据的收发,可以彻底关闭连接
握手过程中的超时重传
- 第一次挥手丢失:当客户端没有接收到服务器的ACK报文时,就无法进入FIN_WAIT2状态,所以就会进行重传,同样的指数后退机制,直到超过阈值,客户端会直接断开连接
- 第二次挥手丢失:如果服务器的第二次握手丢失了,由于ACK报文是不会重传的,所以这次还是由客户端重传,超过阈值时,会直接断开连接
- 第三次挥手丢失:在FIN_WAIT2阶段,当服务器发完所有数据后,便会发送FIN报文,若该报文丢失,服务端就会重发 FIN 报文,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
- 第四次挥手丢失:当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入
TIME_WAIT
状态,但是若服务器迟迟没有收到ACK报文,就会一直处于LAST_ACK阶段,由于ACK报文不会重传,服务端就会重传FIN报文。当然由于TIME_WAIT阶段时长为2MSL,所以超过这个时间便会断开连接
特别注意,如果客户端是由close函数关闭的,那么其处于FIN_WAIT2的时长是有限制的,如果迟迟未收到服务器的FIN报文便会自动断开连接
为什么TIME_WAIT的时长设置为2MSL
MSL:即报文最大生存时间
MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡
TIME_WAIT 等待 2 倍的 MSL,因为一来一回需要等待 2 倍的时间。比如,如果服务器没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN
报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
为什么不是 4 或者 8 MSL 的时长:如果有一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率其实也只有万分之一,这个概率实在是太小了,2倍是一个折中的选择。如果TIME_WAIT过长,会占用大量网络IO资源。
为什么我们需要TIME_WAIT
- 防止历史连接的数据,被新的连接错误接收:因为TIME_WAIT时间设置为2MSL,所以可以保证历史连接的数据报都消失,再出现的数据报都一定是新连接产生的
- 保证服务端能被正确的关闭,TIME_WAIT的作用就是等待足够的时间以确保最后的ACK可以被服务器收到,总而帮助其正常关闭
TCP的有限状态机
- CLOSED 没有任何连接状态
- LISTEN 侦听状态,等待来自远方TCP端口的连接请求
- SYN-SENT 在发送连接请求后,等待对方确认
- SYN-RECEIVED 在收到和发送一个连接请求后,等待对方确认
- ESTABLISHED 代表传输连接建立,双方进入数据传送状态
- FIN-WAIT-1 主动关闭,主机已发送关闭连接请求,等待对方确认
- FIN-WAIT-2 主动关闭,主机已收到对方关闭传输连接确认,等待对方发送关闭传输连接请求
- TIME-WAIT 完成双向传输连接关闭,等待所有分组消失
- CLOSE-WAIT 被动关闭,收到对方发来的关闭连接请求,并已确认
- LAST-ACK 被动关闭,等待最后一个关闭传输连接确认,并等待所有分组消失
- CLOSING 如果通信双方同时发送FIN数据包,则同时进行关闭操作,则双方将同时进入TCP_CLOSING状态。 具体的,本地发送一个FIN数据包以结束本地数据包发送,如果在等待应答期间,接收到远端发送的FIN数据包,则本地将状态设置为TCP_CLOSING状态。 在接收到应答后,再继续装入到TCP_CLOSE_WAIT状态。
TCP可靠传输机制
校验和
在数据传输的过程中,将发送的数据段以16位的整数循环进位相加。
前面的进位不能丢弃,补在后面,最后取反,得到校验和,存入TCP报文段的相应位置
序列号和ACK/NAK
TCP传输时将每个字节的数据都进行了编号,这就是序列号,可以检测出丢失分组以及冗余分组。
使用ACK/NAK来确认对方是否收到了数据
超时重传
在进行TCP传输时,由于确认应答与序列号机制,当发送方发送一部分数据后,都会等待接收方的ACK报文。
如果迟迟没有等到应答报文,就会启动超时重传,简单理解就是发送方在发送完数据后会启动计时器,如果timeout,那么对刚才发送的数据进行重新发送。
如果是因为网络原因导致发送方数据丢失,重传被成功接收,便进行ACK应答。
如果是因为网络原因导致接收方ACK报文丢失,接收方发现接收的数据已存在(判断存在的根据就是序列号),那么直接丢弃,仍旧发送ACK应答。
选择性重传SR selective repeat
发送方只重发没有确认的分组
为每一个分组都添加timer,缓冲区的存在可以暂存失序但正确的分组
待重传分组到达并恢复顺序后,再统一交付给上层
GBN 回退N步 go-back-N
将序号队列分为四部分
- 已被确认
- 窗口内:已发送,未被确认
- 窗口内:可用,未发送
- 不可用
若窗口内某一序号n的ACK timeout,则需要重传n之后的所有分组
当接收到正确序号的ACK时,窗口便前移一个序号
0123 +rcv ack0 => 1234
1234 +rcv ack1 => 2345
ack2 timeout =>re_send (2345)
接受方无buffer,失序分组会被丢弃,所以需要重发来重新确认顺序
连接管理
三次握手和四次挥手
滑动窗口和流量控制
如果发送端的发送速度太快,导致接收端的结束缓冲区很快的填充满了,此时如果发送端仍旧发送数据,那么接下来发送的数据都会丢包。而TCP根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
在TCP协议的报头信息当中,有一个16位字段的窗口大小。窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。
而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。
拥塞控制
防止过多的数据注入到网络中,这样可以使网络中的路由器或链路过载,如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。
因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收(端对端的通信),而拥塞控制是为了降低整个网络的拥塞程度(全局性)
cwnd:拥塞窗口
ssthresh:拥塞阈值
- 当cwnd < ssthresh时,慢启动,指数增长
- 当cwnd > ssthresh时,拥塞避免,线性增长
- 出现三个冗余确认,快速恢复,ssthresh = cwnd/2 ; cwnd = ssthresh+3*MSS;此后每收到一个冗余ACK就增加一个MSS,直到收到正确的ACK,cwnd=ssthresh,进入慢增长
- 出现timeout,ssthresh = cwnd/2 ; cwnd = 1MSS
TCP协议存在什么缺陷?
- 存在队头阻塞的问题,必须保证缓存内的数据报全部到达且有序,才能向上层交付,否则会被一直阻塞
- 网络迁移时需要重新进行握手建立连接
- 连接建立需要较长的时延
TCP和UDP的区别
- 通信即时性:UDP协议的双方随时都可以进行通信,而TCP协议的双方必须经过三次握手后才能通信,并且要经过四次挥手才能断开连接
- 对象不同:UDP是面向报文的,接收来自应用层的数据直接加上首部就发送,数据是有边界的,而TCP是将应用层的数据看作字节流,为其设立缓存,没有边界,保证顺序和可靠。
- 通信数量不同:UDP支持单播,多播,广播;TCP只支持单播
- 数据的安全性不同:网络层以上提供的都是不可靠的传输协议,UDP也是。而TCP保证了可靠传输,解决了丢失,乱序等问题
- 报文大小不同:UDP首部结构简单,8字节;而TCP报文首部有20字节,最大可达60字节。
如何使UDP可靠
运输层不能改变,网络层及下层都无法保证可靠,所以需要从应用层来保证可靠
核心出发点:在应用层模仿TCP的可靠性传输
如:
- 添加添加seq/ack机制,确保数据发送到对端
- 添加发送和接收缓冲区,主要是用户超时重传
- 添加超时重传机制
送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据。
只不过都是由应用层的软件来实现的,比如使用UDP数据包+序列号,UDP数据包+时间戳等方法,在服务器端进行应答确认机制。
比如RUDP或者RTP都是基于UDP实现的可靠传输协议。
QUIC协议
HTTP3弃用TCP后,使用了基于UDP的QUIC协议,使用UDP实现了TCP+TLS的特性,且仅需一次握手即可建立可靠连接(0-RTT 握手)
QUIC协议基本解决了TCP协议存在的缺陷
如何实现可靠传输
- 包号PKN+确认应答SACK来保证确认接收和数据有序性
- 滑动窗口:应用层实现
- 拥塞控制:应用层实现
应用层实现的功能可以拥有很快的更新速度,因为可以随浏览器更新,而不会受到操作系统内核的限制
如何避免队头阻塞
由于TCP为了保证数据的有序到达,只有保证缓冲区数据有序,窗口才能滑动,否则只能重传并等待。停留「发送窗口」会使得发送方无法继续发送数据;停留「接收窗口」会使得应用层无法读取新的数据。
HTTP/2 通过抽象出 Stream 的概念,实现了 HTTP 并发传输,一个 Stream 就代表 HTTP/1.1 里的请求和响应,多个Stream使用一个TCP连接,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。
但是 由于HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,其实还是没有跳出滑动窗口的逻辑,当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。
QUIC 也借鉴 HTTP/2 里的 Stream 的概念,但是 QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。
如何实现更快的连接建立和连接迁移
由于此前TCP和TLS属于不同的协议,所以需要分批次来握手
而QUIC 协议并不是与 TLS 分层,而是内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果
基于TCP+TLS的传输协议在IP地址发生改变时,需要断开连接并重新建立连接,迁移成本较高。
而QUIC协议会为客户端和服务器指定连接ID,因此即使 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,达到了连接迁移的功能。
为什么QQ使用的是UDP协议
登陆和保持连接状态采用TCP,和好友之间发送消息采用UDP,内网传文件采用了P2P
- 当时没有epoll这种可以支持成千上万tcp并发连接的技术,所以使用了应用层封装后的UDP来解决大并发的问题。后面也懒得修改
- 国内网络环境水平参差复杂,特别是在千禧年,带宽窄且抖动厉害,此时TCP的等待握手反而会成为劣势,占用宝贵的时间资源和性能资源
- UDP的特性,即时封装即时发送,所以在在应用层的控制下,可以更快地探测和重传。
QQ如何实现可靠:使用上层协议来实现可靠,如果客户端使用UDP协议发出消息后,服务器收到该包,需要使用UDP协议发回一个应答包。如此来保证消息可以无遗漏传输
TCP的进程安全问题
TCP保活机制:
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,没有响应,连续几次,达到保活探测次数后,此时可以认为该 TCP 连接已经死亡。
所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活,这个工作是在内核完成的。
主机崩溃
如果没有开启保活机制,客户端主机崩溃了,服务端是无法感知到的,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。
进程崩溃
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。
所以,即使没有开启 TCP keepalive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。
主机宕机,又重启
在客户端主机宕机后,服务端向客户端发送的报文会得不到任何的响应,在一定时长后,服务端就会触发超时重传机制,重传未得到响应的报文。服务端重传报文的过程中,若客户端主机重启完成,客户端的内核就会接收重传的报文,然后根据报文的信息传递给对应的进程:
- 如果客户端主机上没有进程绑定该 TCP 报文的目标端口号,那么客户端内核就会回复 RST 报文,重置该 TCP 连接;
- 如果客户端主机上有进程绑定该 TCP 报文的目标端口号,由于客户端主机重启后,之前的 TCP 连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体,于是就会回复 RST 报文,重置该 TCP 连接。
所以,只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接。
拔掉网线
TCP 连接在 Linux 内核中是一个名为 struct socket
的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。
- 拔掉网线后,没有数据传输:如果没有开启 TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。而如果开启了 TCP keepalive 机制,在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送探测报文。
- 拔掉网线后,有数据传输:在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文。如果在服务端重传报文的过程中,客户端刚好把网线插回去了,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文,就感觉什么事情都没有发生。但是,如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。
HTTP协议
超文本传输协议,基于TCP实现
特点
- 可靠
- 简单快速:客户向服务器请求服务时,只需传输方法和路径
- 灵活:支持任意类型的数据
- 无状态,无持久化
请求指令
- GET:从服务器获取一个资源
- PUT:将来自客户端的资源存储到服务器中
- POST:将客户端数据发送到服务器应用程序中去
- DELETE:从服务器中删除资源
- HEAD:仅发送HTTP首部
GET和POST的区别:
- get通过URL传输数据,比如以字段=value的形式,以?和&连接。传输少量数据,URL是可见的,可能会泄漏信息。
- POST可以传输大量数据,且支持标准字符集,可以正确传输中文字符
- 前者着重于获取资源,后者着重于发送数据
状态码
响应报文都会携带一个状态码来告知请求报文的状态
- 200:正确返回
- 302:重定向
- 404:没找到
报文
请求报文
- 起始行:请求指令,URL,HTTP版本号
- 首部:描述浏览器可以接受的字符集,编码方式,期望的语言
- 主体:可能会有,也可能没有
响应报文
- 起始行:版本号,状态码
- 首部:返回的数据类型,长度等信息
- 主体:二进制流
Cookie
身份标识:向服务器表明自己的身份
Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
Cookie存储的数据量有限,且都是保存在客户端浏览器中,大小一般不超过4kb
在服务器发送响应后,会顺带将Set-Cookie也发送给客户端。
当客户端保存后,之后给服务器发送请求时,都会在请求中包含Cookie的头部
应用:判断用户是否已经登录网站,购物车
Session
当用户登录时,发送用户名和密码后,服务端查询数据库是否存在该用户,如果有的话,会自动生成一个sessionid,用于记录登录的时间、状态、属于哪个用户,过期时间等信息,并将这些信息保存在服务端
同时,将这些信息通过cookie的形式将这些数据返回给客户端
当用户再次登录该服务器,访问其他接口的时候,会自动带上sessionid,服务器接收到请求后会自动查询有没有存储这个sessionid的信息
Cookie和Session的区别:
- Session因为存储在服务器上,所以安全性比Cookie更高。
- Cookie中只能存储ASCII字符串,如果是略微复杂的信息如java 对象,unicode字符串,比较艰难,需要进行编码,而session可以存取任意类型的数据,包括java对象
- 隐私策略不同:cookie对客户端可见,客户端的程序是可以窥探到cookie的。而session对客户端是不可见的。
- cookie的过期时间可以设置得很长,而session因为存储在服务器上,出于性能的考虑,不能将存活时间设置得太长
- 当并发量高的时候,session的资源消耗会很高,而cookie就不会给服务器造成太大压力
存在的问题
- 存储在服务器,消耗大量的存储资源
- 查询速度会成为瓶颈,导致响应速度慢
- 在跨端、跨服务器时,需要session同步
- 通过架设数据库集群redis,会导致维护成本高,配置复杂
Token
服务器不存储用户数据,而是直接通过加密的方式把用户数据通过令牌的方式返回给客户端,该令牌将会由服务器自己设置。
每次用户访问时,都会携带这个令牌,用来证明自己的身份,从而得到自己的状态和数据。
服务器不需要存储用户资源导致资源占用过多的问题,也不需要每次查询从而加快了响应速度,而且传递的方式也由双方协定,不管是否跨域,都可以正常传递。
但是,这种token容易被伪造,因为只要任何人拿到了这种令牌都可以称自己是合法的用户
从而获取一些私密的信息,此时如果服务器能拥有某种方式使得能证明该用户是合法的就显得极为重要
sign:由服务端进行设置,且只有服务器知道签名和密钥。
当用户登录时,服务器提取将用户信息(payload)和header组成新的数据,然后再加上sign进行加密得到一个token。
当用户发起请求后,由服务器对sign进行解密,然后再结合自己设置的sign进行对比,如果一致,就证明该token合法,如果不一致,该token就是非法的
JWT
是token的一个实现形式,全称为JSON Web Token,本质是一个字符串,他将用户的信息保存到一个json字符串中,然后进行编码后得到一个JWT token
,并且这个JWT token
带有签名信息,接收后可以校验是否被篡改
JWT的优势:
- 数据量小,传输速度快
- 以JSON加密保存,跨语言
- 不依赖于cookie和session,适合于分布式微服务
JWT的结构
header
头部是一个描述JWT元数据的JSON对象
1 |
|
Payload
有效载荷,提供七个可选字段:
iss
:发行人 exp
:到期时间 sub
:主题 aud
:用户 nbf
:在此之前不可用 iat
:发布时间 jti
:JWT ID用于标识该JWT
1 |
|
Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密钥(secret)。该密钥仅仅为保存在服务器中,并且不能向用户公开。
然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
1 |
|
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.
分隔,就构成整个JWT对象
注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。
服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的签名是否一致,注意secretKey只能保存在服务端。
对于不同的加密算法secretKey含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值
HTTP1.0和HTTP1.1的区别
长连接
HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启长连接keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
节约带宽
HTTP1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。HTTP1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。
HOST域
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名,HTTP1.0没有host域。随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误(400 Bad Request)。
缓存处理
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
错误通知的管理
在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
HTTP1.1和HTTP2.0的区别
多路复用
HTTP2引入了帧和流的概念,将数据分成一个个的二进制形式的帧。帧上面除了 HTTP 数据,还包含数据长度、流标识符、帧类型等信息。而流就是一个建立连接后的双向的虚拟字节流,流具有并行性,即二进制帧都是并行传输的,无需按序等待。HTTP/2 会将所有 HTTP 请求打散成帧,在一个 TCP 连接上做并发请求,充分利用 TCP 带宽。避免了队头阻塞。
头部数据压缩
在HTTP1.1中,HTTP请求和响应都是由状态行、请求/响应头部、消息主体三部分组成。一般而言,消息主体都会经过gzip压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。随着Web功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越大,HTTP2.0对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。
服务器推送
服务端推送是一种在客户端请求之前发送数据的机制。网页使用了许多资源:HTML、样式表、脚本、图片等等。在HTTP1.1中这些资源每一个都必须明确地请求。这是一个很慢的过程。为了改善延迟,HTTP2.0引入了server push,它允许服务端推送资源给浏览器,免得客户端再次创建连接发送请求到服务器端获取。这样客户端可以直接从本地加载这些资源,不用再通过网络。
HTTPS
HTTP+SSL,是可加密的,身份认证的网络协议,更加安全
SSL/TLS
HTTP的缺点:报文都是明文的,始终可见,安全性低
对称加密:双方使用同一种方式来加密和解密,如果加密的规则被破解,那么就不存在密文了
非对称加密:公钥加密后,必须由私钥解密;反之必须由公钥解密。
SSL证书:由CA颁发,拥有SSL证书的服务器就可以向客户端提供公钥,支持HTTPS连接
计算机输入URL后会发生什么
应用层
客户端发起请求后,浏览器解析域名。首先浏览器会查看本地磁盘的host文件(已被舍弃),是否有对应的IP地址,如果有就直接使用
如果没有,就会发送一个DNS请求给本地的DNS服务器,本地DNS服务器会查询缓存记录,如果缓存存在,就直接返回结果。如果没有,使用迭代查询,本地DNS服务器逐个请求:根服务器->顶级域名服务器->权威域名服务器,最后获得IP地址,存入缓存。获取IP地址后,向目的地址主机的80端口发送请求报文。
运输层
TCP的三次握手,运输层接收应用层的数据并封装为TCP报文,交给网络层
网络层
若该主机为第一次接入该家庭网络或者其他私网,那么在发送TCP报文前还需要借助基于UDP的DHCP协议来获取源IP地址。
DHCP使用以0.0.0.0为源地址,255.255.255.255为广播地址,向67端口发送发现报文。DHCP服务器接收到后便会向客户端做出响应,最终客户端会收到DHCP ACK报文,其中包含IP地址以及租用期。
其中对于家庭网络或者其他私网,为了与广域网中的其他子网进行数据传输,还需要借助NAT协议,即网络地址转换,对外:将子网内的IP地址统一转换为在广域网内通信的IP地址;对内:借助NAT转换表将分组转发给内部主机。
网络层接收TCP报文,将其封装为IP数据报,以IPV4协议为例,IPV4数据报包含了32位的目的和源地址,header总大小为20字节,如果数据报大于了链路层所能承载的最大单元MTU,就要进行数据报分片。
主机通过ICMP协议获取网络层的传输状态,若目的网络不可达,路由器便会向客户端发送类型为3的ICMP报文来指示错误,其中ICMP报文封装在IP数据报中。
网络层使用路由器来实现分组交换:每个路由器内部都会动态维护一张转发表,遵循最长前缀匹配原则来找到输出链路,其中转发路径由路由选择算法来实现。
若使用的是IPV6协议,网络层的整个运输链路只要存在使用IPV4的节点,就需要进行处理,比如将IPV6封装在IPV4数据报内,进行兼容传输。
网络层关注的是主机到主机或者主机到路由器,链路传输的管理就交给链路层。
链路层
IP数据报被包装为链路层帧,帧使用CRC即循环冗余检测来进行差错检测,使用多路访问协议来避免出现帧碰撞。
CRC的原理:多项式编码,双方协商好一个多项式,在数据的二进制编码后加上r个零,并使用异或除法除以该多项式,将获得的余数加在数据后,检测时使用这个多项式异或除,若余数为0,则证明无差错。
多路访问协议:信道划分+随机接入,ALOHA,CSMA
其中链路层的数据传输依靠MAC地址,MAC地址是扁平化的,即一个网卡就对应一个MAC地址,不会改变,也不会动态分配分层,类似于一个人的DNA。IP地址与MAC地址的转换依靠地址解析协议ARP,MAC地址与IP地址的映射存储于ARP表中,当无法查询到映射时,便会使用MAC广播地址发送查询分组。比如若该主机第一次接入该网络,则需要使用广播帧来获取网关路由器的MAC地址
链路层的以太网帧转发依赖于链路层交换机:同样具有转发表,交换机具有自学习的特性,若长时间未收到以表内某个地址作为源地址的帧,就会删除该表项,保证转发表内存放的都是高频率使用的表项。
最终
经过DHCP获取IP地址->ARP查询广播获取网关地址->DNS服务器获取目的IP地址->运输层包装TCP报文->路由选择->TCP三次握手建立可靠连接后
应用便可以向该域名发送HTTP GET报文,最终获取到HTTP响应,应用抽取HTML文件,显示到浏览器上。