1. 简介
我思考了很多知识组织方法来帮助理解网络知识,比如按osi模型从底至上,或者按协议种类,或者按网络发展史。但最终我还是决定选择用这个经典的问题,将网络知识串成线。理解从输入url到看到页面的过程,弄明白这中间有哪些步骤,再仔细分析这些步骤的原理和行为,是我所能想到最清晰的一条知识脉络了。
2. 如何看到我们的页面?
这里我们就以访问阮一峰老师的一篇博文(HTTP 协议入门)来讲解吧,大家阅读本篇文章前也可以看看这篇文章。我们在浏览器输入“http://www.ruanyifeng.com/blog/2016/08/http.html”以后,浏览器如何为我们展现网页呢?
step1:浏览器解析url
浏览器会对我们输入的url进行解析,主要将其分为下部分:协议、网络地址、资源路径。其中网络地址指示该连接网络上哪一台计算机,可以是域名或者IP地址,可以包括端口号;协议是从该计算机获取资源的方式,常见的是HTTP,HTTPS,FTP等。不同协议有不同的通讯内容格式;资源路径指示从服务器上需要获取资源的具体路径。
这里浏览器对输入的url解析为如下内容:
url:http://www.ruanyifeng.com/blog/2016/08/http.html
协议:http
网络地址(网站名):www.ruanyifeng.com
资源路径:/blog/2016/08/http
当然,浏览器还知道端口信息和参数信息,但这一步还用不上。另外网络地址由服务器名:www和域名ruanyifeng.com组成。
step2:DNS域名解析
浏览器理解用户输入的信息,知道用户想要用http访问一个网络地址是“www.ruanyifeng.com”的网站。那么如何找到这个地址呢,就像你打的回家,你跟司机说去阮老师家,他哪儿知道阮老师家是哪里呢?你得告诉他门牌号呀。网站服务器的门牌号就是IP地址。所有浏览器首先要确认的是域名所对应的服务器在哪里。将域名解析成对应的服务器IP地址这项工作,是由DNS服务器来完成的。
客户端收到你输入的域名地址后,它首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。一般用户很少去编辑修改hosts文件。
DNS服务器层级如下:
DNS服务器层级(转)
DNS查询的具体步骤如下:
从浏览器缓存中查询。浏览器会存储一定时间的DNS记录,操作系统不会告诉浏览器每个DNS记录的保存时限,不同浏览器设置保存时限为一个固定值(不同浏览器情况不同,一般在2-30分钟)。
从操作系统缓存中查询。如果浏览器中没有包含想要的缓存记录,那浏览器就会发起操作系统请求,继续查询操作系统缓存
从路由器中查询DNS缓存。请求持续发送到你的路由,它通常会有自己的DNS缓存。
从ISP中查询DNS缓存。下一个被查询地方是ISP缓存DNS的服务器。
域名服务器迭代查询,根据返回的地址逐级向上查询。首先从root域名服务器中查询如.com域名服务器,然后逐步向前查询,.com顶级域名服务器到ruanyifeng的域名服务器。一般来说,.com级别的都已经在缓存中了,所以一般不会进行对root域名服务器的查询。下面给出一张迭代查询的图。
DNS服务器递归查询和迭代查询(转)
浏览器客户端向本地DNS服务器发送一个含有域名http://www.cnblogs.com的DNS查询报文。本地DNS服务器把查询报文转发到根DNS服务器,根DNS服务器注意到其com后缀,于是向本地DNS服务器返回comDNS服务器的IP地址。本地DNS服务器再次向comDNS服务器发送查询请求,comDNS服务器注意到其http://www.cnblogs.com后缀并用负责该域名的权威DNS服务器的IP地址作为回应。最后,本地DNS服务器将含有http://www.cnblogs.com的IP地址的响应报文发送给客户端。
从客户端到本地服务器属于递归查询,而DNS服务器之间的交互属于迭代查询。
正常情况下,本地DNS服务器的缓存中已有comDNS服务器的地址,因此请求根域名服务器这一步不是必需的。
一些大型网站域名像wikipedia.org或者facebook.com整个域都映射到不止一个IP地址(土豪房子多),我们可以使用如下解决方法:
Round-robin DNS,DNS轮询是其中一种方法,是DNS查找时返回多个IP时的解决方案。举例来说,Facebook.com实际上就对应了四个IP地址。
Load-balancer,大型的网站一般都会使用高性能的负载均衡器来平衡流量。负载均衡器一直监听一个特殊的IP地址,并转发请求到其他的服务器。(译者注:简单粗暴点理解就是在用户和服务器之间加了个中间层,利用监听和转发请求达到用户相对快速访问,资源最优化使用的目的)
Geographic DNS,基于地理的DNS,依赖客户端的地理位置,这是一个很好的存储静态资源的方法,不同的服务器可以不更新共享状态。
Anycast,一种单个IP地址映射多个物理服务器的技术。
step3:浏览器获取端口号
好了,阮老师家的门牌号知道了,正常来讲是可以出发了。可是对于网络有些不一样,你还需要指定端口号。端口号之于计算机就像窗口号之于银行,一家银行有多个窗口,每个窗口都有个号码,不同窗口可以负责不同的服务。端口只是一个逻辑概念,和计算机硬件没有关系。现在可以这么说,阮老师家好几扇们,办不同的业务走不同的门,你得告诉师傅你走那扇门,你要不说,就默认你是个普通客人,丢大门得了。http协议默认端口号是80。
step4:TCP建立连接
好了,IP和端口都有了,能出发了么?师傅很热心,怕你去了对方家里没人啊,于是根据查到的信息给对方家里打了电话
师傅:“喂,家里有人没啊?有客人来拜访啊?”
对方:“有啊,来吧”
师傅:“好嘞”。
在http消息发送前,需要建立客户端与服务器的TCP链接,也就是进行所谓的三次握手。
TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。
TCP三次握手
TCP三次握手的过程如下:
客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。
服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
step5: 发送HTTP请求
你们和阮老师家里互相确定了到访的事情后,你们终于开心地出发了。也就是说,与服务器建立了连接后,就可以向服务器发起请求了。这里我们先看下请求报文的结构:
请求报文结构-转
我们在chrome浏览器查看报文首部信息:
报文首部信息
我们可以从报文中看到发出的请求的具体信息。具体每个首部字段的作用,会开单章讲解。
step6:服务器处理请求
好了,终于到了阮老师家,坐车去阮老师家的例子到此就结束了。不过我们的渲染页面的目的还没达到,现在请求只是成功达到了服务器,接下来服务器需要响应浏览器的请求。
服务器端收到请求后的由web服务器(准确说应该是http服务器)处理请求,诸如Apache、Ngnix、IIS等。web服务器解析用户请求,知道了需要调度哪些资源文件,再通过相应的这些资源文件处理用户请求和参数,并调用数据库信息,最后将结果通过web服务器返回给浏览器客户端。下面以静态渲染的页面为例,ajax渲染不需要在服务器做页面数据写入。
以静态页面渲染为例
step7:返回响应结果
在HTTP里,有请求就会有响应,哪怕是错误信息。这里我们同样看下响应报文的组成结构:
响应报文结构-转
在响应结果中都会有个一个HTTP状态码,比如我们熟知的200、301、404、500等。通过这个状态码我们可以知道服务器端的处理是否正常,并能了解具体的错误。
状态码由3位数字和原因短语组成。根据首位数字,状态码可以分为五类:
状态码
具体的状态码信息我会开单章。
另外还有一些其他信息(chrome中显示的response headers如下):
response headers
内容编码头部告诉浏览器响应体使用了gzip压缩算法,解压后就会看到你期望的HTML了。
除了压缩信息之外,头部还详细说明了是否和怎么缓存页面、设置cookies(在这个响应中没有)、隐秘信息等
或许有人注意到了设置了内容类型为text/html,这部分头部说明了浏览器将响应内容作为HTML渲染,而不是作为文件下载。浏览器将使用头部决定如何解释响应结果,当然也会考虑其他因素,比如URL的扩展情况。
step8: 关闭TCP连接
为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。
建立一个连接需要三次握手,而终止一个连接要经过四次挥手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
四次挥手
(1) 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
(2) 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。
注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3) 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
(4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
注意:
(1) “通常”是指,某些情况下,步骤1的FIN随数据一起发送,另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。[2]
(2) 在步骤2与步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这称为“半关闭”(half-close)。
(3) 当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是,客户执行主动关闭,但是某些协议,例如,HTTP/1.0却由服务器执行主动关闭。
step9:浏览器加载解析渲染
当浏览器获得一个html文件时,会“自上而下”加载,并在加载过程中进行解析渲染。
解析:
浏览器会将HTML解析成一个DOM树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
将CSS解析成 CSS Rule Tree 。
根据DOM树和CSSOM来构造 Rendering Tree。注意:Rendering Tree 渲染树并不等同于 DOM 树,因为一些像 Header 或 display:none 的东西就没必要放在渲染树中了。
这一块的过程比较复杂,会开单章。
step10:浏览器发送嵌入在HTML中的对象的请求
随着浏览器渲染HTML,浏览器会注意到有些标签需要请求其他URLs的资源,浏览器将会发送一个GET请求来重新获取每个文件 。比如js文件,css文件,图片资源等。
每个URLs会像获取HTML页面的过程一样获取相应资源。所以,浏览器会在DNS中查询域名,并向URL发送请求,进行重定向(其实以上步骤我是省略了重定向这一步的)等等以上步骤
当然,静态文件和动态网站不一样,它们允许被浏览器缓存。一些文件可能会根本不经过服务器,直接被从缓存中取出。因为响应结果中返回一个包含着Expires头的文件,所以浏览器知道要缓存一个文件多久。另外每个响应可能包含着ETag头,其作用类似版本号,如果浏览器发现已经拥有了一个文件的ETag,那么就会立即停止此文件传输。
step11:浏览器发送异步请求
在web2.0时代,即使在页面渲染后客户端还是持续与服务器端通信。这个模式被称为AJAX。我会开单章讲述。
3. 总结
以上步骤只是大略地解析了从浏览器输入url到最终页面展示在用户眼前的流程,更多细节我会开单章进行讲解。