基于浏览器一次完整的http请求流程

当我们在web浏览器的地址栏中输入: www.baidu.com,然后回车,到底发生了什么?

过程概览

1.URL解析/DNS解析查找域名IP地址

2.网络连接发起HTTP请求

3.HTTP报文传输过程

4.服务器接收数据

5.服务器响应请求/MVC

6.服务器返回数据

7.客户端接收数据

8.浏览器加载/渲染页面

下面我们来详细看看这几个过程的具体细节

1.URL解析/DNS解析查找域名IP地址

*URL解析

输入 URL 「回车」后,这时浏览器会对 URL 进行检查,浏览器对 URL 进行检查时首先判断协议,如果是 http/https 就按照 Web 来处理,另外还会对 URL 进行安全检查,然后直接调用浏览器内核中的对应方法,接下来是对网络地址进行处理,如果地址不是一个IP地址而是域名则通过DNS(域名系统)将该地址解析成IP地址。IP地址对应着网络上一台计算机,DNS服务器本身也有IP,你的网络设置包含DNS服务器的IP

*DNS解析

a)首先会搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存)

 注:我们怎么查看Chrome自身的缓存?可以使用 chrome://net-internals/#dns 来进行查看

b)如果浏览器自身的缓存里面没有找到,那么浏览器会搜索系统自身的DNS缓存

   注:怎么查看操作系统自身的DNS缓存,Windows下,可以在命令行下使用 ipconfig /displaydns 来进行查看  ,MAC下,可以使用nslookup baidu.com 或者cat /etc/resolv.conf

c)如果还没有找到,那么尝试从 hosts文件里面去找

d)   如果还没有找到,则需要再向上层找路由器缓存

d)在前面四个过程都没获取到的情况下,就递归地去域名服务器去查找,具体过程如下

DNS递归查询和迭代查询的区别

递归查询是以本地名称服务器为中心的,是DNS客户端和服务器之间的查询活动,递归查询的过程中“查询的递交者” 一直在更替,其结果是直接告诉DNS客户端需要查询的网站目标IP地址。

迭代查询则是DNS客户端自己为中心的,是各个服务器和服务器之间的查询活动,迭代查询的过程中“查询的递交者”一直没变化,其结果是间接告诉DNS客户端另一个DNS服务器的地址。

DNS优化:两个方面:DNS缓存、DNS负载均衡

2. 应用层客户端发送HTTP请求

互联网内各网络设备间的通信都遵循TCP/IP协议,利用TCP/IP协议族进行网络通信时,会通过分层顺序与对方进行通信。分层由高到低分别为:应用层、传输层、网络层、数据链路层。发送端从应用层往下走,接收端从数据链路层网上走。如图所示

从上面的步骤中得到 IP 地址后,浏览器会开始构造一个 HTTP 请求,HTTP请求报文由三部分组成:请求行,请求头、空行和请求数据4部分组成,如图所示

1.请求行

请求方法

HTTP/1.1 定义的请求方法有8种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。

最常的两种GET和POST,如果是RESTful接口的话一般会用到GET、POST、DELETE、PUT

请求地址

URL:统一资源定位符,是一种自愿位置的抽象唯一识别方法。

组成:<协议>://<主机>:<端口>/<路径>

端口和路径有时可以省略(HTTP默认端口号是80)

如下例:


协议版本

协议版本的格式为:HTTP/主版本号.次版本号,常用的有HTTP/1.0和HTTP/1.1

2.请求头部

请求头部为请求报文添加了一些附加信息,由“名/值”对组成,每行一对,名和值之间使用冒号分隔。

常见请求头如下:

请求头部的最后会有一个空行,表示请求头部结束,接下来为请求数据,这一行非常重要,必不可少。

3.请求数据

可选部分,比如GET请求就没有请求数据。

下面是一个POST方法的请求报文:

3. 传输层TCP传输报文

当应用层的 HTTP 请求准备好后,浏览器会在传输层发起一条到达服务器的 TCP 连接,位于传输层的TCP协议为传输报文提供可靠的字节流服务。它为了方便传输,将大块的数据分割成以报文段为单位的数据包进行管理,并为它们编号,方便服务器接收时能准确地还原报文信息。TCP协议通过“三次握手”等方法保证传输的安全可靠。“三次握手”的过程是,发送端先发送一个带有SYN(synchronize)标志的数据包给接收端,在一定的延迟时间内等待接收的回复。接收端收到数据包后,传回一个带有SYN/ACK标志的数据包以示传达确认信息。接收方收到后再发送一个带有ACK标志的数据包给接收端以示握手成功。在这个过程中,如果发送端在规定延迟时间内没有收到回复则默认接收方没有收到请求,而再次发送,直到收到回复为止。

5. 网络层IP协议查询MAC地址

IP协议的作用是把TCP分割好的各种数据包封装到IP包里面传送给接收方。而要保证确实能传到接收方还需要接收方的MAC地址,也就是物理地址才可以。IP地址和MAC地址是一一对应的关系,一个网络设备的IP地址可以更换,但是MAC地址一般是固定不变的。ARP协议可以将IP地址解析成对应的MAC地址。当通信的双方不在同一个局域网时,需要多次中转才能到达最终的目标,在中转的过程中需要通过下一个中转站的MAC地址来搜索下一个中转目标。

6. 数据到达数据链路层

在找到对方的MAC地址后,已被封装好的IP包再被封装到数据链路层的数据帧结构中,将数据发送到数据链路层传输,再通过物理层的比特流送出去。这时,客户端发送请求的阶段结束。

7. 服务器接收数据

接收端的服务器在链路层接收到数据包,再层层向上直到应用层。这过程中包括在传输层通过TCP协议将分段的数据包重新组成原来的HTTP请求报文。

8. 服务器响应请求并返回相应文件

服务接收到客户端发送的HTTP请求后,服务器上的的 http 监听进程会得到这个请求,然后一般情况下会启动一个新的子进程去处理这个请求,同时父进程继续监听。http 服务器首先会查看重写规则,然后如果请求的文件是真实存在,例如一些图片,或 html、css、js 等静态文件,则会直接把这个文件返回,如果是一个动态的请求就根据动态语言的脚本,来决定调用什么类型的动态文件脚本解释器来处理这个请求。(java、php、)

首先会匹配后端路由,由这个路由所定义的处理方法去处理,如果请求需要浏览的内容是一个动态的内容,那么处理函数会相应的从数据源里面取出数据,这个地方一般会有一个缓存,例如 redis或memcached 来减小 db 的压力,如果引入了 orm 框架的话,那么处理函数直接向 orm 框架索要数据就可以了,由 orm 框架来决定是使用内存里面的缓存还是从 db 去取数据,一般缓存都会有一个过期的时间,而 orm 框架也会在取到数据回来之后,把数据存一份在内存缓存中的。

orm 框架负责把面向对象的请求翻译成标准的 sql 语句,然后送到后端的 db 去执行,db 这里以 mysql 为例的话,那么一条 sql 进来之后,db 本身也是有缓存的,不过 db 的缓存一般是用 sql 语言 hash 来存取的,也就是说,想要缓存能够命中,除了查询的字段和方法要一样以外,查询的参数也要完全一模一样才能够使用 db 本身的查询缓存,sql 经过查询缓存器,然后就会到达查询分析器,在这里,db 会根据被搜索的数据表的索引建立情况,和 sql 语言本身的特点,来决定使用哪一个字段的索引,值得一提的是,即使一个数据表同时在多个字段建立了索引,但是对于一条 sql 语句来说,还是只能使用一个索引,所以这里就需要分析使用哪个索引效率最高了,一般来说,sql 优化在这个点上也是很重要的一个方面。

sql 由 db 返回结果集后,再由 orm 框架把结果转换成模型对象,然后由 orm 框架进行一些逻辑处理,将准备好的数据再从动态脚本解释器送回到 http 服务器,构建响应报文,再通过 tcp ip 协议,送回到客户机浏览器

HTTP响应报文主要由状态行、响应头部、空行以及响应数据组成。

1.状态行

由3部分组成,分别为:协议版本,状态码,状态码描述

其中协议版本与请求报文一致,状态码描述是对状态码的简单描述,所以这里就只介绍状态码。

状态码

状态代码为3位数字。

1xx:指示信息--表示请求已接收,继续处理。

2xx:成功--表示请求已被成功接收、理解、接受。

3xx:重定向--要完成请求必须进行更进一步的操作。

4xx:客户端错误--请求有语法错误或请求无法实现。

5xx:服务器端错误--服务器未能实现合法的请求。

下面列举几个常见的:

2.响应头部

与请求头部类似,为响应报文添加了一些附加信息

常见响应头部如下:


3.响应数据

用于存放需要返回给客户端的数据信息。

下面是一个响应报文的实例:

9. Web服务器关闭TCP连接

一般情况下,一旦 Web 服务器向浏览器发送了请求的数据,它就要关闭 TCP 连接,但是如果浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive

TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。与创建TCP连接的3次握手类似,关闭TCP连接,需要4次握手。

TCP四次挥手


上图可以这么理解:

10.浏览器开始处理数据信息并渲染页面

1.解析响应头

分析状态码

如果是 200 开头的就好办,表示请求成功,直接进入渲染流程,如果是 300 开头的就要去相应头里面找 location 域,根据这个 location 的指引,进行跳转,这里跳转需要开启一个跳转计数器,是为了避免两个或者多个页面之间形成的循环的跳转,当跳转次数过多之后,浏览器会报错,同时停止。比如:301表示永久重定向,即请求的资源已经永久转移到新的位置。在返回301状态码的同时,响应报文也会附带重定向的url,客户端接收到后将http请求的url做相应的改变再重新发送。如果是 400 开头或者 500 开头的状态码,浏览器也会给出一个错误页面。比如:404 not found 就表示客户端请求的资源找不到。

解码

浏览得到一个正确的 200 响应之后,首先浏览器会去看响应头里面指定的 encoding 域,如果有了这个东西,那么就按照指定的 encoding 去解析字符,如果没有的话,那么浏览器会使用一些比较智能的方式,去猜测和判断这一坨字节流应该使用什么字符集去解码

解析编码,常见的有:gzip、compress、defalte、identity

部分传输

状态码206 Partial Content 部分内容响应;

Range 请求的资源范围;

Content-Range 响应的资源范围;

客户端通过并发的请求相同资源的不同片段,来实现对某个资源的并发分块下载。从而达到快速下载的目的。目前流行的FlashGet和迅雷基本都是这个原理。

缓存判断

If-Modified-Since:把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比。如果时间一致,那么返回304,客户端就直接使用本地缓存文件。如果时间不一致,就会返回200和新的文件内容。客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示在浏览器中。

If-None-Match:If-None-Match和ETag一起工作,工作原理是在HTTP Response中添加ETag信息。 当用户再次请求该资源时,将在HTTP Request 中加入If-None-Match信息(ETag的值)。如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。否则将返回200状态和新的资源和Etag.  使用这样的机制将提高网站的性能。例如: If-None-Match: "03f2b33c0bfcc1:0"。

Pragma:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝;在HTTP/1.1版本中,它和Cache-Control:no-cache作用一模一样。Pargma只有一个用法, 例如: Pragma: no-cache

浏览器第一次请求:

浏览器再次请求时

2.渲染页面

浏览器的主要组件包括:

用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的你请求的页面外,其他显示的各个部分都属于用户界面。

浏览器引擎 - 在用户界面和渲染引擎之间传送指令。

渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。

网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。

用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。

JavaScript 解释器。用于解析和执行 JavaScript 代码,比如chrome的javascript解释器是V8。

数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5)定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

如图


值得注意的是,不同于大多数浏览器,Chrome 浏览器为每个标签页(Tab)都分配了各自的渲染引擎实例,每个标签页都是一个独立的进程(即每个标签页面都在独立的“沙箱”内运行,在提高安全性的同时,一个标签页面的崩溃也不会导致其他标签页面被关闭)。

2.2 渲染引擎简介

  常见的渲染引擎。Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome(28版本以前)浏览器使用的都是 Webkit。

2013年7月10日发布的Chrome 28 版本中,Chrome浏览器开始正式使用Blink内核。所以,Webkit已经成为了Chrome浏览器的前内核。

2.3主流程

渲染引擎解析HTML文档,并将文档中的标签转化为dom节点树,即”内容树”。同时,它也会解析外部CSS文件以及style标签中的样式数据。这些样式信息连同HTML中的”可见内容”一道,被用于构建另一棵树——”渲染树(Render树)”。

  渲染树构建完毕之后,将会进入”布局”处理阶段,即为每一个节点分配一个屏幕坐标。再下一步就是绘制(painting),即遍历render树,并使用UI后端层绘制每个节点

值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

主流程示例

解析与DOM树构建

解析工作一般由两个组件共同完成: 

1)词法分析器(有时也称为标记生成器),负责将输入内容分解成一个个有效标记。词法分析器知道如何将无关的字符(比如空格和换行符)分离出来。; 

2)解析器负责根据语言的语法规则分析文档的结构,从而构建解析树。

2.5、转换

很多时候,解析树还不是最终结果。解析通常是在转换过程中使用的,而转换是指将输入文档转换成另一种格式。编译就是一个例子。编译器可将源代码编译成机器代码,具体过程是首先将源代码解析成解析树,然后将解析树翻译成机器代码文档。


针对html文档具体的解析过程
1.包含内联样式和内联脚本的 HTML 文档

如果 HTML 文档中存在内联样式和脚本,这个时候,问题变得稍微复杂一些。浏览器解析 HTML,构建 DOM 树,当解析到<style>标签时,样式信息开始被解析,CSSOM 被构建,但是它并不会影响到 HTML 的解析和 DOM 树的构建。当 HTML 解析到<script>标签时,因为脚本有可能改变 DOM 内容,所以 HTML 的解析必须等到脚本执行完毕后再继续。脚本又有可能操作 CSSOM ,所以脚本必须等到 CSS 解析完毕后才能执行。确保此刻 CSS 解析完成,脚本被交到 JS 引擎手里,由 JS 引擎执行。当脚本执行完毕,HTML 继续解析,直到全部 HTML 解析完毕,DOM 树构建完成(触发DOMContentLoaded事件)。

注意:DOMContentLoaded事件只和 HTML 的加载和解析有关,一旦 HTML 解析完成,这个事件就会被触发,不管此时还有没有CSS的解析、图片的下载或者异步脚本的加载和执行。DOM 树一旦构建完成,就会开始构建 render 树,并不管 CSS 是否解析完毕。如果构建 render 树的时候,CSS 还没有解析完成,那么 render 树会用占位符代替应该有的 CSSOM 节点,当该节点加载解析好后,再重新计算样式。

但是同步脚本的执行会阻塞 HTML 的解析,从而会影响到DOMContentLoaded事件的触发。同时又要注意,CSS 会阻塞 JS 脚本的执行,从而间接影响到 HTML 的解析和DOMContentLoaded事件的触发。

2.包含外部 CSS 和脚本的 HTML 文档

如果 HTML 文档中存在外联样式表和脚本,问题变得更复杂一点。HTML 文档加载完成后,浏览器首先扫描 HTML 文档,查看有哪些外部资源需要启动 network operation 来请求资源,并在 HTML 解析的同时,发送所有的请求。CSS 资源加载完毕后,会立即开始解析构建 CSSOM。(同步脚本加载完毕后,并不能立刻执行。)当 HTML 解析到<script>标签,先确认脚本加载完毕了没,如果没,那得等;如果加载好了,还得看 CSS 解析好了没。如果没,那还得等;如果 CSS 解析好了,那就能把脚本交给 JS 引擎去执行了。当 JS 执行完毕,HTML 继续解析,DOM 继续构建,直到全部构建完成,DOMContentLoaded事件被触发。紧接着,就是构建 render 树。

如果脚本有async属性,问题就又不一样了。async属性默认该脚本不会影响到 DOM 内容,所以只要脚本下载完成,(相关)CSS 解析完毕,脚本立刻执行,不用等着 HTML 解析到<script>标签再开始执行。同样,HTML 也不会等着脚本执行完毕再解析。仿佛两者看不到对方,只管做自己的事情就行了。

渲染树树构建,期间会计算元素的样式

建立渲染树和 DOM 树的关系

布局

呈现器在创建完成并添加到渲染树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排

 Dirty 位系统

  为避免对所有细小更改都进行整体布局,浏览器采用了一种“dirty 位”系统。如果某个呈现器发生了更改,或者将自身及其子代标注为“dirty”,则需要进行布局。

  有两种标记:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。

全局布局和增量布局

  全局布局是指触发了整个渲染树范围的布局,触发原因可能包括:

影响所有呈现器的全局样式更改,例如字体大小更改。

屏幕大小调整。

布局可以采用增量方式,也就是只对 dirty 呈现器进行布局(这样可能存在需要进行额外布局的弊端)。

当呈现器为 dirty 时,会异步触发增量布局。例如,当来自网络的额外内容添加到 DOM 树之后,新的呈现器附加到了渲染树中。

绘制

全局绘制和增量绘制

  和布局一样,绘制也分为全局(绘制整个渲染树)和增量两种。

动态变化

  在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个渲染树都会进行重新布局和绘制。 

为了更方便理解上面提到的每一层的概念

                            最后附上七层网络架构OSI参考模型

推荐阅读更多精彩内容