蛮荆

HTTP 鲜为人知的细节

2018-02-16

连接类型

1. 短连接与长连接

当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问的 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。

长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。

  • 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close
  • 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 Connection : Keep-Alive

长连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外,减少开销的那部分时间,使 HTTP 请求和响应能够更早地结束,这样 Web 页面的显示速度也就相应提高了,在 HTTP/1.1 中,所有的连接默认都是长连接

2. Pipelining

默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。

流水线是在同一条长连接上连续发出请求,而不用等待响应返回,这样可以减少延迟。

持久化依然是顺序请求,管道化可以并发请求。

图片来源: 图解 HTTP


Request Method

幂等

一个 HTTP 方法是 幂等 的,指的是: 相同的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用。

例如 GET 请求方法是幂等的,连续调用多次,客户端接收到的结果都是一样的。

GET https://dbwu.tech/

例如 POST 请求方法不是幂等的,如果调用多次,就会增加多行记录。

POST https://dbwu.tech/   -> 新增 1 条记录
POST https://dbwu.tech/   -> 新增 2 条记录
POST https://dbwu.tech/   -> 新增 3 条记录

同样,DELETE 请求方法也是幂等的,即使不同的请求接收到的状态码不一样。

DELETE https://dbwu.tech/1024   -> 记录已删除
DELETE https://dbwu.tech/1024   -> 404 (因为记录已删除)
DELETE https://dbwu.tech/1024   -> 404 (因为记录已删除)

安全

一个 HTTP 方法是 安全 的,指的是: 这个方法不会修改服务端的数据。也就是说,这是一个对服务端数据只读操作的方法。

所有安全的方法都是幂等的,但并非所有幂等方法都是安全的。

请求方法列表

动词 描述 幂等 安全性
GET 请求指定的资源
POST 发送数据给服务器. 请求主体的类型由 Content-Type 首部指定
PUT 创建或者替换目标资源
PATCH 对资源进行部分修改
DELETE 删除指定的资源
HEAD 请求资源的头部信息, 并且这些头部与 HTTP GET方法请求时返回的一致
OPTIONS 用于获取目的资源所支持的通信选项

PUT && POST

PUT 与 POST 方法的区别在于,PUT 方法是幂等的,调用一次与连续调用多次是等价的(即没有副作用),而连续调用多次 POST 方法可能会有副作用 (比如将一个订单重复提交多次)。


不常见请求方法

下面列出几个属于 HTTP 协议,但是几乎用不到的请求方法。

TRACE

TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。

图片来源: 图解 HTTP

发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器端就将该数字减1,当数值刚好减到 0 时,就停止继续传输,最后接收到请求的服务器端则返回状态码 200 OK 的响应。

图片来源: 图解 HTTP

从应用角度来看,TRACE 方法可以理解为 HTTP 协议提供的 “链路追踪” 功能,当然,现在链路追踪已经被标准化为 OpenTelemetry

客户端通过 TRACE 方法可以查询发送出去的请求是怎样被加工修改/篡改的,这是因为: 请求想要连接到源目标服务器可能会通过代理中转,TRACE 方法就是用来确认连接过程中发生的一系列操作。但是,TRACE 方法不是常见的方法,再加上它容易引发 XST(Cross-SiteTracing,跨站追踪)攻击,所以实际开发中不会被用到 (读者当个八卦看看就行)。

CONNECT

CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。主要使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全,也就是 SSL 的继任者)协议把通信内容加密后经网络隧道传输。

图片来源: 图解 HTTP


GET & POST 差异

GET 请求用于获取资源,而 POST 请求用于传输实体主体。

参数编码

GET 和 POST 请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中,不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。

因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如 中文 会转换为 %E4%B8%AD%E6%96%87,而空格会转换为 %20,POST 参数支持标准字符集。

参数长度限制

此外,GET 请求在 URL 中传送的参数是有长度限制的,而 POST 请求没有,具体的限制 可以点击这个链接

如果 GET 请求数据既超过浏览器限制长度,同时也超过服务器限制长度,多数就会截掉超出的长度且并没任何警告,一些服务器也许会发送一个 414 响应码 (Status Request URI Too Long)。最佳实践: 遵从 RESTFul 设计规范,保持 GET 请求参数的短小精悍。

参数保留

GET 请求参数会被完整保留在浏览器历史记录里,而 POST 请求中的参数不会被保留。

XMLHttpRequest

为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:

XMLHttpRequest 是一个浏览器 API,它为客户端提供了在客户端和服务器之间传输数据的功能。通过提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新,使得网页只更新一部分页面而不会打扰到用户,转换为应用 API, 也就是前端中大量用到的 AJAX。

  • 在使用 XMLHttpRequest 发送 POST 请求时,浏览器会先发送 Header 再发送 Data, 但并不是所有浏览器会这么做 (例如 FireFox)
  • 在使用 XMLHttpRequest 发送 GET 请求时,Header 和 Data 会一起发送

也就是说,GET 产生一个 TCP 数据包,POST 产生两个 TCP 数据包 (先询问 -> 100 continue -> 发送数据)。

100 Continue 的作用: HTTP 客户端应用程序有一个实体的主体部分要发送服务器,但希望在发送之前,确认服务器是否会接受这个实体。

如果客户端在向服务器发送一个实体,并愿意在发送实体之前等待 100 Continue 响应,那么客户端就要发送一个携带了值为 Expect: 100 Continue 的请求头, 如果客户端没有发送实体,就不应该发送 Expect: 100 Continue 的请求头,避免服务器误认为客户端要发送一个实体。

对于 POST 请求来说,浏览器先发送 Header,服务器响应 100 continue,浏览器再发送 Data,服务器响应 200 OK。

如果服务器收到一条带有 Expect: 100 Continue 的请求头,它会用 100 Continue 或一条错误码来进行响应,服务器永远也不应该向没有发送 100 Continue 期望的客户端发送 100 Continue 状态码。


StatusCode

HTTP/1.1 警告码

图片来源: 图解 HTTP

206

返回指定资源的部分内容 (RFC 9110, 15.3.7)。

接收到附带 Range 请求头时,服务器会在处理请求之后返回状态码为 206 (Partial Content) 的响应,无法处理该范围请求时,则会返回状态码 200 OK 的响应和资源全部内容。

在实现类似 断点续传分片 (小文件) 下载 功能时,会使用到这个状态码。

分片下载响应示例

# 请求示例
GET /largefile.txt HTTP/1.1

Host: example.com
Range: bytes=0-499
Accept-Ranges: bytes
If-Range: "123456789"
# 响应示例
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-499/123456
Content-Length: 500
Content-Type: text/plain
[response body: bytes 0-499 of the resource]

3XX 重定向

  • 301 永久重定向
  • 302 临时重定向

302 和 303 状态码有着相同的功能,但 303 状态码明确表示客户端应当采用 GET 方法获取资源,这点与 302 状态码有区别。

当 301、302、303响应状态码返回时,几乎所有的浏览器都会把 POST 改成 GET,并删除请求报文内的主体,之后请求会自动再次发送,但其实 301、302标准是禁止将 POST 方法改变成 GET 方法的。

307 会遵照浏览器标准,不会从 POST 请求变成 GET 请求。

503

响应头 Retry-After 告知客户端应该在多久之后再次发送请求,主要配合 503 (Service Unavailable) ,或 3xx (Redirect) 状态码一起返回。


避免重定向

访问 HTTP 站点,然后 (重定向) 跳转到 HTTPS,重定向意味着要重新发起请求

为了避免这种跳转,可以用 HSTS (HTTP Strict Transport Security) 策略,就是告诉浏览器,以后访问这个站点时,必须使用 HTTPS 协议来访问,让浏览器帮忙做转换,而不是请求到了服务器后,才知道要转换,只需要在响应头部加上 Strict-Transport-Security: max-age=31536000 。

缓存控制 Cache-Control

禁止缓存

no-store 指令规定不能对请求或响应的任何一部分进行缓存。

Cache-Control: no-store

强制确认缓存

no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效时才能使用该缓存对客户端的请求进行响应。

Cache-Control: no-cache

私有缓存和公共缓存

private 指令规定了将资源作为私有缓存,只能被单独用户使用,一般存储在用户浏览器中。

Cache-Control: private

public 指令规定了将资源作为公共缓存,可以被多个用户使用,一般存储在代理服务器中。

Cache-Control: public

缓存过期机制

max-age 指令出现在响应报文,表示缓存资源在缓存服务器中保存的时间,如果缓存资源的缓存时间小于该指令指定的时间,那么就可以使用该缓存。

Cache-Control: max-age=3600

表示响应在被接收后可以被缓存 1 小时。

Cache-Control: max-age=0

表示响应在被接收后立即过期,不应该被缓存。

Expires 首部字段可以用于告知缓存服务器该资源什么时候会过期。

Expires: Wed, 04 Jul 2018 08:26:05 GMT

  • 在 HTTP/1.1 中,会优先处理 max-age 指令
  • 在 HTTP/1.0 中,max-age 指令会被忽略

缓存验证

图片来源: 图解 HTTP

需要先了解 ETag 首部字段的含义,它是资源的唯一标识,URL 不能唯一表示资源,例如 https://dbwu.tech/ 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。

ETag: "82e22293907ce725faf67773957acd12"

可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。

If-None-Match: "82e22293907ce725faf67773957acd12"

Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有实体主体的 304 Not Modified 响应报文。

Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT

缓存最佳实践

服务端大部分响应在响应头中应该携带 Last-Modified, ETag, Vary, Date 信息,客户端可以在随后请求这些资源的时候, 在请求头中使用 If-Modified-Since, If-None-Match 等请求头来确认资源是否经过修改,如果资源没有进行过修改,那么就可以响应 304 Not Modified 并且不在响应实体中返回任何内容。

  1. 客户端第一次访问服务端请求数据,服务端返回响应数据 (缓存有效期 60 秒)
$ curl -i https://dbwu.tech/
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2018 15:31:30 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2018 15:31:30 GMT
  1. 客户端第二次访问服务端请求数据,服务端返回 304 和缓存数据
$ curl -i https://dbwu.tech/ -H "If-Modified-Since: Thu, 05 Jul 2018 15:31:30 GMT"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2018 15:31:45 GMT
Vary: Accept, Authorization
Last-Modified: Thu, 05 Jul 2018 15:31:30 GMT
  1. 客户端第三次访问服务端请求数据,服务端依然返回 304 和缓存数据 (因为缓存有效期 60 秒)
$ curl -i https://dbwu.tech/ -H 'If-None-Match: "644b5b0155e6404a9cc4bd9d8b1ae730"'
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2018 15:31:55 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2018 15:31:30 GMT

可缓存

如果要对服务端响应进行缓存,需要满足以下条件:

  • 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在大多数情况下也是不可缓存的 (非幂等语义)
  • 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501
  • 响应报文的 Cache-Control 字段没有禁止缓存

代理

代理服务器的基本行为就是接收客户端发送的请求,然后转发给其他服务器,代理不改变请求 URI,会直接发送给前方持有资源的目标服务器。

图片来源: 图解 HTTP

正向代理

正向代理,即代理客户端的请求。

正向代理是一个位于客户端和原始服务器之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标服务器,然后代理向原始服务器转发请求并将获得的响应内容返回给客户端。客户端必须要进行一些特别的设置才能使用正向代理,换句话说,用户察觉得到正向代理的存在。

作用:

  • 访问客户端原本无法访问的服务器 (VPN)
  • 网络提速 (游戏加速器)
  • 缓存
  • 客户端访问授权
  • 隐藏客户端信息

反向代理

反代理,即代理服务器的响应。

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器,来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。反向代理一般位于内部网络中,用户察觉不到其存在。

代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器;并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

作用:

  • 负载均衡
  • 过配置缓存功能加速 Web 请求:可以缓存真实 Web 服务器上的某些静态资源,减轻真实 Web 服务器的负载压力
  • 增加安全性: 保护和隐藏原始资源服务器,屏蔽黑名单中的 IP,限制每个客户端的连接数
  • 提高可扩展性和灵活性: 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置
  • 本地终结 SSL 会话: 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作
  • 免除了在每个后端服务器上安装 X.509 证书的需要
  • 压缩: 压缩服务器响应
  • 缓存: 直接返回命中的缓存结果
  • 静态内容: 直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

带来的问题

  • 引入反向代理会增加系统的复杂度
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如实现 故障转移)会进一步增加复杂度

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上
  • 即使只有一台 Web 服务器或者应用服务器时,反向代理也有用
  • NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡

HTTP 隧道

HTTP tunnel 是 HTTP/1.1 中引入的一个功能,主要为了解决明文的 HTTP Proxy 无法代理跑在 TLS 中的流量(也就是 HTTPS )的问题,同时提供了作为任意流量的 TCP 通道的能力。

由于 CONNECT 请求报文还是 HTTP 方式,所以也可以跑在 TLS 里,也就是说: 客户端和代理服务器之间有一个 TLS 层,在这个 TLS 层里面包含着客户端和远端服务器的 TLS 层。这样当代理服务器需要用户名密码验证,而验证方式又是 Basic 时,就可以通过 TLS 来保护代理服务器请求时,报文中的明文用户名密码。

HTTP/1.1 语法如下:

HTTP/2 语法如下:

为什么需要 HTTP tunnel?可以看看 这篇文章


WebSockets

对于较小的消息 (数据) 传输,WebSocket 会比 HTTP 更加高效,因为省去了 HTTP 首部的开销,而且 WebSocket 支持全双工通信。对于实时类要求较高的场景,通常使用 WebSocket 替换 HTTP。

HTTP 和 WebSocket 的比较

HTTP 和 WebSocket 的比较

从 HTTP 升级连接到 WebSockets

  1. 直接发送普通的 HTTP GET 请求
  2. 在请求头中包含升级(Upgrade Header)到 WebSocket 协议

客户端请求

服务器响应

从 HTTP/2 升级连接到 WebSockets

在整个通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流 (Stream)。

HTTP/2 需要升级整个 HTTP 连接 (也就是 TCP 连接上所有的数据流,这完全没有必要),因为仅需要一个单独的数据流来负责 WebSockets 数据传输就可以了,所以方法和 HTTP/1 略微不同:

客户端请求

服务器响应

WebSocket (单独的) 数据流建立完成之后,可以在该流的两个方向上传输 WebSocket 数据,该连接上的其他数据流可以继续处理 HTTP 请求,当然,可以继续使用类似方式继续建立其他 WebSocket 通信,甚至其他的协议。


跨域问题及其解决方案

跨域问题及其解决方案

上述思维导图免费下载: https://www.processon.com/view/link/65c223109d45c83bfb5b0b6e 访问密码:9IX9


Reference

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。