浏览器系列之从 URL 输入到页面展现发生了什么,详解 TCP 三次握手和四次挥手
yuanyuanbyte opened this issue · 0 comments
本系列的主题是浏览器,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末。
如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。
前言
很多大公司面试喜欢问这样一道面试题,输入URL到看见页面发生了什么?今天我们来总结一下。
总体来说分为以下几个过程:
- 浏览器缓存
- DNS 解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
浏览器缓存
浏览器会判断所请求的资源是否有对应 URL 的缓存,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
根据是否需要重新向服务器发起请求来分类,浏览器缓存分为强缓存和协商缓存。
详细的浏览器缓存策略:304 状态码是什么意思?图解 HTTP 强缓存和协商缓存
域名解析(DNS)
在浏览器输入网址后,首先要经过域名解析,因为浏览器并不能直接通过域名找到对应的服务器,而是要通过 IP 地址。
DNS解析实际上就是寻找你所需要的资源的过程。假设你输入www.baidu.com,而这个网址并不是百度的真实地址,互联网中每一台机器都有唯一标识的IP地址,这个才是关键,但是它不好记,乱七八糟一串数字谁记得住啊,所以就需要一个网址和IP地址的转换,也就是DNS解析。
浏览器如何通过域名去查询 URL 对应的 IP 呢
- 浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。
- 操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。
- 路由缓存:路由器也有 DNS 缓存。
- ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的DNS 服务器应对 DNS 查询请求。
- 根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器.com 域名服务器的 IP 地址,然后再问.baidu 域名服务器,依次类推)
小结
浏览器通过向 DNS 服务器发送域名,DNS 服务器查询到与域名相对应的 IP 地址,然后返回给浏览器,浏览器再将 IP 地址打在协议上,同时请求参数也会在协议搭载,然后一并发送给对应的服务器。接下来介绍向服务器发送 HTTP 请求阶段,HTTP 请求分为三个部分:TCP 三次握手、http 请求响应信息、关闭 TCP 连接。
TCP 三次握手
在客户端发送数据之前会发起 TCP 三次握手用以同步客户端和服务端的序列号和确认号,并交换 TCP 窗口大小信息。
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息
。
TCP 三次握手的过程
刚开始客户端处于 Closed
的状态,服务端处于 Listen
状态。 进行三次握手:
-
第一次握手:客户端给服务端发一个
SYN
报文,并指明客户端的初始化序列号ISN(c)
。此时客户端处于SYN_SEND
状态。(第一次握手,由浏览器发起,告诉服务器我要发送请求了)首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
-
第二次握手:服务器收到客户端的
SYN
报文之后,会以自己的SYN
报文作为应答,并且也是指定了自己的初始化序列号ISN(s)
。同时会把客户端的ISN + 1
作为ACK
的值,表示自己已经收到了客户端的SYN
,此时服务器处于SYN_RCVD
的状态。(第二次握手,由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧)在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
-
第三次握手:客户端收到
SYN
报文之后,会发送一个ACK
报文,当然,也是一样把服务器的ISN + 1
作为ACK
的值,表示已经收到了服务端的SYN
报文,此时客户端处于ESTABLISHED
状态。服务器收到ACK
报文之后,也处于ESTABLISHED
状态,此时,双方已建立起了连接。(第三次握手,由浏览器发送,告诉服务器,我马上就发了,准备接收吧)确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
ESTABLISHED :表示TCP连接已经成功建立
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
为什么会采用三次握手,若采用两次握手可以吗? 四次呢?
“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。——谢希仁著《计算机网络》
PS:失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』。
若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入TCP连接成功状态
,但是服务端在收到连接请求后就进入TCP连接成功状态
。如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。服务端正确接收并确认应答,双方便建立了连接开始通信,通信结束后释放连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在 某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端 ,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,由于只有两次握手,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
为什么不是四次握手呢? 大家应该知道通信中著名的蓝军红军约定, 这个例子说明, 通信不可能100%可靠, 而上面的三次握手已经做好了通信的准备工作, 再增加握手, 并不能显著提高可靠性, 而且也没有必要。
红军协同对抗蓝军问题
在网络协议中,有这样一个经典问题:红军协同对抗蓝军问题
处于两地的红军A与红军B要与蓝军作战,但单独的红军A或红军B打不过蓝军,而红军A与红军B联合对抗蓝军则100%取得胜利。
于是红军A与红军B需要商议在何时进攻,但由于无线网络信号质量很差,无法确保红A与红B发出的消息能够送达对方,在此情境下,能否设计出一种可靠的通信协议使得红军一定取得胜利(即通信信道不完全可靠的情况下,设计出完全可靠的通信协议)。
分析:
请求确认
假定红A计划与红B在次日凌晨2点共同向蓝军发起攻击,红A必定要向B发送请求进攻报文“次日2点进攻蓝军”,但是由于通信信道的不可靠性,红B必须向红A发送一个确认报文。
在这种协议下,对红A来说,是否发动攻击取决于有没有收到B的确认报文,而对于红B来说,是否发动攻击取决于有没有收到红A的请求进攻信号,如图所示:
而在该协议中红B并不知道红A有没有收到确认报文,假如红B的确认报文丢失,红A只能等待,而红B单独进攻蓝军,最后失败告终。
确认的确认
为了解决该问题,即需要让红B知道红A已收到确认报文,在原来协议的基础上增加:红A收到确认报文后向红B发送“确认的确认”。
在这种协议下,对红A来说,收到红B的确认报文后决定发起进攻,而对红B来说,在收到“确认的确认”报文后决定发起进攻。
但实际上“确认的确认”报文也可能丢失,而红A并不知道B是否收到了“确认的确认”,因此,如果“确认的确认”丢失,会导致红A单独作战。
……
为解决以上问题,需红B再次发送对“确认的确认”的确认报文,但这同样会导致相同的问题,无限循环下去。
总结
在不可靠通信信道上无法设计出一种完全可靠的通信协议,因为对最后一次确认报文的发送,发送方无法知晓接收方是否收到,因而发送方无法判定约定是否有效。
发送 HTTP 请求
TCP
链接建立后发送HTTP
请求
请求报文由请求行(request line)、请求头(header)、请求体四个部分组成,如下图所示:
请求行包含请求方法、URL、协议版本
- 请求方法包含 8 种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。
- URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
- 协议版本即 http 版本号
POST /chapter17/user.html HTTP/1.1
请求行:
服务器响应 HTTP 请求
服务器响应 HTTP 请求并返回 HTTP 报文。
服务器是网络环境中的高性能计算机,它侦听网络上的其他计算机(客户机)提交的服务请求,并提供相应的服务,比如网页服务、文件下载服务、邮件服务、视频服务。而客户端主要的功能是浏览网页、看视频、听音乐等等,两者截然不同。 每台服务器上都会安装处理请求的应用——web server。常见的 web server 产品有 apache、nginx、IIS 或 Lighttpd 等。
响应报文由响应行(response line)、响应头部(header)、响应主体三个部分组成。如下图所示:
HTTP/1.1 200 OK // 状态行
状态行:
根据请求类型的不同,响应的数据格式也有所不同,有可能是二进制文件流、JSON 对象、字符串、HTML 文件等。
浏览器解析渲染页面
浏览器拿到响应文本 HTML 后,接下来介绍下浏览器渲染机制
浏览器解析渲染页面分为一下五个步骤:
- 根据 HTML 解析出 DOM 树
- 根据 CSS 解析生成 CSS 规则树
- 结合 DOM 树和 CSS 规则树,生成渲染树
- 根据渲染树计算每一个节点的信息
- 根据计算好的信息绘制页面
根据 HTML 解析 DOM 树
- 根据 HTML 的内容,将标签按照结构解析成为 DOM 树,DOM 树解析的过程是一个深度优先遍历。即先构建当前节点及其所有子节点,再构建下一个兄弟节点。
- 在读取 HTML 文档,构建 DOM 树的过程中,若遇到 script 标签,则 DOM 树的构建会暂停,直至脚本执行完毕。
深度优先遍历,顾名思义即是先抓着一个元素一直往下遍历它的子孙元素,直到没有未被遍历的元素时则换下一个元素继续往下遍历。当全部元素都遍历完了就结束。
根据 CSS 解析生成 CSS 规则树
- 解析 CSS 规则树时 js 执行将暂停,直至 CSS 规则树就绪。
- 浏览器在 CSS 规则树生成之前不会进行渲染。
结合 DOM 树和 CSS 规则树,生成渲染树
- DOM 树和 CSS 规则树全部准备好了以后,浏览器才会开始构建渲染树。
- 精简 CSS 并可以加快 CSS 规则树的构建,从而加快页面相应速度。
根据渲染树计算每一个节点的信息(布局)
- 布局:通过渲染树中渲染对象的信息,计算出每一个渲染对象的位置和尺寸
- 回流:在布局完成后,发现了某个部分发生了变化影响了布局,那就需要倒回去重新渲染。
根据计算好的信息绘制页面
- 绘制阶段,系统会遍历呈现树,并调用呈现器的“paint”方法,将呈现器的内容显示在屏幕上。
- 重绘:某个元素的背景颜色,文字颜色等,不影响元素周围或内部布局的属性,将只会引起浏览器的重绘。
- 回流:某个元素的尺寸发生了变化,则需重新计算渲染树,重新渲染。
TCP 四次挥手断开连接
TCP 四次挥手
当数据传送完毕,需要断开 tcp 连接,此时发起 tcp 四次挥手。
建立一个连接需要三次握手,而终止一个连接要经过四次挥手。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端 在结束它的发送后 还能接收来自另一端数据的能力。
注:
FIN 表示关闭连接
ACK 表示确认
MSL
是TCP报文里面最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。
刚开始双方都处于 TCP连接成功状态
,客户端或服务器均可主动发起挥手动作,假如是客户端先发起关闭请求。四次挥手的过程如下:
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于
FIN_WAIT1
状态。即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1
(终止等待1)状态,等待服务端的确认。 - 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于
CLOSE_WAIT
状态。即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT
(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2
(终止等待2)状态,等待服务端发出的连接释放报文段。 - 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于
LAST_ACK
的状态。即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK
(最后确认)状态,等待客户端的确认。 - 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于
TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入CLOSED
状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于CLOSED
状态。即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT
(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL
后,客户端才进入CLOSED
状态。
TCP 状态变迁图
一个连接从开始建立到断开,经历了一连串的状态变化。
这次主要分析下它的状态变迁图,粗的实线箭头表示正常的客户端状态变迁,粗的虚线箭头表示正常的服务器状态变迁:
- CLOSED:初始状态,表示TCP连接是“关闭着的”或“未打开的”
- LISTEN :表示服务器端的某个SOCKET处于监听状态,可以接受客户端的连接
- SYN_SENT:表示客户端已发送SYN报文,客户端发起连接(主动打开),变成此状态,如果SYN超时,或者服务器不存在直接CLOSED
- SYN_RCVD :表示服务器接收到了来自客户端请求连接的SYN报文
- ESTABLISHED :表示TCP连接已经成功建立
- FIN_WAIT_1:客户端执行主动关闭,发送完FIN包之后便进入FIN_WAIT_1状态
- FIN_WAIT_2:客户端发送FIN包之后,收到ACK,即进入此状态,其实就是半关闭的状态
- TIME_WAIT :表示收到了对方的FIN报文,并发送出了ACK报文。 TIME_WAIT状态下的TCP连接会等待
2MSL
,然后即可回到CLOSED 可用状态了 - CLOSE_WAIT:接收到FIN之后,被动的一方进入此状态,并回复ACK
- LAST_ACK:被动的一端发送FIN包之后 处于
LAST_ACK
状态 - CLOSING:两边同时发出FIN请求
将关闭部分的状态转移摘出来,得到下图:
TCP 的半关闭
牢记 TCP 是 全双工 的。
既然一个TCP连接是全双工(即数据在两个方向上能同时传递,可理解为两个方向相反的独立通道),因此每个方向必须单独地进行关闭。
全双工:意味着,TCP的收发是可以在两个方向上同时进行的。在任意时刻,通信双方既可以发送数据也可以接收数据,每个方向的数据流是独立的。
半关闭:TCP提供了连接的一端 在结束了它的发送后 还能接收来自另外一端数据的能力。但是只有很少的应用程序利用它。
为了实现这个特性,编程接口必须提供一种方法来说明“我已经完成了数据的传送,并且发了FIN给另外一端,但是我还是想接收另外一端发送来的数据,直到结束(向我发送FIN)”。
为什么 TCP 断开连接要四次挥手
我们知道 TCP 采用三次握手策略让发送端和接收端都能确认双方收发功能OK,以此保证可靠传输。
为何断开却要四次?
注:
FIN 表示关闭连接
ACK 表示确认
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭,也就是客户端和服务端分别释放连接的过程,其实是客户端和服务端的两次挥手。
因为当服务端收到客户端的 FIN 数据包后(第一次挥手),服务端不会立即close,为什么不立即close?因为可能还有数据没发完,服务端会先将 ACK 发过去告诉客户端我收到你的断开请求了(第二次挥手),但请再给我一点时间,这段时间用来发送剩下的数据报文,发完之后再将 FIN 包发给客户端表示现在可以断了(第三次挥手)。客户端收到 FIN 包后发送 ACK 确认断开信息给服务端(第四次挥手)。
为什么TIME_WAIT状态需要经过2MSL才能进入CLOSED状态
MSL
是TCP报文里面最大生存时间,超过这个时间报文将被丢弃。
理论上,四个报文都发送完毕,就可以直接进入CLOSED
状态了,但是网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT
状态就是用来重发可能丢失的ACK报文。
为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK
状态的服务器收不到对连接释放报文段(FIN-ACK)的确认报文(ACK)。这个时候服务器会超时重传这个连接释放报文段(FIN-ACK),接着客户端再重传一次确认,重新启动2MSL
时间等待计时器,最后客户端和服务端都进入到CLOSED
状态,都能正常的关闭。假如客户端在TIME-WAIT
状态不等待2MSL,而是在发送完确认报文(ACK)之后立即释放连接,一旦这个确认报文(ACK)丢失的话,则无法收到服务端重传的连接释放报文段(FIN-ACK),所以不会再发送一次确认报文,服务器收不到 确认报文 就无法正常的进入CLOSED
关闭连接状态。
参考
- https://juejin.cn/post/6844903958624878606
- https://segmentfault.com/a/1190000017184701
- ljianshu/Blog#8
- https://github.com/ljianshu/Blog
- https://juejin.cn/post/6844903832435032072#heading-3
- https://juejin.cn/post/6844903958624878606#heading-7
- https://www.cnblogs.com/qq952693358/p/5766887.html
- https://www.cnblogs.com/yuerdongni/p/12828335.html
- https://www.cnblogs.com/yorkyang/p/7657683.html
- https://zhuanlan.zhihu.com/p/40013850
- https://blog.csdn.net/qq_21586317/article/details/106463935
- https://blog.csdn.net/waisock2017/article/details/78388380
- https://blog.csdn.net/wdscq1234/article/details/52416641
- https://www.jianshu.com/p/9d30976a2d7f
- https://blog.csdn.net/wdscq1234/article/details/52416641
- https://blog.csdn.net/wk_bjut_edu_cn/article/details/82343939
- https://blog.csdn.net/wk_bjut_edu_cn/article/details/82343939
- https://blog.csdn.net/pzqingchong/article/details/53583131
- https://www.cnblogs.com/GuoXinxin/p/11657933.html
博文系列目录
- JavaScript 深入系列
- JavaScript 专题系列
- JavaScript 基础系列
- 网络系列
- 浏览器系列
- Webpack 系列
- Vue 系列
- 性能优化与网络安全系列
- HTML 应知应会系列
- CSS 应知应会系列
交流
各系列文章汇总:https://github.com/yuanyuanbyte/Blog
我是圆圆,一名深耕于前端开发的攻城狮。