在上一篇文章中我介绍了TCP协议是如何保证数据完整传输的,相信你还记得,一个TCP连接过程包括了建立连接、传输数据和断开连接三个阶段。
而HTTP协议,正是建立在TCP连接基础之上的。HTTP是一种允许浏览器向服务器获取资源的协议,是Web的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如HTML文件、CSS文件、JavaScript文件、图片、视频等。此外,HTTP也是浏览器使用最广的协议,所以要想学好浏览器,就要先深入了解HTTP。
不知道你是否有过下面这些疑问:
这一切的秘密都隐藏在HTTP的请求过程中。所以,在今天这篇文章中,我将通过分析一个HTTP请求过程中每一步的状态来带你了解完整的HTTP请求过程,希望你看完这篇文章后,能够对HTTP协议有个全新的认识。
如果你在浏览器地址栏里键入极客时间网站的地址:http://time.geekbang.org/index.html, 那么接下来,浏览器会完成哪些动作呢?下面我们就一步一步详细“追踪”下。
首先,浏览器构建请求行信息(如下所示),构建好后,浏览器准备发起网络请求。
GET /index.html HTTP1.1
在真正发起网络请求之前,浏览器会先在浏览器缓存中查询是否有要请求的文件。其中,浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术。
当浏览器发现请求的资源已经在浏览器缓存中存有副本,它会拦截请求,返回该资源的副本,并直接结束请求,而不会再去源服务器重新下载。这样做的好处有:
当然,如果缓存查找失败,就会进入网络请求过程了。
不过,先不急,在了解网络请求之前,我们需要先看看HTTP和TCP的关系。因为浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息;并使用TCP/IP作传输层协议将它发到网络上,所以在HTTP工作开始之前,浏览器需要通过TCP与服务器建立连接。也就是说HTTP的内容是通过TCP的传输数据阶段来实现的,你可以结合下图更好地理解这二者的关系。
那接下来你可以思考这么“一连串”问题:
在上一篇文章中,我们介绍过数据包都是通过IP地址传输给接收方的。由于IP地址是数字标识,比如极客时间网站的IP是39.106.233.176, 难以记忆,但使用极客时间的域名(time.geekbang.org)就好记多了,所以基于这个需求又出现了一个服务,负责把域名和IP地址做一一映射关系。这套域名映射为IP的系统就叫做“域名系统”,简称DNS(Domain Name System)。
所以,这样一路推导下来,你会发现在第一步浏览器会请求DNS返回域名对应的IP。当然浏览器还提供了DNS数据缓存服务,如果某个域名已经解析过了,那么浏览器会缓存解析的结果,以供下次查询时直接使用,这样也会减少一次网络请求。
拿到IP之后,接下来就需要获取端口号了。通常情况下,如果URL没有特别指明端口号,那么HTTP协议默认是80端口。
现在已经把端口和IP地址都准备好了,那么下一步是不是可以建立TCP连接了呢?
答案依然是“不行”。Chrome有个机制,同一个域名同时最多只能建立6个TCP连接,如果在同一个域名下同时有10个请求发生,那么其中4个请求会进入排队等待状态,直至进行中的请求完成。
当然,如果当前请求数量少于6,会直接进入下一步,建立TCP连接。
排队等待结束之后,终于可以快乐地和服务器握手了,在HTTP工作开始之前,浏览器通过TCP与服务器建立连接。而TCP的工作方式,我在上一篇文章中已经做过详细介绍了,如果有必要,你可以自行回顾下,这里我就不再重复讲述了。
一旦建立了TCP连接,浏览器就可以和服务器进行通信了。而HTTP中的数据正是在这个通信过程中传输的。
你可以结合下图来理解,浏览器是如何发送请求信息给服务器的。
首先浏览器会向服务器发送请求行,它包括了请求方法、请求URI(Uniform Resource Identifier)和HTTP版本协议。
发送请求行,就是告诉服务器浏览器需要什么资源,最常用的请求方法是Get。比如,直接在浏览器地址栏键入极客时间的域名(time.geekbang.org),这就是告诉服务器要Get它的首页资源。
另外一个常用的请求方法是POST,它用于发送一些数据给服务器,比如登录一个网站,就需要通过POST方法把用户信息发送给服务器。如果使用POST方法,那么浏览器还要准备数据给服务器,这里准备的数据是通过请求体来发送。
在浏览器发送请求行命令之后,还要以请求头形式发送其他一些信息,把浏览器的一些基础信息告诉服务器。比如包含了浏览器所使用的操作系统、浏览器内核等信息,以及当前请求的域名信息、浏览器端的Cookie信息,等等。
历经千辛万苦,HTTP的请求信息终于被送达了服务器。接下来,服务器会根据浏览器的请求信息来准备相应的内容。
一旦服务器处理结束,便可以返回数据给浏览器了。你可以通过工具软件curl来查看返回请求数据,具体使用方法是在命令行中输入以下命令:
curl -i https://time.geekbang.org/
注意这里加上了-i
是为了返回响应行、响应头和响应体的数据,返回的结果如下图所示,你可以结合这些数据来理解服务器是如何响应浏览器的。
首先服务器会返回响应行,包括协议版本和状态码。
但并不是所有的请求都可以被服务器处理的,那么一些无法处理或者处理出错的信息,怎么办呢?服务器会通过请求行的状态码来告诉浏览器它的处理结果,比如:
状态码类型很多,这里我就不过多介绍了,网上有很多资料,你可以自行查询和学习。
随后,正如浏览器会随同请求发送请求头一样,服务器也会随同响应向浏览器发送响应头。响应头包含了服务器自身的一些信息,比如服务器生成返回数据的时间、返回的数据类型(JSON、HTML、流媒体等类型),以及服务器要在客户端保存的Cookie等信息。
发送完响应头后,服务器就可以继续发送响应体的数据,通常,响应体就包含了HTML的实际内容。
以上这些就是服务器响应浏览器的具体过程。
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接。不过如果浏览器或者服务器在其头信息中加入了:
Connection:Keep-Alive
那么TCP连接在发送后将仍然保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求。保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。比如,一个Web页面中内嵌的图片就都来自同一个Web站点,如果初始化了一个持久连接,你就可以复用该连接,以请求其他资源,而不需要重新再建立新的TCP连接。
到这里似乎请求流程快结束了,不过还有一种情况你需要了解下,比如当你在浏览器中打开geekbang.org后,你会发现最终打开的页面地址是 https://www.geekbang.org。
这两个URL之所以不一样,是因为涉及到了一个重定向操作。跟前面一样,你依然可以使用curl来查看下请求geekbang.org 会返回什么内容?
在控制台输入如下命令:
curl -I geekbang.org
注意这里输入的参数是-I
,和-i
不一样,-I
表示只需要获取响应头和响应行数据,而不需要获取响应体的数据,最终返回的数据如下图所示:
从图中你可以看到,响应行返回的状态码是301,状态301就是告诉浏览器,我需要重定向到另外一个网址,而需要重定向的网址正是包含在响应头的Location字段中,接下来,浏览器获取Location字段中的地址,并使用该地址重新导航,这就是一个完整重定向的执行流程。这也就解释了为什么输入的是 geekbang.org,最终打开的却是 https://www.geekbang.org 了。
不过也不要认为这种跳转是必然的。如果你打开 https://12306.cn,你会发现这个站点是打不开的。这是因为12306的服务器并没有处理跳转,所以必须要手动输入完整的 https://www.12306.cn 才能打开页面。
说了这么多,相信你现在已经了解了HTTP的请求流程,那现在我们再回过头来看看文章开头提出的问题。
如果第二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。
那么,哪些数据会被缓存呢?从上面介绍的核心请求路径可以发现,DNS缓存和页面资源缓存这两块数据是会被浏览器缓存的。其中,DNS缓存比较简单,它主要就是在浏览器本地把对应的IP和域名关联起来,这里就不做过多分析了。
我们重点看下浏览器资源缓存,下面是缓存处理的过程:
首先,我们看下服务器是通过什么方式让浏览器缓存数据的?
从上图的第一次请求可以看出,当服务器返回HTTP响应头给浏览器时,浏览器是通过响应头中的Cache-Control字段来设置是否缓存该资源。通常,我们还需要为这个资源设置一个缓存过期时长,而这个时长是通过Cache-Control中的Max-age参数来设置的,比如上图设置的缓存过期时间是2000秒。
Cache-Control:Max-age=2000
这也就意味着,在该缓存资源还未过期的情况下, 如果再次请求该资源,会直接返回缓存中的资源给浏览器。
但如果缓存过期了,浏览器则会继续发起网络请求,并且在HTTP请求头中带上:
If-None-Match:"4f80f-13c-3a1xb12a"
服务器收到请求头后,会根据If-None-Match的值来判断请求的资源是否有更新。
关于缓存的细节内容特别多,具体细节你可以参考这篇 HTTP缓存,在这里我就不赘述了。
简要来说,很多网站第二次访问能够秒开,是因为这些网站把很多资源都缓存在了本地,浏览器缓存直接使用本地副本来回应请求,而不会产生真实的网络请求,从而节省了时间。同时,DNS数据也被浏览器缓存了,这又省去了DNS查询环节。
通过上面的介绍,你已经了解了缓存是如何工作的。下面我们再一起看下登录状态是如何保持的。
Set-Cookie: UID=3431uad;
UID=3431uad
保持到本地。Cookie: UID=3431uad;
UID=3431uad
的信息时,服务器查询后台,并判断该用户是已登录状态,然后生成含有该用户信息的页面数据,并把生成的数据发送给浏览器。好了,通过这个流程你可以知道浏览器页面状态是通过使用Cookie来实现的。Cookie流程可以参考下图:
简单地说,如果服务器端发送的响应头内有 Set-Cookie 的字段,那么浏览器就会将该字段的内容保持到本地。当下次客户端再往该服务器发送请求时,客户端会自动在请求头中加入 Cookie 值后再发送出去。服务器端发现客户端发送过来的Cookie后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到该用户的状态信息。
本篇文章的内容比较多、比较碎,但是非常重要,所以我先来总结下今天的主要内容。
为了便于你理解,我画了下面这张详细的“HTTP请求示意图”,用来展现浏览器中的HTTP请求所经历的各个阶段。
从图中可以看到,浏览器中的HTTP请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备IP和端口、等待TCP队列、建立TCP连接、发起HTTP请求、服务器处理请求、服务器返回请求和断开连接。
然后我还通过HTTP请求路径解答了两个经常会碰到的问题,一个涉及到了Cache流程,另外一个涉及到如何使用Cookie来进行状态管理。
通过今天系统的讲解,想必你已经了解了一个HTTP完整的工作流程,相信这些知识点之于你以后的学习或工作会很有帮助。
另外,你应该也看出来了本篇文章是有很多分析问题的思路在里面的。所以在学习过程中,你也要学会提问,通过最终要做什么和现在有什么,去一步步分析并提出一些问题,让疑问带领着你去学习,抓住几个本质的问题就可以学透相关知识点,让你能站在更高维度去查看整体框架。希望它能成为你的一个学习技巧吧!
最后,还是留给你个思考题:结合今天所讲HTTP请求的各个阶段,如果一个页面的网络加载时间过久,你是如何分析卡在哪个阶段的?
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。
评论