HTTP代理服务器是怎么工作的?

早年间,上网速比较慢,特别是跨运营商的访问更是如此。笔者上大学时用的是教育网,访问教育网外的服务就慢如蜗牛,甚至都连不上。这时,各种各样的代理服务器就应运而生了。

如上图,客户机想连接右边的目标服务器,但直连网速很差,可能是跨运营商了。如果它跟代理服务器,而且代理服务器跟目标服务器的网速较快,那它就可以利用代理服务器来加速。

使用代理时,客户机需要先将请求发给代理服务器,由它帮忙请求目标服务器。按采用的协议不同,代理服务器可以分为好几种,例如 SOCKS ,当然也有 HTTP

本节就趁着讲 HTTP 协议的机会,跟大家唠一唠 HTTP 代理服务器的运行原理。

HTTP代理服务器

想要学习 HTTP 代理服务器的工作原理,最好的方法莫过于:抓包观察它的通信过程。为此,笔者为读者们准备了一台 squid 代理服务器,让大家动手实践一下。

squid 是当前最流行的代理服务器,特别擅长代理 HTTP 请求,支持访问控制和资源缓存,策略配置也非常灵活。事不宜迟,我们先把它跑起来,在命令行下执行以下 docker 命令即可:

1
docker run --name http-proxy-lab --rm -it --privileged --cap-add=NET_ADMIN --cap-add=SYS_ADMIN -v /data -h proxy-server fasionchan/netbox:0.9 bash /script/http-proxy-lab.sh

跟其他网络实验一样,笔者将 squid 代理服务器打包成一个 docker 镜像,拉下来就能跑。不过,由于镜像比较大,可能要耗一些时间,不妨耐心等待。

squid 环境跑起来后,可以看到屏幕被分为三个窗口:最上面的窗口为 squid 输出的日志;左边窗口是 squid 所在代理服务器的命令行;右边窗口是客户端主机的命令行。实验网络拓扑大致如下:

我们将在客户机 client 上执行 curl 命令,以 proxy-server 为代理服务器,来访问 cors.fasionchan.com 。于此同时,我们将在代理服务器 proxy-server 执行 tcpdump 命令进行抓包,以便观察协议的通信过程。

HTTP代理

我们先切到 proxy-server 代理服务器,嗅探所有 TCP 协议相关的流量:

1
$ tcpdump -nXi any tcp

如何在不同窗口间进行切换呢? 我们来复习一下 tmux 窗口切换操作:

  1. 按下 tmux 功能键 Ctrl-B
  2. 再按下 Q ,这时每个窗口会出现一个数字;
  3. 按下窗口上的数字,即可切到对应的窗口;

现在我们切回 client 客户机,执行 curl 命令请求 http://cors.fasionchan.com/about.txt

1
$ curl -x http://10.0.0.1:3128 http://cors.fasionchan.com/about.txt

注意到,我们指定了 -x 选项,告诉 curl10.0.0.1 上的 squid (3128端口)为 HTTP 代理服务器。这样一来,curl 会将请求先发给代理服务器,由 squid 代为请求。

我们一按下回车,即可看到 squid 输出了访问日志:

1
1662188882.266    623 10.0.0.2 TCP_MISS/200 1058 GET http://cors.fasionchan.com/about.txt - HIER_DIRECT/59.37.142.223 text/plain

于此同时,代理服务器上的 tcpdump 命令嗅探到相关的网络流量。经过分析不难得知,curl 先经过三次握手跟 squid 建立 TCP 连接,然后向 squid 发出 HTTP 代理请求:

1
2
3
4
5
6
GET http://cors.fasionchan.com/about.txt HTTP/1.1
Host: cors.fasionchan.com
User-Agent: curl/7.68.0
Accept: */*
Proxy-Connection: Keep-Alive

HTTP 代理请求跟普通 HTTP 请求一样,只不过请求行(第一行)中指定了完整的 URL 。回顾前面学习的 HTTP 协议,正常的请求行只需指定 URL 中的路径部分。

squid 接到这样一个请求后,检查请求行中的 URL ,知道这是一个去往 cors.fasionchan.com 的代理请求。它便建立到目标服务器 80 端口的 TCP 连接,并发起 HTTP 请求:

1
2
3
4
5
6
7
8
9
GET /about.txt HTTP/1.1
User-Agent: curl/7.68.0
Accept: */*
Host: cors.fasionchan.com
Via: 1.1 proxy-server (squid/4.10)
X-Forwarded-For: 10.0.0.2
Cache-Control: max-age=259200
Connection: keep-alive

这是一个正常的 HTTP 请求,代替客户端请求 http://cors.fasionchan.com/about.txt 这个资源。注意到,squid 还为请求添加了若干头部,来传递代理信息和缓存控制信息。

请注意,squid 添加的这些头部都是可选的,不加也可以。

目标 Web 服务器 cors.fasionchan.com 处理 squid 发来的请求后,向 squid 进行响应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
HTTP/1.1 200 OK
Server: Tengine
Content-Type: text/plain
Content-Length: 86
Connection: keep-alive
Date: Sat, 03 Sep 2022 06:22:34 GMT
x-oss-request-id: 6312F2AA897E313830360F9D
x-oss-cdn-auth: success
Accept-Ranges: bytes
ETag: "CBF7C2EF8AA696F37FC9D577EE66CFF3"
Last-Modified: Mon, 03 Jan 2022 04:12:48 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 9909182790107755790
x-oss-storage-class: Standard
Content-MD5: y/fC74qmlvN/ydV37mbP8w==
x-oss-server-time: 43
Ali-Swift-Global-Savetime: 1662186154
Via: cache12.l2cn1851[309,309,304-0,M], cache14.l2cn1851[311,0], kunlun14.cn3157[0,0,200-0,H], kunlun17.cn3157[12,0]
Age: 2728
X-Cache: HIT TCP_MEM_HIT dirn:8:936568846
X-Swift-SaveTime: Sat, 03 Sep 2022 06:22:34 GMT
X-Swift-CacheTime: 3600
Access-Control-Allow-Origin: *
Timing-Allow-Origin: *
EagleId: 3b258ea716621888823201375e

作者:fasionchan
网站:https://fasionchan.com
微信公众号:小菜学编程

这也是一个普通的 HTTP 响应,资源数据位于响应体,只有三行文本。注意到,请求中包含了很多 HTTP 头部,大部分跟缓存相关,本文不作深究。

squid 接到响应后,再将相应内容转发给客户机 client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
HTTP/1.1 200 OK
Server: Tengine
Content-Type: text/plain
Content-Length: 86
Date: Sat, 03 Sep 2022 06:22:34 GMT
x-oss-request-id: 6312F2AA897E313830360F9D
x-oss-cdn-auth: success
Accept-Ranges: bytes
ETag: "CBF7C2EF8AA696F37FC9D577EE66CFF3"
Last-Modified: Mon, 03 Jan 2022 04:12:48 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 9909182790107755790
x-oss-storage-class: Standard
Content-MD5: y/fC74qmlvN/ydV37mbP8w==
x-oss-server-time: 43
Ali-Swift-Global-Savetime: 1662186154
Age: 2728
X-Cache: HIT TCP_MEM_HIT dirn:8:936568846
X-Swift-SaveTime: Sat, 03 Sep 2022 06:22:34 GMT
X-Swift-CacheTime: 3600
Access-Control-Allow-Origin: *
Timing-Allow-Origin: *
EagleId: 3b258ea716621888823201375e
X-Cache: MISS from proxy-server
X-Cache-Lookup: MISS from proxy-server:3128
Via: cache12.l2cn1851[309,309,304-0,M], cache14.l2cn1851[311,0], kunlun14.cn3157[0,0,200-0,H], kunlun17.cn3157[12,0], 1.1 proxy-server (squid/4.10)
Connection: keep-alive

作者:fasionchan
网站:https://fasionchan.com
微信公众号:小菜学编程

squid 转发基本上只是将目标服务器 cors.fasionchan.com 回复的响应抄了一遍,再调整几个跟缓存相关的头部。其实这些头部调不调整都无所谓,全抄也完全可以。

由于 squid 还会对请求结果进行缓存,因此需要调整一些缓存头部加以配合。HTTP 缓存控制细节不在本文讨论范围,这里就不再赘述了。

最后,我们用一张图来总结 HTTP 代理的通信原理:

  1. curl 作为客户端,先建立到 squid 代理服务器的 TCP 连接;
  2. 客户端向 squid 代理服务器发出 HTTP 代理请求;
  3. squid 代理服务器建立到目标服务器的 TCP 连接;
  4. squid 代理服务器代替客户端向目标服务器发起 HTTP 请求;
  5. 目标服务器处理请求后,向 squid 回复响应,squid 这时作为客户端;
  6. squid 将目标服务器的响应转发给客户端,squid 这时作为服务器;

TCP隧道

虽然 HTTP 可以说是互联网中最常用的协议,但并非所有应用都是使用 HTTP 协议通信的。举个例子,我们发送邮件时,必须通过 SMTP 协议。

此外,为提高安全性,HTTPS 采用 TLSTCP 连接中的数据进行加密。代理服务器无法解析 HTTPS 请求,也就无法代替客户端来请求。为了解决这些问题,人们只能另辟蹊径。

HTTP 协议提供了 CONNECT 请求 方法method ),支持通过 HTTP 代理服务器建立到目标服务器的 TCP 隧道。通过 HTTP 代理发起的 HTTPS 请求,便采用这种方式。

我们执行 curl 命令,以 squid 为代理来请求一个 HTTPS 资源,以便观察 CONNECT 建立 TCP 隧道的通信过程:

1
$ curl -x http://10.0.0.1:3128 https://cors.fasionchan.com/about.txt

通过观察 tcpdump 抓到的网络流量,我们发现 curl 先向 squid 发来 HTTP CONNECT 请求,要求代理服务器帮它建立到 cors.fasionchan.com 主机 443 端口的 TCP 连接:

1
2
3
4
5
CONNECT cors.fasionchan.com:443 HTTP/1.1
Host: cors.fasionchan.com:443
User-Agent: curl/7.68.0
Proxy-Connection: Keep-Alive

squid 收到请求后,先按要求建立到 cors.fasionchan.com:443TCP 连接,并输出日志:

1
1662188906.555    118 10.0.0.2 TCP_TUNNEL/200 4086 CONNECT cors.fasionchan.com:443 - HIER_DIRECT/59.37.142.223 -

当连接成功建立后,它向客户端回复 200 响应:

1
2
HTTP/1.1 200 Connection established

之后,squid 便当起了搬运工,在客户端和目标服务器间倒腾数据——将客户端连接发过来的数据转发给服务器,将服务器连接发过来的数据转发给客户端。

squid 在两个 TCP 连接间搬运数据,并不关心数据的内容,因此属于 TCP 层的数据转发。转发关系建立后,两个 TCP 连接就像两个水管连接在一起一样。

客户端收到 200 响应后,知道 squid 已经帮它建好 TCP 隧道。后续该 TCP 连接就像是连到目标服务器一样,通过它发出去的数据会被送到目标服务器,反之亦然。

随后,客户端便开始利用该 TCP 隧道,向目标服务器发起 HTTPS 请求。HTTPS 请求先建立 TLS 加密隧道,之后的 HTTP 请求则是密文,我们无法解读。

TCP 隧道建立好后,可以承担任何应用层协议通信。举个例子,邮件客户端也可以先通过 HTTP 代理服务器与邮件服务器建立 TCP 隧道,再采用 SMTP 协议来发送邮件。

至此,相信大家对 HTTP 代理服务器的工作原理已经了然于胸了。如果大家有兴趣的话,可以自行写个程序来练练手,例如通过一台代理服务器来发 HTTP 请求,乃至实现一个 HTTP 代理服务器。

小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注:

【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: