从输入url到页面加载发生了什么

Posted by Zi Ning on July 13, 2017

从输入url到页面加载发生了什么

最近在看《计算机网络:自顶向下方法》,看完了第二章应用层后,对标题所说的问题渐渐有了清晰的认识。虽然网络上关于这个的文章有很多,但是还是想自己梳理一遍,加深一下印象(可能并不是非常全面…求知欲很强的同学可以看看这篇,保证不会让你失望→_→

大致来说,如果抛开硬件操作系统方面的话,应该会有如下几个流程:

  1. 根据浏览器中输入的url地址进行DNS查询找到其IP地址。(DNS查询)
  2. 客户端向服务器请求建立连接。(TCP三次握手)
  3. 建立连接后,客户端向服务器发送请求。服务器收到请求,检索数据库资源,确认无误后将资源发送给客户端。(HTTP请求)
  4. 客户端收到资源,并将资源展现在浏览器上,形成我们所看到的“页面”。(页面渲染)

这其中每一步都隐藏着许多细节,来看看吧。

分层的网络体系结构

首先要介绍一个概念。为了给网络协议设计提供一个结构,使其具有概念化结构化和封装的优点,设计者以分层的方式组织以及实现这些协议的网络硬件和软件。

按照自顶向下的方法分为:应用层、传输层、网络层、链路层和物理层。

数据由应用层向下传播,每一层对数据进行各自的封装,提供不同的用途:

  • 应用层:应用层是网络应用程序及他们的应用层协议存留的地方,根据不同的用途提供了多种应用层协议。例如,HTTP(Web文档)、SMTP(邮件)、FTP(文件传输)等。
  • 运输层:当运输层收到应用层报文(message)后,会附上附加信息(运输层首部信息)。这样应用层报文和运输层首部信息一起构成了运输层报文段(segment)。根据采用的运输层协议(TCP or UDP)来提供了是否确保传递,流量控制和阻塞控制的机制。
  • 网络层:运输层继续向网络层传递该报文段,网络层增加了源和目的地址等网络层首部消息,产生了网络层数据报(datagram)。转发并选择路由。
  • 链路层:链路层收到数据报后,加上自己的链路层首部信息创建链路层帧(frame).
  • 物理层:将收到的帧从一个网络元素移到另一个网络元素,将该帧的一个一个比特从一个节点移动到另一个节点。

可以看出,每经过一层都会被加上各自的首部字段。当服务器接受时,会按照相反的方向层层解码,把首部剥离出去。最后得到原始报文。

现在可以进入具体过程了!

DNS查询

科普时间

主机名(hostname)

互联网上的主机和人类一样有许多种方式进行标志,比如说使用名字。主机名就是这么一种标志方式,像我们常用的www.google.com,twitter.com,还有cis.poly.edu等。

IP地址

然而,主机名大部分却没有办法提供关于主机在因特网中的位置信息,而且使用不定长的数字字母表示也很难被路由器所处理。IP地址就是用来解决这一缺陷的新的标志方法,它由字节组成,有严格的层次结构。常见的的IP地址分为IPv4IPv6两大类。

拿IPv4来说,它由4个字节32位组成。作为例子:121.7.106.83这样一个IP地址,每个字节都被句点分隔开来,表示0-255的十进制数字。之所以说它有层次结构,是因为我们从左到右扫描它时,会得到越来越具体的关于主机位于因特网何处的信息(即众多网络的哪个网络里)。

DNS

全名叫做域名管理系统(Domain Name System)。通俗来说,DNS就是用来将主机名转化为IP地址的服务。关于DNS有几点重要的信息需要知道:

  • 它是一个由分层的DNS服务器实现的分布式数据库。
  • 它是一个使主机能够查询分布式数据库的应用层协议
  • 运行在UDP(运输层协议的一种,稍后解释)之上,使用53号端口。

DNS服务器

为了处理扩展性问题,DNS使用了大量的DNS服务器,它们以层次的方式组织,并分布在全世界范围内。没有一台DNS服务器拥有因特网上所有主机的映射,该映射分布在所有的DNS服务器上。这样就避免了一旦单个DNS服务器故障,所有网络瘫痪的噩梦,而且还减少了通讯流量,方便后期维护。

大致来说,有三种类型的DNS服务器。

  • 根DNS服务器,DNS服务器中最高级别的域名服务器,负责返回顶级域名DNS服务器的地址。
  • 顶级域DNS服务器(TLD),负责顶级域名如com、org、net、edu和gov以及所有国家的顶级域名。
  • 权威DNS服务器,在互联网上具有公共可访问主机的每个主机机构都必须提供DNS记录(将主机名映射为IP),而权威服务器收藏了这些记录。一般主机的IP就是从这里得到的。

还有一种重要的DNS服务器叫做本地DNS服务器(local DNS serve)。严格来说它并不属于DNS服务器体系、每个ISP都有一台本地DNS服务器。当主机发出DNS请求时,该请求一般被发往本地DNS服务器,它起着代理的作用,将请求转发给上述DNS服务器层次结构中。

DNS工作流程

好了,进入正片!

假设当我们在浏览器请求一个url,为了发送一个HTTP请求报文,必须要获的该url主机名的IP地址。

  • 浏览器从上述的url中抽取主机名(很多基于UNIX的机器上,应用会调用gethostbyname()),并将该主机名传给DNS应用的客户端。
  • DNS客户端向本地DNS服务器缓存中发送一个包含主机名的请求。
  • 如果本地DNS服务器缓存了该主机名对应的IP地址,就会以回答报文的形式发送给DNS客户端。
  • 如果本地DNS服务器没有缓存呢?它会将该请求转发给根DNS服务器;该根DNS服务器注意到顶级域名,如.com,并向本地DNS服务器返回负责com的顶级域名DNS服务器(后用DTL表示)的IP地址列表;该本地DNS服务器再次向这些DTL服务器之一发送查询报文;该DTL服务器注意到主机名前缀,并用权威服务器的IP地址进行响应。最后,本地DNS服务器直接向权威服务器重发查询报文,权威服务器用主机名对应的IP地址进行响应。
  • 本地DNS服务器向DNS应用客户端发送收到的IP地址。而且本地DNS服务器会保存这次收到的IP地址到缓存区,在一段时间后才会丢弃该缓存。(这样下次请求,就可以绕过复杂且速度慢的查询DNS服务器体系了)

图解DNS 域名解析过程:

DNS 域名解析过程

注意

  1. 在流程第四步查询DNS服务器体系过程中使用的是迭代查询。理论上,任何DNS查询既可以是迭代的也可以是递归的。在实践中,通常是从请求主机到本地DNS服务器的查询是递归的,其余查询是迭代的。
  2. 实际上不只是本地DNS服务器会有缓存,实际上浏览器、操作系统、hosts文件、都会保留缓存,所以也会按照上述过程依次进行缓存查询和缓存。

TCP三次握手

在上述DNS查询中,我们得到了目的服务器的IP地址,那是不是直接就可以与自由的它进行交互和请求资源?答案时否定的,这是因为在真正的请求发送前需要先与服务器建立连接。(可以简单理解为运输前要先铺好管道)。在这里属于运输层所做的事情(回忆一下上文,DNS查询属于应用层),互联网为应用程序提供了两个运输层协议,即UDP和TCP。在本文中,由于传输的是web文档,所以我们使用的是TCP作为建立连接的运输层协议,当然UDP也有自身的用途,来对比看一下吧!

TCP

TCP服务模型包括面向连接和可靠数据传输服务。某个应用程序调用TCP作为其运输协议时,该应用程序就能获得这两种服务。

  • 面向连接的服务:在应用层数据报文(我们所说的请求)开始流动之前,TCP让客户和服务器相互交换运输层控制信息。这就是所谓的握手过程,它提示客户和服务器,使它们为大量分组的到来做好准备,在握手阶段后,一个TCP连接就在两个进程的套接字(后文解释)之间建立了。这条连接是双全工的,即连接双方的进程可以在此连接上同时进行报文收发。当应用程序结束报文发送时,必须拆除该连接。
  • 可靠的数据传送服务:通信进程能够依靠TCP,无差错、按适当顺序交付所有发送的数据,这样就能保证数据没有字节的丢失和冗余。
  • 阻塞控制机制:当发送方和接收方之间网络出现阻塞时,它会抑制发送进程,试图限制每个TCP连接

UDP

  • 无面向连接的服务,没有握手阶段。
  • 无可靠的数据传输服务,不保证数据不会丢失。
  • 没有阻塞控制机制。

以上就是TCP和UDP的区别,据此可以知道为什么web文档需要使用TCP而不是UDP作为运输层协议了吧!当然它们之间也有着相同点,即都没有提供加密机制。这里说一句题外话,我们常常提到的HTTPS就是在应用层加了一层SSL套接字来实现数据的加密和解密,这里就不详细阐述了。

三次握手

首先,我们会在客户端和服务器之间建立连接。这涉及到”三次握手“的过程,即客户端向服务器发送一个小的TCP报文段,服务器用一个小的TCP报文段作为确认和响应,最后客户向服务器返回确认。这样,一个TCP连接就建立起来了。

还是看图清晰一点:

图解HTTP中的TCP三次握手

一定要注意这最后一步,客户会结合三次握手的第三部分(确认)向该TCP连接发送一个HTTP请求报文

HTTP请求

现在到了离我们最接近的HTTP请求部分了,还是需要解释一些基本概念,可能有点多,be patient!每一条都会有用的。

基本概念

socket

套接字,我也不知道为什么会被翻译成这个会让人难以理解的名字,你把它按照直译插座理解起来更好。它是应用层和运输层的接口(API),等TCP建立连接后,浏览器和服务器会通过socket接口来传输数据。简单类比一下,socket就像是房间里的门,无论你进去还是出门都需要通过它。

持续连接和非持续连接

他们之间的区别在,即每个请求、相应是经一个单独的TCP连接发送,还是所有的请求及其相应经过相同的TCP连接发送。前者被称为非持续连接,后者被成为持续连接。

先说一下非持续连接,非持续连接必须为每个请求对象建立和维护一个全新的连接,也就是说在客户端收到请求资源后,TCP连接会断开。这也意味着对每个这样的连接,在客户和服务器都分配TCP的缓存区和保持TCP变量,这给Web服务器带来了严重的负担。而且,每次连接都会重复的进行TCP握手这样也会带来时间上的浪费;而采用了持续连接,服务器在发送相应后保持TCP连接打开。在相同的客户与服务器之间的后序请求和相应报文能够通过相同的连接进行传送。虽然HTTP默认模式是使用带流水线的持续连接,如要关闭持续连接,只需要在HTTP报文中设置Connection: close

HTTP报文

超文本传输协议(HyperText Transfer Protocol)简称HTTP,是一种用于分布式,协作式和超媒体信息系统的应用层协议。HTTP规范包含了对HTTP报文格式的定义。报文有两种请求报文和响应报文。

请求报文

通过请求报文,我们可以向服务器请求我们需要的资源。所以,这里必须要包含一些基本的信息。下面提供了一个典型的HTTP请求报文:

GET /somedir/page.html/ HTTP/1.1   // 请求行(方法字段、url字段、HTTP版本字段)
// 以下为首部行
Host: www.someshcool.edu          // 主机名 
Connection: close                 // 是否持续连接
User-agent: Mozilla/5.0           // 用户代理(浏览器类型)
Accept-language: fr               // 接受的语言 

HTTP请求报文第一行叫做请求行(request line),其后继的行叫做首部行(header line)。

请求行有三个字段:分别为方法字段、URL字段、和HTTP版本字段。方法字段可以有几种不一样的值,包括GET、POST、HEAD、PUT和DELETE。这几种方法的区别,就不介绍了,在网络上可以找到很多的资料。在文章中,我们使用GET方法来请求对象。URL字段带有请求对象的标识,例子中是/somedir/page.html/,标识了请求对象的路径和名称。最后是一个HTTP的版本号,可以看出这里使用的是HTTP/1.1版本。

首部行中也有许多字段,就不一一详解了。再来看一个请求报文的通用格式:

http request

这里会发现比我们刚才看到的格式多了一个空行和实体主体。注意,当我们使用GET方法时实体为空,而使用POST方法时才是用该实体。使用POST报文时,仍然可以向服务器请求页面,而返回的资源页面内容依赖于POST实体。例如:我们在登录表单中输入的账户和密码,就会放在实体主体部分提交给服务器,然后返回给我们特定的页面。

响应报文

根据上面的请求报文,来看下相应的响应报文是什么样的吧。

HTTP/1.1 200 OK  // 状态行(status line)
// 后面是首部行
Connection: close                               // 是否是持续连接
Date: Tue, 09, Aug 2011 15:44:04 GMT  		   // 日期,从服务器取出发送的时间
Server: Apache/2.2.3(CentOS)          		   // 服务器名称(系统),类似于User-agent
Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT   // 验证缓存是否需要更新
Content-length: 6821                  		   // 发送对象的字节数
Content-Type: text/html               		   // 实体对象是HTML文本

结构看起来和上面差不多,第一行为初始状态行,然后首部行。状态行有三个字段:协议版本字段、状态码和相应状态信息。这里唯一有疑问的可能就是状态码了,状态码及其相应的短语指示了请求的结果。来看看常见的状态码。

状态码

2XX 成功

  • 200 OK :请求成功,信息在返回的响应报文中。

  • 204 No Content:服务器接受请求已成功处理,但返回的响应报文中不包含资源内容。
  • 206 Partial Content: 客户端进行范围请求,响应报文包含由Content-Range指定范围的内容。

3XX 重定向

  • 301 Moved Permanently :永久性重定向,表示请求资源已经被分配新的URL,以后应使用资源现在所指的URL。注意,当指定资源路径的最后忘记添加”/”,就会产生301状态码。(以后,尽量不要忘记在URL最后添加/
  • 302 Found:临时性重定向,表示请求的资源已经分配给了新的URL,希望用户本次能使用新的URL访问。
  • 304 Not Modified:表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回304 Not Modified。

4XX 客户端错误

  • 400 Bad Request:请求报文中存在语法错误,需修改请求内容后再次发送请求。
  • 403 Forbidden:表明请求资源的访问被服务器拒绝了。
  • 404 Not Found:最常见的错误了,表明服务器无法找到请求的资源。除此之外,也可以在服务器拒绝请求且不想说明理由时使用。

5XX 服务器错误

  • 500 Internet Server Error 服务器端在执行请求时发生了错误。也可能是Web应用存在的bug或某些临时的故障。

  • 505 Server Unavailable:服务器暂时处于超负荷或正在进行停机维护,现在无法处理请求。

相应报文的通用格式和请求报文差不多:

HTTP响应报文

可以看出,这里的实体主体就是我们请求的资源对象了!

HTTP服务器是无状态的。这简化了服务器的设计,并且允许工程师们去开发可以同时处理数以千计的TCP连接的高性能Web服务器。但常常Web站点希望能够识别用户,把内容和用户的身份联系起来。这就是cookie的作用了,现在大部分的站点都使用了cookie。cookie运行在客户端,保留着一个cookie文件,由用户的浏览器进行管理。简单来说,如果访问一个没有去过的网站时,该web站点将产生一个唯一识别码,随着返回的报文存储到cookie文件中。我们下一次请求这个页面时,会将它发送给服务端,经过服务端验证后返回特定的页面给浏览器。

web缓存

web缓存器(Web cache)也叫代理服务器,它和前面提到的DNS缓存用处差不多,用来保存最近请求过得对象的副本,大大降低对客户请求的相应时间和流量的减少。值得注意的是,Web缓存器是服务器同时又是客户。当它接受浏览器的请求并返回相应时,它是一个服务器。当它向初始服务器发出请求并接受相应时,它是一个客户。

基本概念介绍完了,来看下基本流程吧!

流程

假设我们访问的是https://www.amazon.com/亚马逊的页面,页面采取非持续连接。

  • 浏览器建立一个到web缓存器的TCP连接,并通过socket向Web缓存器中的对象发送一个HTTP请求(如果以前访问过该站点,会浏览器会从cookie文件中取出识别码,并放在请求报文的首部行中)。
  • Web缓存器进行检查,看看本地是否有该对象的缓存。如果有,Web缓存器就向客户端用HTTP响应报文返回该对象。
  • 如果Web缓存器没有该对象,它就打开一个与该对象的初始服务器(Amazon.com的服务器)的TCP连接。Web缓存则在这个TCP上将请求转发给初始服务器。
  • 当请求报文到达服务器时,会验证请求报文的cookie识别码,这样就能跟踪用户在Amazon的活动。
  • 如果是第一次访问该页面,当请求报文到达该服务器时,该web站点会产生一个唯一识别码。以此作为索引在后端数据库中产生一个表项,接下来Amazon服务器用一个包含Set-cookie首部的HTTP响应报文和包含资源的实体主体对缓存服务器进行响应。
  • 当Web缓存器收到该对象时,她在本地存储空间存储一份副本,并向客户的浏览器用HTTP响应报文发送该副本。
  • 浏览器收到请求报文后,解析渲染页面,并断开之前使用的TCP连接。

到这里,主要的流程就结束了。还需要补充一下渲染页面的过程,对于前端来说,这个还是挺重要的。

渲染页面

在网上找到一篇比较好的关于浏览器渲染页面文章,是酷壳的陈皓大神写的。下面内容基本是这篇文章的简略版,如果想要更细致的了解,直接看原文好了。

浏览器收到请求后,又该怎么去渲染页面,形成我们看到的网页呢?

先看一个Webkit的渲染过程图:

render process

  1. 首先浏览器会解析三个东西
  • HTML/SVG/XHTML,解析后会生成DOM Tree。
  • CSS文件,解析后生成CSS Style Rules。
  • JS脚本,主要通过操作DOM和CSS来改变页面样式。
  1. 解析完成后,浏览器根据DOM Tree + CSS Style Rules来绘制Render Tree。
  2. Layout – 定位坐标和大小,是否换行,各种position, overflow, z-index属性 ……[reflow]
  3. 最后通过操作系统的Native GUI的API绘制。[painting]

在这个过程中涉及两个关键的概念:

  • Reflow ——页面中每个元素都是以盒模型的形式存在的,这些都需要浏览器去计算位置和大小。而且当元素的几何形状发生了变化,需要重新计算Render Tree。这也可能会导致其他元素的位置和大小发生变化,因此成本非常高。
  • Repaint——屏幕的一部分要重画,一般只是颜色背景发生变化,不涉及几何尺寸。

注意

  1. 在构建DOM树时,浏览器由上至下的解析html文件。遇到css文件会阻塞渲染树的构建(DOM树不会),遇到js文件会阻塞DOM树的构建。尤其当遇到js文件时,当下载完毕后,会立刻执行,会阻塞树的构建,可能造成页面的卡顿。这也是为什么我们建议在页面的最后才添加js文件的原因。(ps:下载的过程不会触发阻塞,因为会新开一个线程来执行下载过程)。
  2. Reflow和Repaint非常消耗性能(尤其Reflow),所以在一些优化页面体验的文章中,会强调在操作DOM时尽量减少Reflow,比如使用类替换而不是逐项修改style;先在documentFragment上操作,最后再添加到DOM树中等技巧。

如何减少reflow和repaint

参考资料