超文本传输协议(HTTP)作为目前应用最为广泛的应用层协议,在网站、App 以及开放接口中随处可见。虽然 HTTP 协议设计简洁,但涵盖内容丰富。下面我们将深入探讨 HTTP 协议的重点内容,包括高频面试要点及易产生理解误区之处。
一、WWW 的诞生
1990 年,蒂姆·伯纳斯·李开发出首个浏览器,编写了首个 Web 服务器程序并创建了第一张网页。其中网页所用语言后来被称为超文本标记语言(HTML)。在服务器与客户端之间传输网页时,伯纳斯·李并未直接采用传输层协议,而是在 TCP 基础上构建了应用层协议,即超文本传输协议 HTTP。
万维网(World Wide Web,WWW)是伯纳斯·李对包括 Web 服务、HTTP 协议、HTML 语言等一系列发明的综合体系。
二、请求响应与长连接
HTTP 协议采用请求/返回模型。客户端(通常为浏览器)发起 HTTP 请求,Web 服务端收到请求后将数据回传。HTTP 的请求和响应均为文本,可简单理解为 HTTP 协议利用 TCP 协议传输文本。当用户想要浏览网页时,便发送文本请求至 Web 服务器,服务器解析后将网页回传给浏览器。
此时产生一个问题,是否每次发送请求都要建立一个 TCP 连接呢?显然不能如此,为节省握手、挥手的时间,当浏览器向 Web 服务器发送请求时,服务器内部会设置一个定时器。在特定时间范围内,如果客户端继续发送请求,服务器则重置定时器;若在该时间范围内服务器未收到请求,便会断开连接。这样既能避免浪费握手、挥手的资源,又可防止连接占用时间过长导致内存使用效率降低。
此功能可通过 HTTP 协议头进行配置,例如这条请求头:
Keep-Alive: timeout=5s,
它告知 Web 服务器连接的持续时间为 5 秒,若 5 秒内无请求,连接将断开。
三、HTTP 2.0 的多路复用
Keep-Alive 并非伯纳斯·李在设计 HTTP 协议之初就具备的功能。伯纳斯·李设计的第一版 HTTP 协议为 0.9 版,随后逐渐完善至 1.0 版。而 Keep-Alive 是在 HTTP 1.1 版中新增的功能,旨在应对日益复杂的网页资源加载需求。自 HTTP 协议诞生以来,网页所需资源愈发丰富,打开一张页面所需发送的请求也越来越多,于是便有了 Keep-Alive 的设计。
同样,当一个网站需要加载的资源较多时,浏览器会利用多线程技术尝试并发发送请求。浏览器通常会限制同时发送的并发请求数量为 6 个,这样做一方面是为了保护用户本地体验,防止浏览器抢占过多网络资源;另一方面也是对站点服务的保护,避免瞬时流量过大。
在 HTTP 2.0 之后,引入了多路复用能力。与我们之前讲解 RPC 框架时提到的多路复用类似,请求和返回会被拆分成切片,然后混合传输。如此一来,请求与返回之间便不会相互阻塞。可以思考一下,在 HTTP 1.1 的 Keep-Alive 设计中,对于一个 TCP 连接,第二个请求必须等待第一个请求返回。若第一个请求阻塞,后续所有请求都会被阻塞。而在 HTTP 2.0 的多路复用中,将请求和返回都切分成小片,利用同一个连接,请求相当于并行发出,互不干扰。
四、HTTP 方法与 RestFul 架构
随着 HTTP 的发展,诞生了一些著名架构,如 RestFul。在面试中,RestFul 经常被提及。RestFul 是三个单词的合并缩写:
- Re(Representational)、
- st(State)、
- Ful(Transfer)
这种命名方式非常有趣,让人联想到 grep 命令的命名方式——global regular pattern match。这是一种高端的命名技巧,提取词汇中的部分组合成一个朗朗上口的新词汇,建议在实际命名中也可以尝试。
在 RestFul 架构中,状态仅存在于服务端,前端无状态。这里的状态(State)可理解为业务状态,由服务端管理。这与服务端目前倡导的无状态设计并不冲突,现在服务端倡导的无状态设计是指容器内的服务没有状态,状态全部存储在合适的存储中。所以 Restful 中的 State 指的是服务端状态。
前端(浏览器、应用等)没有业务状态,但又需要展示内容,因此前端拥有的是状态的表示,即 Representation。例如一个订单,状态存储在服务端(数据库中),前端展示订单只需部分信息,无需全部信息。前端只需要展示数据,而展示数据需由服务端提供。所以服务端提供的不是状态,而是状态的表示。
前端无状态,当用户想要改变订单状态(如支付)时,前端会向服务端提交表单,服务端触发状态变化。这个过程称为转化(Transfer)。从这个角度看,Restful 讲述的是一套前端无状态、服务端管理状态,并设计中间转化途径(请求、函数等)的架构方法。这种方法能使前后端职责清晰,前端负责渲染,服务端负责业务。前端无需关注业务状态,只需进行展示。服务端除了关心状态,还需提供状态的转换接口。
五、HTTP 方法
在 Restful 架构中,除了约定整体架构方案外,还对一些实现细节进行了约束,例如用名词性的接口和 HTTP 方法来设计服务端提供的接口。
我们使用 GET 获取数据或进行查询。比如 GET /order/123,这里 GET 是 HTTP 方法,/order 是名词性质的命名。这样设计语义清晰,该接口用于获取订单数据(即订单的 Representation)。
对于更新数据的场景,根据 HTTP 协议约定,PUT 是一种幂等的更新行为,POST 是一种非幂等的更新行为。例如,PUT /order/123 {...订单数据},若订单 123 尚未创建,此接口会创建订单;若订单 123 已存在,则更新该订单数据。因为 PUT 代表幂等,对于幂等接口,无论请求多少次,最终状态一致,即操作的都是同一笔订单。
若换成用 POST 更新订单:POST /order {...订单数据}。POST 代表非幂等设计,像这种用 POST 提交表单的接口,多次调用往往会产生多个订单。即非幂等设计每次调用结束后都会产生新的状态。
此外,HTTP 协议中还约定了 DELETE 方法用于删除数据。其实还有其他一些方法,如 OPTIONS、PATCH,感兴趣的小伙伴可以查询了解。
六、缓存
在 HTTP 的使用中,我们经常会遇到两种缓存:强制缓存和协商缓存。下面通过两个场景进行说明。
1、强制缓存
假设你的公司通过版本号管理某个对外提供的 JS 文件,例如 libgo.1.2.3.js 是 libgo 的 1.2.3 版本,其中 1 是主版本,2 是副版本,3 是补丁编号。每次有任何改动,都会更新 libgo 版本号。在这种情况下,当浏览器请求了一次 libgo.1.2.3.js 文件后,还需要再次请求吗?
整理需求可知,浏览器在第一次进行 GET /libgo.1.2.3.js 操作后,如果后续某个网页还用到这个文件,我们不再发送第二次请求。这种请求过一次便无需再次发送请求的缓存模式在 HTTP 协议中称为强制缓存。当一个文件被强制缓存后,下一次请求会直接使用本地版本,而不会真正发出请求。
使用强制缓存时需注意,切勿将需要动态更新的数据进行强制缓存。一个负面例子是小明将获取用户信息数据的接口设置为强制缓存,导致用户更新信息后,需等到强制缓存失效才能看到此次更新。
2、协商缓存
再看一个场景:小明开发了一个接口,提供全国省市区的三级信息。首先思考一个问题,这个场景可以使用强制缓存吗?小明起初认为强制缓存可行,但突然有一天接到运营通知,某市下属的两个县合并了,需要调整接口数据。小明措手不及,更新了接口数据,但数据要等到强制缓存失效才能生效。
为应对这种场景,HTTP 协议设计了协商缓存。启用协商缓存后,第一次获取接口数据时,会将数据缓存到本地,并存储下数据的摘要。第二次请求时,浏览器检查到本地有缓存,将摘要发送给服务端。服务端会检查服务端数据的摘要与浏览器发送来的是否一致。若不一致,说明服务端数据已更新,服务端会回传全部数据;若一致,说明数据未更新,服务端无需回传数据。
从这个角度看,协商缓存节省了流量。对于小明开发的这个接口,多数情况下协商缓存会生效。当小明更新数据后,协商缓存失效,客户端数据可以马上更新。与强制缓存相比,协商缓存的代价是需要多发一次请求。
文章(专栏)将持续更新,欢迎关注公众号:服务端技术精选。欢迎点赞、关注、转发。
个人小工具程序上线啦,通过公众号(服务端技术精选)菜单【个人工具】即可体验,欢迎大家体验后提出优化意见!