HTTP的实体数据

TCP协议是传输层的协议,所以TCP只关心文件是否送达,而不关心内容是什么。

对于HTTP而言,由于他说应用层协议,所以必须要关系传输内容,从而告诉上层应用这是什么数据。

同样的,上层应用也应该告诉HTTP协议,他能接受什么内容。

因此,我们一起来关心HTTP body 数据。

在HTTP body 数据中,HTTP 协议为此定义了两个 Accept 请求头字段和两个 Content 实体头字段,用于客户端和服务器进行“内容协商”。也就是说,客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。

如下图:

Accept 字段标记的是客户端可理解的 MIME type,可以用“,”做分隔符列出多个类型

Accept-Encoding 字段标记的是客户端支持的压缩格式,例如上面说的 gzip、deflate 等,同样也可以用“,”列出多个

同理,Content-Type和Content-Encoding是发送端的相应信息。

MIME 主要有八大类,每个大类下再细分出多个子类,形式是“type/subtype”的字符串,巧得很,刚好也符合了 HTTP 明文的特点,所以能够很容易地纳入 HTTP 头字段里。

这里简单列举一下在 HTTP 里经常遇到的几个类别:

  1. text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。
  2. image:即图像文件,有 image/gif、image/jpeg、image/png 等。
  3. audio/video:音频和视频数据,例如 audio/mpeg、video/mp4 等。
  4. application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json,application/javascript、application/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是 application/octet-stream,即不透明的二进制数据。

Encoding type 就少了很多,常用的只有下面三种:

  1. gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
  2. deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
  3. br:一种专门为 HTTP 优化的新压缩算法(Brotli)。

HTTP传输大文件

对于大数据传输,一个最基本的解决方案,那就是“数据压缩”。

数据压缩

即前面提到的浏览器在发送请求时带着的“Accept-Encoding”头字段,

和服务器从“Accept-Encoding”选择一种的压缩算法,放进“Content-Encoding”字段里,再把原数据压缩后发给浏览器。

不过这个解决方法也有个缺点,gzip 等压缩算法通常只对文本文件有较好的压缩率,而图片、音频视频等多媒体数据本身就已经是高度压缩的,再用 gzip 处理也不会变小(甚至还有可能会增大一点),所以它就失效了。不过数据压缩在处理文本的时候效果还是很好的,所以各大网站的服务器都会使用这个手段作为“保底”。例如,在 Nginx 里就会使用“gzip on”指令,启用对“text/html”的压缩。

分块传输

在响应报文里有头字段“Transfer-Encoding: chunked”,表示分块传输。

意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。

这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省下来了。

分块传输也可以用于“流式数据”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段“Content-Length”里给出确切的长度,所以也只能用 chunked 方式分块发送。

注意 : “Transfer-Encoding: chunked”和“Content-Length”这两个字段是互斥的。

也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点一定要记住。

范围请求

有了分块传输编码,服务器就可以轻松地收发大文件了,但对于上 G 的超大文件,还有一些问题需要考虑。

因为分块传输是按照次序一个一个传输的,如果需要跳过某些数据,获得后面的数据(例如看视频时快进),该怎么办?

答案是 : 范围请求”(range requests)

服务器可以用 Accept-Ranges: noneAccept-Ranges: bytes告诉浏览器其是否支持范围请求。

浏览器用 Range: bytes=x-y来发送范围请求。其中的 x 和 y 是以字节为单位的数据范围。

服务器收到 Range 字段后,需要做四件事。

  1. 它必须检查范围是否合法,比如文件只有 100 个字节,但请求“200-300”,这就是范围越界了。服务器就会返回状态码 416,意思是“你的范围请求有误,我无法处理,请再检查一下”。
  2. 如果范围正确,服务器就可以根据 Range 头计算偏移量,读取文件的片段了,返回状态码“206 Partial Content”,和 200 的意思差不多,但表示 body 只是原数据的一部分。
  3. 服务器要添加一个响应头字段 Content-Range,告诉片段的实际偏移量和资源的总大小,格式是“bytes x-y/length”,与 Range 头区别在没有“=”,范围后多了总长度。例如,对于“0-10”的范围请求,值就是“bytes 0-10/100”。
  4. 最后剩下的就是发送数据了,直接把片段用 TCP 发给客户端,一个范围请求就算是处理完了。

多段数据

刚才说的范围请求一次只获取一个片段,其实它还支持在 Range 头里使用多个“x-y”,一次性获取多个片段数据。

这种情况需要使用一种特殊的 MIME 类型:“multipart/byteranges”,表示报文的 body 是由多段字节序列组成的,并且还要用一个参数“boundary=xxx”给出段之间的分隔标记。

例如 :

请求:

1
2
3
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29 //多段数据请求

响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP/1.1 206 Partial Content
//多字节序列返回与段时间的分割标记
Content-Type: multipart/byteranges; boundary=00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000000001 //分隔标记
Content-Type: text/plain
Content-Range: bytes 0-9/96 //**响应头字段 Content-Range**,告诉片段的实际偏移量和资源的总大小

--00000000001 //分隔标记
Content-Type: text/plain
Content-Range: bytes 20-29/96 //**响应头字段 Content-Range**,告诉片段的实际偏移量和资源的总大小

ext json d
--00000000001--

小结

  1. 压缩 HTML 等文本文件是传输大文件最基本的方法;
  2. 分块传输可以流式收发数据,节约内存和带宽,使用响应头字段“Transfer-Encoding: chunked”来表示;
  3. 范围请求可以只获取部分数据,即“分块请求”,实现视频拖拽或者断点续传,使用请求头字段“Range”和响应头字段“Content-Range”,响应状态码必须是 206;
  4. 也可以一次请求多个范围,这时候响应报文的数据类型是“multipart/byteranges”,body 里的多个部分会用 boundary 字符串分隔。

HTTP连接管理

短连接

HTTP 协议最初(0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求 - 应答”方式。它底层的数据传输基于 TCP/IP,每次发送请求前需要先与服务器建立连接,收到响应报文后会立即关闭连接,所以是“短连接”(short-lived connections)。

早期的 HTTP 协议也被称为是“无连接”的协议。

**短连接的缺点:**严重制约了服务器的服务能力,导致它无法处理更多的请求。(TCP 建立连接要有“三次握手”,发送 3 个数据包,需要 1 个 RTT;关闭连接是“四次挥手”,4 个数据包需要 2 个 RTT。而 HTTP 的一次简单“请求 - 响应”通常只需要 4 个包,如果不算服务器内部的处理时间,最多是 2 个 RTT。每次连接占用的时间太长)

长连接

针对短连接暴露出的缺点,HTTP 协议就提出了“长连接”的通信方式.

其实解决办法也很简单,用的就是“成本均摊”的思路,既然 TCP 的连接和关闭非常耗时间,那么就把这个时间成本由原来的一个“请求 - 应答”均摊到多个“请求 - 应答”上。

长短连接对比如下 :

短连接如下图:

由于长连接对性能的改善效果非常显著,所以在 HTTP/1.1 中的连接都会默认启用长连接。不需要用什么特殊的头字段指定,只要向服务器发送了第一次请求,后续的请求都会重复利用第一次打开的 TCP 连接,也就是长连接,在这个连接上收发数据。

当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是 Connection,值是“keep-alive”

不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“Connection: keep-alive”字段,告诉客户端:“我是支持长连接的,接下来就用这个 TCP 一直收发数据吧”。

长连接的缺点:

因为 TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。

解决办法:

  • 在客户端,可以在请求头里加上“Connection: close”字段,告诉服务器:“这次通信后就关闭连接”

  • 服务器端 ,以Nginx举例:

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

对头阻塞

Head-of-line blocking.

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

因为 HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。

队列里的请求没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求被最优先处理。

如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。

性能优化

因为“请求 - 应答”模型不能变,所以“队头阻塞”问题在 HTTP/1.1 里无法解决,只能缓解,有什么办法呢?

办法一 :

“并发连接”(concurrent connections),也就是同时对一个域名发起多个长连接,用数量来解决质量的问题

但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。

办法二:

“域名分片”(domain sharding),还是用数量来解决质量的思路。是针对并发连接的缺点来做的.

HTTP 协议和浏览器不是限制并发连接数量吗?好,那我就多开几个域名,比如 shard1.chrono.com、shard2.chrono.com,而这些域名都指向同一台服务器 www.chrono.com,这样实际长连接的数量就又上去了。

小结

  1. 早期的 HTTP 协议使用短连接,收到响应后就立即关闭连接,效率很低;
  2. HTTP/1.1 默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率;
  3. 服务器会发送“Connection: keep-alive”字段表示启用了长连接;报文头里如果有“Connection: close”就意味着长连接即将关闭;
  4. 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
  5. “队头阻塞”问题会导致性能下降,可以用“并发连接”和“域名分片”技术缓解。

HTTP的重定向和跳转

重定向过程

我们在《HTTP概览》中就讲过,HTTP状态响应码3XX代表了重定向。这里来详细谈谈。

301 是“永久重定向”,302 是“临时重定向”,浏览器收到这两个状态码就会跳转到新的 URI。

以下是用 Chrome 访问 URI “/18-1”,然后使用 302 立即跳转到“/index.html”的截图。

这一次“重定向”实际上发送了两次 HTTP 请求,第一个请求返回了 302,然后第二个请求就被重定向到了“/index.html”

但如果不用开发者工具的话,你是完全看不到这个跳转过程的,也就是说,重定向是“用户无感知”的。

其中,302状态码返回的报文如下:

这里出现了一个新的头字段“Location: /index.html”,它就是 301/302 重定向跳转的秘密所在。

“Location”字段属于响应字段,必须出现在响应报文里。但只有配合 301/302 状态码才有意义,它标记了服务器要求重定向的 URI,这里就是要求浏览器跳转到“index.html”。

浏览器收到 301/302 报文,会检查响应头里有没有“Location”。如果有,就从字段值里提取出 URI,发出新的 HTTP 请求,相当于自动替我们点击了这个链接

重定向状态码

  • 301 俗称“永久重定向”(Moved Permanently),意思是原 URI 已经“永久”性地不存在了,今后的所有请求都必须改用新的 URI。
  • 302 俗称“临时重定向”(“Moved Temporarily”),意思是原 URI 处于“临时维护”状态,新的 URI 是起“顶包”作用的“临时工”。

以上两个比较常用,其他的303 307 308 的接受程度较低,有的浏览器和服务器可能不支持.

重定向应用场景

理解了重定向的工作原理和状态码的含义,我们就可以在服务器端拥有主动权,控制浏览器的行为,不过要怎么利用重定向才好呢?

使用重定向跳转,核心是要理解“重定向”“永久 / 临时”这两个关键词。

先来看什么时候需要重定向。

  • 最常见的原因就是“资源不可用”,需要用另一个新的 URI 来代替。

    域名变更、服务器变更、网站改版、系统维护。。。

  • “避免重复”,让多个网址都跳转到一个 URI,增加访问入口的同时还不会增加额外的工作量。

    有的网站都会申请多个名称类似的域名,然后把它们再重定向到主站上。

再来看使用什么重定向

使用301(永久)还是302(临时)重定向?

  • 301 :如果域名、服务器、网站架构发生了大幅度的改变,比如启用了新域名、服务器切换到了新机房、网站目录层次重构,这些都算是“永久性”的改变。

  • 302 :原来的 URI 在将来的某个时间点还会恢复正常,常见的应用场景就是系统维护,把网站重定向到一个通知页面,告诉用户过一会儿再来访问。

    另一种用法就是“服务降级”,比如在双十一促销的时候,把订单查询、领积分等不重要的功能入口暂时关闭,保证核心服务能够正常运行。

重定向的问题

  1. “性能损耗”。很明显,重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。
  2. “循环跳转”。如果重定向的策略设置欠考虑,可能会出现“A=>B=>C=>A”的无限循环,不停地在这个链路里转圈圈,后果可想而知。所以 HTTP 协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。

HTTP的Cookie机制

HTTP 是“无状态”的,这既是优点也是缺点。

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

由于“无状态”的,服务器会在浏览器的请求处理完立刻就忘得一干二净。即使这个请求会让服务器发生 500 的严重错误,下次来也会依旧“热情招待”。

好在 HTTP 协议是可扩展的,后来发明的 Cookie 技术,给 HTTP 增加了“记忆能力”

Cookie工作过程

Cookie工作要用到两个字段:

  1. 响应头字段 Set-Cookie
  2. 请求头字段 Cookie

下图是一个Cookie工作过程。

  1. 当浏览器第一次访问服务器的时候,服务器肯定是不知道他的身份的。
  2. 所以,服务器就要创建一个独特的身份标识数据,格式是“key=value”,然后放进 Set-Cookie 字段里,随着响应报文一同发给浏览器。
  3. 浏览器收到响应报文,看到里面有 Set-Cookie,知道这是服务器给的身份标识,于是就保存起来。
  4. 浏览器下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。因为第二次请求里面有了 Cookie 字段,服务器就知道这个用户不是新人,之前来过,就可以拿出 Cookie 里的值,识别出用户的身份,然后提供个性化的服务。
  5. 不过因为服务器的“记忆能力”实在是太差。所以,服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key=value”。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行。

从上图中我们也能够看到,Cookie 是由浏览器负责存储的,而不是操作系统。所以,它是“浏览器绑定”的,只能在本浏览器内生效。

Cookie 就是服务器委托浏览器存储在客户端里的一些数据,而这些数据通常都会记录用户的关键识别信息。所以,就需要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。

  • 首先,我们应该设置 Cookie 的生存周期,也就是它的有效期,让它只能在一段时间内可用,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。

    通过“Expires(过期时间,绝对时间)”和”Max-Age(相对时间,单位秒)“来设置。

  • 其次,我们需要设置 Cookie 的作用域,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。

    通过“Domain”和“Path”设置,指定了 Cookie 所属的域名和路径

  • 最后要考虑的就是 Cookie 的安全性了,尽量不要让服务器以外的人看到。

    在前端中,在 JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致“跨站脚本”(XSS)攻击窃取数据。

    属性“HttpOnly”会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。

    另一个属性“SameSite”可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。

    还有一个属性叫“Secure”,表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

Chrome 开发者工具是查看 Cookie 的有力工具,在“Network-Cookies”里可以看到单个页面 Cookie 的各种属性,另一个“Application”面板里则能够方便地看到全站的所有 Cookie。

Cookie的使用

Cookie 最基本的一个用途就是身份识别,保存用户的登录信息,实现会话事务。

比如,你用账号和密码登录某电商,登录成功后网站服务器就会发给浏览器一个 Cookie,内容大概是“name=yourid”,这样就成功地把身份标签贴在了你身上。之后你在网站里随便访问哪件商品的页面,浏览器都会自动把身份 Cookie 发给服务器,所以服务器总会知道你的身份,一方面免去了重复登录的麻烦,另一方面也能够自动记录你的浏览记录和购物下单(在后台数据库或者也用 Cookie),实现了“状态保持”。

Cookie 的另一个常见用途是广告跟踪。

你上网的时候肯定看过很多的广告图片,这些图片背后都是广告商网站(例如 Google),它会“偷偷地”给你贴上 Cookie 小纸条,在浏览器里存储广告商的cookie。这样你上其他的网站,别的广告就能用 Cookie 读出你的身份,然后做行为分析,再推给你广告。

这种 Cookie 不是由访问的主站存储的,所以又叫“第三方 Cookie”(third-party cookie)。如果广告商势力很大,广告到处都是,那么就比较“恐怖”了,无论你走到哪里它都会通过 Cookie 认出你来,实现广告“精准打击”。

为了防止滥用 Cookie 搜集用户隐私,互联网组织相继提出了 DNT(Do Not Track)和 P3P(Platform for Privacy Preferences Project),但实际作用不大。

HTTP缓存控制

缓存(Cache)是计算机领域里的一个重要概念,可以避免多次请求 - 应答的通信成本,节约网络带宽,也可以加快响应速度。是优化系统性能的利器。

基于“请求 - 应答”模式的特点,可以大致分为客户端缓存服务器端缓存

服务器缓存控制

服务器缓存控制整个流程翻译成 HTTP 就是:

  1. 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
  2. 服务器响应请求,返回资源,同时标记资源的有效期(Cache-Control字段头);
  3. 浏览器缓存资源,等待下次重用。

有效期Cache-Control字段,里面的值可能是

  • ““max-age=xxx””,xxx就代表了xxx秒后此页面过期(类似TTL,time-to-live,计时起始时间是响应报文创建时刻(即Date字段)而非客户端收到时刻)。(因为网络数据动态化,这样尽量保证和真实数据相同)
  • no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;
  • no-cache:可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
  • must-revalidate:缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。

下图为服务器缓存控制策略图,其中写清楚了各个“Cache-Control”的区别

客户端缓存控制

除了服务器。客户端也可以发Cache-Control字段头,进行缓存控制。

以访问我的博客pfzone.top为例,当你访问完网页,然后在600秒内再次访问时,就会使用缓存,“from disk cache”,意思是没有发送网络请求,而是读取的磁盘上的缓存。

如下图。

但是当你用F5刷新时,浏览器会在请求头里加一个“Cache-Control: max-age=0”。因为 max-age 是“生存时间”,max-age=0 的意思就是“我要一个最最新鲜的数据”,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。(如果用Ctrl+F5 “强制刷新”它其实是发了一个“Cache-Control: no-cache”,含义和“max-age=0”基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。)

服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。

下图是使用F5刷新的截图。

条件请求

浏览器用“Cache-Control”做缓存控制只能是刷新数据,不能很好地利用缓存数据。又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。

这样就造成了浏览器使用缓存的不便 : 要么不使用,要么使用前验证是否过期。、

因此,浏览器使用缓存的时候,需要两次连续请求来组成“验证”。先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET 请求,获取最新的版本。

但这样的两个请求网络成本太高了。HTTP 协议就定义了一系列“If”开头的“条件请求”字段,专门用来检查验证资源是否过期。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。

条件请求一共有 5 个头字段,我们最常用的是“if-Modified-Since”“If-None-Match”这两个。

需要第一次的响应报文预先提供“Last-modified”“ETag”

Last-modified:文件的最后修改时间

ETag:“实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。使用 ETag 就可以精确地识别资源的变动情况,让浏览器能够更有效地利用缓存。ETag 还有“强”“弱”之分。

然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。

如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

过程如下图 :

总结 : 验证资源是否失效需要使用“条件请求”,常用的是“if-Modified-Since”和“If-None-Match”,收到 304 就可以复用缓存里的资源;验证资源是否被修改的条件有两个:“Last-modified”和“ETag”,需要服务器预先在响应报文里设置,搭配条件请求使用;

还是以访问pfzone.top为例,加载图片时走的就是304缓存。

HTTP代理服务

之前讲HTTP 的时候,遵循了“请求 - 应答”模型,协议中只有两个互相通信的角色,分别是“请求方”浏览器(客户端)和“应答方”服务器。

事实上,还可能会有代理

链条的起点还是客户端(也就是浏览器),中间的角色被称为代理服务器(proxy server),链条的终点被称为源服务器(origin server),意思是数据的“源头”“起源”。

代理服务

所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份

面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;

而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。

代理的作用

为什么要有代理呢?

你也许听过这样一句至理名言:“计算机科学领域里的任何问题,都可以通过引入一个中间层来解决”(在这句话后面还可以再加上一句“如果一个中间层解决不了问题,那就再加一个中间层”)。TCP/IP 协议栈是这样,而代理也是这样。

由于代理处在 HTTP 通信过程的中间位置,相应地就对上屏蔽了真实客户端,对下屏蔽了真实服务器,简单的说就是“欺上瞒下”。在这个中间层的“小天地”里就可以做很多的事情,为 HTTP 协议增加更多的灵活性,实现客户端和服务器的“双赢”。可以做的事情有

  • 负载均衡。这是代理的最基本的功能。因为在面向客户端时屏蔽了源服务器,客户端看到的只是代理服务器,源服务器究竟有多少台、是哪些 IP 地址都不知道。于是代理服务器就可以掌握请求分发的“大权”,决定由后面的哪台服务器来响应请求。(常用的负载均衡算法有轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。)
  • 健康检查:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
  • 安全防护:保护被代理的后端服务器,限制 IP 地址或流量,抵御网络攻击和过载;
  • 加密卸载:对外网使用 SSL/TLS 加密通信认证,而在安全的内网不加密,消除加解密成本;
  • 数据过滤:拦截上下行的数据,任意指定策略修改请求或者响应;
  • 内容缓存:暂存、复用服务器响应,这个以后再说。

代理服务器需要使用字段“Via”标记自己的身份,多个代理会形成一个列表;

如果想要知道客户端的真实 IP 地址,可以使用字段“X-Forwarded-For”和“X-Real-IP”;

HTTP缓存代理

HTTP 的缓存控制和 HTTP 的代理服务。那么,把这两者结合起来就是这节课所要说的“缓存代理”,也就是支持缓存控制的代理服务。

之前谈到缓存时,主要讲了客户端(浏览器)上的缓存控制,它能够减少响应时间、节约带宽,提升客户端的用户体验。

但 HTTP 传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,“就近”获得响应结果。

特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让 RPS(request per second)降低好几个数量级,减轻应用服务器的并发压力,对性能的改善是非常显著的。

缓存代理服务

下图是缓存代理的示意图。

在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能。

加入了缓存后就不一样了。

代理服务收到源服务器发来的响应数据后需要做两件事。

  1. 当然是把报文转发给客户端。
  2. 把报文存入自己的 Cache 里。

下一次再有相同的请求,代理服务器就可以直接发送 304 或者缓存数据,不必再从源服务器那里获取。这样就降低了客户端的等待时间,同时节约了源服务器的网络带宽。

在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它“既是客户端,又是服务器”,同时也“既不是客户端,又不是服务器”。

  • 即是客户端又是服务器”,是因为它面向源服务器时是客户端,在面向客户端时又是服务器,所以它即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略,也就是说它可以同时使用HTTP缓存控制的各种“Cache-Control”属性。
  • “即不是客户端又不是服务器”,因为它只是一个数据的“中转站”,并不是真正的数据消费者和生产者,所以还需要有一些新的“Cache-Control”属性来对它做特别的约束。

源服务器的缓存控制

服务器端的“Cache-Control”属性:max-age、no-store、no-cache 和 must-revalidate,

这 4 种缓存属性可以约束客户端,也可以约束代理。

但客户端和代理是不一样的,客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对代理的缓存再多一些限制条件。

  • 我们要区分客户端上的缓存和代理上的缓存,可以使用两个新属性“private”和“public”。

    “private”表示缓存只能在客户端保存,是用户“私有”的,不能放在代理上与别人共享。(比如登录论坛,返回的响应报文里用“Set-Cookie”添加了论坛 ID)

    而“public”的意思就是缓存完全开放,谁都可以存,谁都可以用。

  • 缓存失效后的重新验证也要区分开。(即使用条件请求“Last-modified”和“ETag”)

    “must-revalidate”是只要过期就必须回源服务器验证,而新的“proxy-revalidate”只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。

    再次,缓存的生存时间可以使用新的“s-maxage”(s 是 share 的意思,注意 maxage 中间没有“-”),只限定在代理上能够存多久,而客户端仍然使用“max-age”。

    还有一个代理专用的属性“no-transform”。代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,而“no-transform”就会禁止这样做,不许“偷偷摸摸搞小动作”。

下面的流程图是完整的服务器端缓存控制策略,可以同时控制客户端和代理。

客户端的缓存控制

客户端在 HTTP 缓存体系里要面对的是代理和源服务器,也必须区别对待,直接看图。

max-age、no-store、no-cache 这三个属性在”HTTP缓存控制”讲已经介绍过了,它们也是同样作用于代理和源服务器。

关于缓存的生存时间,多了两个新属性“max-stale”和“min-fresh”

  • “max-stale”的意思是如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要。
  • “min-fresh”的意思是缓存必须有效,而且必须在 x 秒后依然有效。
  • 有的时候客户端还会发出一个特别的“only-if-cached”属性,表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)。

小结

  • 计算机领域里最常用的性能优化手段是“时空转换”,也就是“时间换空间”或者“空间换时间”,HTTP 缓存属于后者;
  • 缓存代理是增加了缓存功能的代理服务,缓存源服务器的数据,分发给下游的客户端;
  • “Cache-Control”字段也可以控制缓存代理,常用的有“private”“s-maxage”“no-transform”等,同样必须配合“Last-modified”“ETag”等字段才能使用;
  • 缓存代理有时候也会带来负面影响,缓存不良数据,需要及时刷新或删除。