代理是夹在客户端和服务器中间的转发程序:对客户端它扮服务器,对服务器它扮客户端。 它不改变请求的方法和语义,只在首部里留个脚印。
上一节说的是「一台服务器怎么装下多个站」。 这一节开始说「中途插进来的程序」,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 秒实验
- 把 P1 的缓存去掉,重跑,看第二次请求源服务器计数会不会变成 2。
- 给 P1 加一个
Via: 1.0 x的初始值,看最后叠成什么样。 - 想一想:如果
X-Forwarded-For可以伪造,源服务器该怎么判断请求来源?
常见误区
- 「代理会改变请求方法」——正常代理不改方法、不改语义,只加
Via。 - 「透明代理就是用户察觉不到的代理」——书里指的是「不加工报文」。
- 「反向代理不是代理」——是代理,只是站在服务器这一边。
自测三问(点开对答案)
1. 代理的身份为什么说是双重的? —— 对客户端它是服务端(监听端口、收请求),对源服务器它是客户端(发起连接、发请求)。 2. `Via` 记的是什么?多个代理怎么叠? —— 记请求经过的每一跳代理,每过一跳追加一项,按经过顺序从左到右。 3. 正向代理和反向代理最本质的区别? —— 代理站在哪一边:正向代理由客户端配置、替客户端出网;反向代理由服务端部署、替服务端接客。代理只搬字节。如果一个中间程序要认识两种不同的协议,那就是下一节的网关。
- ☐ (本节完,下一节 5.2.2 网关)