You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

253 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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