📘 第五章 与HTTP协作的Web服务器
📄 一句话概括

代理是夹在客户端和服务器中间的转发程序:对客户端它扮服务器,对服务器它扮客户端。 它不改变请求的方法和语义,只在首部里留个脚印。

📝 本节和前面几节的关系

上一节说的是「一台服务器怎么装下多个站」。 这一节开始说「中途插进来的程序」,HTTP 通信里这类程序有三种:代理、网关、隧道。

一、代理在做一件很简单的事

它接收客户端发来的请求,原样转发给源服务器;再把源服务器的响应原样转回来。 转发的时候不动方法、不动路径语义、不动正文,只往首部里加一样东西:Via, 记下「这个请求经过了谁」。 因为两头都要说话,代理的身份是双重的。对浏览器来说它就是一个服务器(有地址有端口,能连); 对源服务器来说它就是一个客户端(发起连接、发请求、读响应)。

二、书里的两种分法

书里按两个基准给代理分类。第一个基准是是否使用缓存,分成「缓存代理」和「透明代理」; 第二个基准是是否修改报文,分成「普通代理」和「匿名代理」。 这里的「透明」要小心理解:它指的是「不加工报文」,不是指「用户察觉不到」。 用户完全不知道自己在走代理的那种,业界叫强制代理或拦截代理,是另一个概念。 中文里「透明」两个字很容易读成「无感」,这是本书最容易被误读的一处。 缓存代理会把响应存下来,下次同样的请求直接回,不再打源服务器。 Demo 里第二次请求同一个 URL,源服务器一次都没被打扰。

三、正向代理与反向代理

书里没有系统讲这一组,但今天绕不开。区别只有一句话:代理站在谁那一边。 | 对比项 | 正向代理 | 反向代理 | |---|---|---| | 谁配置它 | 客户端(用户自己) | 服务端(站长自己) | | 客户端知道真实服务器吗 | 不知道,也不需要知道 | 不知道,以为代理就是服务器 | | 典型场景 | 翻墙、公司统一出口、爬虫池 | 负载均衡、CDN、WAF、TLS 卸载 | | 缓存谁的内容 | 替很多用户缓存外面的内容 | 替很多用户缓存自己的内容 | | 客户端怎么找它 | 浏览器或系统里配代理地址 | 域名直接解析到它 | 从报文角度看,两者要做的事几乎一样:收请求、转发、回响应、加 Via。 所以同一套软件(Nginx、Squid、HAProxy)既能做正向也能做反向,配置不同而已。

四、Via:一条链路上的脚印

每过一跳代理,就在 Via 后面追加一项。 Demo 里走了两级代理,源服务器看到的是 Via: 1.1 p1, 1.1 p2。 这一行的用处是排障:出问题时能顺着它还原「请求到底经过了谁」。 反过来,如果 Via 里出现了你没部署过的代理,那就要查是不是被劫持了。 Via 之外还有一组用来记客户端真实地址的首部, 老的是 X-Forwarded-For/X-Forwarded-Proto/X-Real-IP,2014 年有了统一写法 Forwarded。 这类首部全部可以伪造。 源服务器如果直接暴露在公网,攻击者自己带一个 X-Forwarded-For: 127.0.0.1 就能冒充内网。 所以只有在「确信请求一定来自自己的代理」时才允许读它。

五、动手 Demo:两级代理与缓存命中

起三个服务端:源服务器、代理 P2、代理 P1,串成一条链。 客户端只连 P1,发一个带绝对 URI 的请求,看 Via 怎么一层层叠起来; 再发一次同样的 URL,看缓存代理怎么把它挡住。

def p2(raw, st):
    first, hdrs, _b = H.parse_headers(raw)
    nv = hdrs.get(u'via', u'')
    keep = [(u'Via', (nv + u', ' if nv else u'') + u'1.1 p2')]
    fwd = req(u'GET', first.split(u' ')[1], upstream, keep,
              hdr_host=hdrs.get(u'host', u''))   # Host 保持原样
    return ask(o_port, fwd)

def p1(raw, st):
    first, hdrs, _b = H.parse_headers(raw)
    target = first.split(u' ')[1]
    if target in st[u'cache']:          # 缓存代理:直接回
        st[u'hit'] = st.get(u'hit', 0) + 1
        return st[u'cache'][target]
    out = ask(p2_port, req(u'GET', target, p2, via1))
    st[u'cache'][target] = out
    return out

实跑输出:

--------------------------------------------------------------------
代理:两级转发,Via 头一层一层往上叠
--------------------------------------------------------------------
  链路:客户端 → 代理 P1(:57323) → 代理 P2(:57322) → 源服务器(:57321)

  客户端发出去的请求(代理场景要用绝对 URI):
  文本 GET http://origin.example/doc.html HTTP/1.1\r\nHost: origin.example\r\n\r\n
  十六 47 45 54 20 68 74 74 70 3a 2f 2f 6f 72 69 67 69 6e 2e 65 78 61 6d 70 6c 65 2f 64 6f 63 2e 68 74 6d 6c 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 6f 72 69 67 69 6e 2e 65 78 61 6d 70 6c 65 0d 0a 0d 0a

  第 1 次                    HTTP/1.1 200 OK | 响应 181 字节 | 往返 31.14 毫秒
                             X-Origin-Hits:1
                             X-Origin-Saw-Via:1.1 p1, 1.1 p2

  源服务器收到的请求(Via 已经被两级代理叠起来了):
  文本 GET http://origin.example/doc.html HTTP/1.1\r\nHost: origin.example\r\nVia: 1.1 p1, 1.1 p2\r\n\r\n
  十六 47 45 54 20 68 74 74 70 3a 2f 2f 6f 72 69 67 69 6e 2e 65 78 61 6d 70 6c 65 2f 64 6f 63 2e 68 74 6d 6c 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 6f 72 69 67 69 6e 2e 65 78 61 6d 70 6c 65 0d 0a 56 69 61 3a 20 31 2e 31 20 70 31 2c 20 31 2e 31 20 70 32 0d 0a 0d 0a

  第 2 次同 URL              HTTP/1.1 200 OK | 响应 181 字节
  源服务器被访问次数         1 次(第 2 次被 P1 的缓存挡住了)
  P1 缓存命中                1 次
  P2 转发次数                1 次

  代理对客户端是服务端,对服务端是客户端,两头都说得上话。
  Via 头记的是「经过哪些代理」,每过一跳就追加一项,出事时能顺着查。

客户端发的是绝对 URI:GET http://origin.example/doc.html HTTP/1.1。 代理场景必须用这种形态,因为代理要知道「连谁」,光有路径不够。 源服务器收到的 Via 是 1.1 p1, 1.1 p2——左边是近的,右边是远的,按经过顺序追加。 这一行是排障时最直接的证据。 第 1 次响应 181 字节,源服务器计数 1。 第 2 次同样的 URL,源服务器计数还是 1,P1 的缓存命中 1 次,P2 的转发次数也没涨。 请求根本没出 P1。 注意 Host 一路都是 origin.example,没被改成上游的 IP 加端口。 这是对的:代理改 Host 会让源服务器的虚拟主机分派失效,那一节刚讲过 Host 是分派依据。

30 秒实验

  1. 把 P1 的缓存去掉,重跑,看第二次请求源服务器计数会不会变成 2。
  2. 给 P1 加一个 Via: 1.0 x 的初始值,看最后叠成什么样。
  3. 想一想:如果 X-Forwarded-For 可以伪造,源服务器该怎么判断请求来源?

常见误区

  1. 「代理会改变请求方法」——正常代理不改方法、不改语义,只加 Via。
  2. 「透明代理就是用户察觉不到的代理」——书里指的是「不加工报文」。
  3. 「反向代理不是代理」——是代理,只是站在服务器这一边。
自测三问(点开对答案) 1. 代理的身份为什么说是双重的? —— 对客户端它是服务端(监听端口、收请求),对源服务器它是客户端(发起连接、发请求)。 2. `Via` 记的是什么?多个代理怎么叠? —— 记请求经过的每一跳代理,每过一跳追加一项,按经过顺序从左到右。 3. 正向代理和反向代理最本质的区别? —— 代理站在哪一边:正向代理由客户端配置、替客户端出网;反向代理由服务端部署、替服务端接客。

代理只搬字节。如果一个中间程序要认识两种不同的协议,那就是下一节的网关。

相关笔记