langliu1216@gmail.com

透视 HTTP 协议 - 2023/06/21

在我们日常的生活与工作中离不开网络的支持,在这个过程中会不停的使用到 HTTP 请求,而作为一个前端开发更是会经常使用到 HTTP 与后端开发进行对接联调,HTTP 使用起来非常简单,如果有人问你 HTTP 相关的知识你也能说出不少内容来,但是我们日常接触到的仅仅是 HTTP 的冰山一角,本文从 HTTP 的历史出发,一步步讲解 HTTP 的演变过程,让你在这个过程中全面的了解 HTTP 为什么这么设计

连接管理

HTTP 中的短连接与长连接

短连接

在 HTTP/1.1 之前的版本中,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接。 因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“短连接”(short-lived connections)。

短连接的缺点相当严重,因为在 TCP 协议里,建立连接和关闭连接都是非常“昂贵”的操作。TCP 建立连接要有“三次握手”,发送 3 个数据包,需要 1.5 个 RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。

而 HTTP 的一次简单“请求 - 响应”通常只需要 4 个包,如果不算服务器内部的处理时间,最多是 2 个 RTT。这么算下来,浪费的时间就是“3.5÷5.5=64%”,有约三分之二的时间被浪费掉了,传输效率低得惊人。

RTT 指的是“往返时延”(Round-Trip Time),即从发送方发送数据开始,到发送方接收到来自接收方的确认消息所经过的时间。RTT 时延通常由三部分决定:链路的传播时间、末端系统的处理时间、路由器等网络中间节点的缓存和排队时间。正常情况下报文的传输时间和在应用处理时间相对固定,在网络拥堵情况下会出现 RTT 时延的波动。

长连接

短连接有两个比较大的问题:创建新连接耗费的时间尤为明显,另外 TCP 连接的性能只有在该连接被使用一段时间后(热连接)才能得到改善。为了缓解这些问题,长连接的概念便被设计出来了。

一个长连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间,还可以利用 TCP 的性能增强能力。当然这个连接也不会一直保留着:连接在空闲一段时间后会被关闭(服务器可以使用 Keep-Alive 协议头来指定一个最小的连接保持时间)。

长连接也是有缺点的;就算是在空闲状态,它还是会消耗服务器资源,而且在重负载时,还有可能遭受 DoS 攻击。这种场景下,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。

在 HTTP/1.1 及之后的版本中,长连接默认是启用的。既然长连接有缺点,那么我们就可能需要在某些场景下手动关闭长连接,这就需要借助于 HTTP 请求头中的 Connection{:yaml} 字段。

在客户端请求时在请求头里加上 Connection: close{:yaml} 字段,告诉服务器需要在本次请求后关闭长连接,服务器接受到这个请求后在响应报文里也加上这个字段,发送后就调用 Socket API 关闭 TCP 连接。

客户端通常不会去关闭连接,服务器通常也不会主动关闭连接,那在实际的开发中就不去处理这块的优化吗?也不是的,虽然服务器不会主动关闭连接,但是像 Nginx 这类型的服务器都提供了策略去优化连接。

在 Nginx 中提供了两种方式处理主动断开连接:

  1. 使用 keepalive_timeout{:yaml} 指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。(默认 75s)
  2. 使用 keepalive_requests{:yaml} 指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。(默认 100)

短连接vs长连接

域名分片

作为 HTTP/1.x 的连接,请求是序列化的,哪怕本来是无序的,在没有足够庞大可用的带宽时,也无从优化。一个解决方案是,浏览器为每个域名建立多个连接,以实现并发请求。曾经默认的连接数量为 2 到 3 个,现在比较常用的并发连接数已经增加到 6 条。如果尝试大于这个数字,就有触发服务器 DoS 保护的风险。

如果服务器端想要更快速的响应网站或应用程序的应答,它可以迫使客户端建立更多的连接。例如,不要在同一个域名下获取所有资源,假设有个域名是 www.example.com ,我们可以把它拆分成好几个域名:www1.example.com、www2.example.com、www3.example.com。所有这些域名都指向同一台服务器,浏览器会同时为每个域名建立 6 条连接。这一技术被称作域名分片。

除非你有紧急而迫切的需求,不要使用这一过时的技术;而是升级到 HTTP/2。在 HTTP/2 里,做域名分片就没必要了:HTTP/2 的连接可以很好的处理并发的无优先级的请求。域名分片甚至会影响性能。大多数 HTTP/2 的实现还会使用一种称作连接聚合的技术去尝试合并被分片的域名。

队头阻塞

队头阻塞与短连接和长连接无关,而是由 HTTP 基本的“请求-应答”模型所导致的。

因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。 如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

短连接 VS 长连接

HTTP 的重定向与跳转

跳转

在网页中用户由一个页面进入到另一个页面有两种方式:

  1. 用户主动点击网页中的链接进行跳转,这种方式叫“主动跳转”;
  2. 当用户发送一个请求时,服务器控制进入另一个页面,浏览器无法控制,这种方式叫“被动跳转”,也称为“重定向”。

重定向

重定向按照时间的长短分为永久重定向临时重定向,在 HTTP 的状态码中: 301{:bash} 是永久重定向, 302{:bash} 是临时重定向,浏览器收到这两个状态码就会跳转到新的 URI。重定向的 URI 在 HTTP 请求的响应头中以 Location{:yaml} 来表示。

Location{:yaml} 字段必须配合 3xx{:bash} 状态码才会生效。

Location{:yaml} 里的 URI 既可以使用绝对 URI,也可以使用相对 URI。所谓“绝对 URI”,就是完整形式的 URI,包括 scheme、host:port、path 等。所谓“相对 URI”,就是省略了 scheme 和 host:port,只有 path 和 query 部分,是不完整的,但可以从请求上下文里计算得到。

# 相对URI
Location: /index.html

# 绝对URI
Location: https://www.baidu.com/index.html

重定向的应用

重定向的应用主要是区分好“永久”和“临时”,比如我们有一个网站上线了一个活动页面,活动结束后如果用户再次通过链接访问了这个页面,我们需要将用户重定向到主页,从需求来看如果这个活动页面下架之后再也不会上线,那么我们就需要永久重定向,如果过一段时间还会再次上线的话我们需要使用临时重定向。

重定向的问题

  • 性能损耗:重定向会在前一个请求成功后在会进行重定向,会比正常的跳转多一次请求;
  • 循环跳转:如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,不过现代浏览器都提供了循环跳转的检测功能,只要发现有循环跳转的情况都会中止并给出错误提示。

HTTP 中的身份识别

HTTP 是无状态的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群,而缺点就是无法支持需要记录状态的事务操作。

在实际的网页开发中我们有很多的页面都需要判断用户的身份,只有通过授权的用户才能访问,可是 HTTP 是无状态的,难道我们每次请求的时候都需要手动带上身份标识吗?为了简化这个操作,HTTP 提出了 Cookies 的概念,在请求的时候可以自动带上 Cookies 中的内容。

什么是 Cookies?

HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

Cookies

Cookies 是由服务端设置值,然后发送到浏览器器由浏览器进行存储的。服务端在 HTTP 请求的响应头中通过 Set-Cookie{:yaml} 属性设置 Cookie 的值,值的类型为 key=value{:yaml},可以同时设置多个键值对,多个键值对之间以 ;{:bash} 进行分割。浏览器发现请求的响应头中有 Set-Cookie{:yaml} 字段后会自动存储 Cookie 并且在下一次请求时将刚刚存储的 Cookie 放入 HTTP 请求头中的 Cookie{:yaml} 字段中,服务器在接收到请求时解析请求头中的 Cookie{:yaml} 字段就可以判别这个请求对应的用户身份了。

属性描述
Expires{:yaml}过期时间
Max-Age{:yaml}有效期
Secure{:yaml}标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端
HttpOnly{:yaml}JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。
Domain{:yaml}指定了哪些主机可以接受 Cookie
Path{:yaml}Path 属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie 标头。以字符 %x2F (“/”) 作为路径分隔符,并且子路径也会被匹配。
SameSite{:yaml}SameSite 属性允许服务器指定是否/何时通过跨站点请求发送(其中站点由注册的域和方案定义:http 或 https)。这提供了一些针对跨站点请求伪造攻击(CSRF)的保护。它采用三个可能的值:StrictLaxNone

相关阅读 📖