自己如何编写程序发起HTTP请求?

我们已经学习了 HTTP 协议的通信细节,那么如何编程发起 HTTP 请求呢?

现成类库

现代高级程序开发语言,基本都内置了现成的 HTTP 请求库,直接调用即可。以 Python 为例,标准库中提供了 urllib.request 模块用于发起 HTTP 请求:

1
2
3
4
5
6
7
8
>>> import urllib.request
>>> with urllib.request.urlopen('http://cors.fasionchan.com/about.txt') as response:
...   print(response.read().decode('utf8'))
...
作者fasionchan
网站https://fasionchan.com
微信公众号小菜学编程

这段代码请求 http://cors.fasionchan.com/about.txt 这个地址,并将服务器响应的文本资源打印到屏幕上。现成类库将大量通信细节隐藏起来,短短几行代码即可实现目的,因此更加简单易用。

尽管如此,如果只会调用现成类库,而不懂底层通信细节,容易沦为 API 调用侠,不利于开发能力发展。因此,本节打算从建立 TCP 连接开始,介绍如何一步步请求 HTTP 服务。

telnet

Linux 下的 telnet 命令,原本是一个古老的远程登录工具。它的通信原理非常简单,先建立跟服务器的 TCP 连接,随后将用户输入的内容通过 TCP 传输到服务器,并将服务器发来的数据打印到屏幕上。因此,我们可以利用它来发起 TCP 通信。

接下来,我们利用 telnet 命令,在命令行下发起 HTTP 请求先练练手。以 http://cors.fasionchan.com/about.txt 这个资源为例,服务器主机为 cors.fasionchan.com ,端口为默认的 80

由于服务器主机以域名形式给出,我们需要先通过 DNS 将其解析成 IP 地址:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
$ dig cors.fasionchan.com

; <<>> DiG 9.10.6 <<>> cors.fasionchan.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17096
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 4, ADDITIONAL: 10

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;cors.fasionchan.com.		IN	A

;; ANSWER SECTION:
cors.fasionchan.com.	600	IN	CNAME	cors.fasionchan.com.w.cdngslb.com.
cors.fasionchan.com.w.cdngslb.com. 60 IN A	59.37.142.223

dig 命令用于 DNS 域名解析,我们在 DNS 那章介绍过,本文不再重复介绍。

dig 命令的输出表明,域名 cors.fasionchan.comCNAMEcors.fasionchan.com.w.cdngslb.com ,而后者的 IP 地址是 59.37.142.223

域名解析结果可能因时间改变,请以实验操作时为准。

接下来,我们执行 telnet 命名,建立到服务器 59.37.142.223 80 端口的 TCP 连接:

1
telnet 59.37.142.223 80

如果看到 Connected 字样提示,说明 TCP 连接建立成功:

1
2
3
Trying 59.37.142.223...
Connected to 59.37.142.223.
Escape character is '^]'.

TCP 连接建立好后,即可发送 HTTP 请求,格式如下:

1
2
3
GET /about.txt HTTP/1.0
Host: cors.fasionchan.com

第一行是请求行,写明请求方法、路径和协议版本;接下来是头部,只有一个 Host 头部写明要请求的主机;最后是一个空行。请逐行输入,每行敲好后按下回车接着输入下一行。

telnet 中按下一次回车,它会在用户输入内容后面加上 \r\n 换行符,并发给服务器。

HTTP 请求发送完毕,服务器立即发来 HTTP 响应:

 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
32
33
34
35
36
37
$ telnet 59.37.142.223 80
Trying 59.37.142.223...
Connected to 59.37.142.223.
Escape character is '^]'.
GET /about.txt HTTP/1.0
Host: cors.fasionchan.com

HTTP/1.1 200 OK
Server: Tengine
Content-Type: text/plain
Content-Length: 86
Connection: close
Date: Sat, 10 Sep 2022 09:49:15 GMT
x-oss-request-id: 631C5D9B5C5A723837AE31DC
x-oss-cdn-auth: success
Accept-Ranges: bytes
ETag: "CBF7C2EF8AA696F37FC9D577EE66CFF3"
Last-Modified: Tue, 06 Sep 2022 05:11:41 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: 1662803355
Via: cache12.l2cn1851[311,311,200-0,H], cache14.l2cn1851[313,0], kunlun14.cn3157[0,0,200-0,H], kunlun12.cn3157[1,0]
Age: 2063
X-Cache: HIT TCP_MEM_HIT dirn:10:551321968
X-Swift-SaveTime: Sat, 10 Sep 2022 09:49:15 GMT
X-Swift-CacheTime: 3600
Access-Control-Allow-Origin: *
Timing-Allow-Origin: *
EagleId: 3b258ea216628054186426120e

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

首先是状态行,200 状态码表示请求成功;接着是响应头部,每个头部一行;其中 Content-Type 告诉我们数据是 text 纯文本,Content-Length 告诉我们数据总共有 86 字节;最后是数据,它位于响应体,即空行之后。

编程实现

掌握 HTTP 通信原理,即可自行编写程序,发起 HTTP 请求,大致步骤如下:

  1. 解析 URL ,得到服务器地址、端口和资源路径;
  2. 服务器地址如为域名,先执行 DNS 解析,得到它的 IP 地址;
  3. 建立到服务器 IP 和端口的 TCP 连接,细节可参考 TCP 套接字编程;
  4. 按照 HTTP 协议组织请求报文,并通过 TCP 连接发给服务器;
  5. TCP 接收服务器发来的响应报文,并从中解析出状态码、头部以及响应体(数据);

虽然发起 HTTP 请求主要步骤是确定的,但不同语言实现细节略有差异。我用不同语言分别实现了一遍,请选择自己熟悉的版本来看。代码不再深入解说,请对照注释来学习:

CONNECT隧道

我们再来看看如何发送 CONNECT 方法请求,通过代理服务器建立 TCP 隧道。笔者准备了一个 docker 镜像,启动后将提供一个 squid 代理服务器一个简单的 TCP 服务 tcp-upper-server ,网络架构如下:

我们准备在宿主机连接代理服务器,请求建立到 tcp-upper-serverTCP 连接,以此为跳板来访问 tcp-upper-server 。现在先执行 docker 命令,将镜像跑起来:

1
docker run --name http-proxy-lab --rm -it --privileged --cap-add=NET_ADMIN --cap-add=SYS_ADMIN -e PEER_BOX_NAME=tcp-upper-server -p 13128:3128 -v /data -h proxy-server fasionchan/netbox:0.10 bash /script/http-proxy-lab.sh

镜像跑起来后,我们可以看到三个窗口:上面是代理服务器 squid 输出的访问日志;左边是 tcp-upper-server 输出的访问日志,右边是代理服务器上的命令行。

由于 proxy-servertcp-upper-server 均跑在 docker 容器内部,跟外面的宿主(即执行 docker 命名的本地电脑)是隔离的。为方便访问,docker 命令指定了 -p 选项,将 proxy-server 容器中的 3128 端口( squid )映射到宿主的 13128 端口。这样一来,连接宿主本地的 13128 端口就相当于连接容器 proxy-server 中的 3128 端口。

现在,我们在宿主机执行 telnet 命令,先建立跟代理服务器的 TCP 连接:

1
2
3
4
$ telnet 127.0.0.1 13128
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

接下来,我们发送 CONNEC 方法请求,要求代理服务器建立到 tcp-upper-serverTCP 连接,IP 地址为 10.0.0.2 ,端口为 9999 。注意到,该请求可以不写 HTTP 头部,输入请求行和一个空行即可:

1
2
CONNECT 10.0.0.2:9999 HTTP/1.0

我们很快收到代理服务器的 200 响应,这表明它已经成功建立到 tcp-upper-server 的连接:

1
2
HTTP/1.1 200 Connection established

与此同时,我们在左边窗口输出的 tcp-upper-server 访问日志中,确认到这一点:

1
10.0.0.1:52776 connected

日志表明,tcp-upper-server 接收到一个新连接,正是来源于代理服务器 10.0.0.1

至此,TCP 隧道建立完毕,代理服务器接下来会充当数据搬运工,在两个 TCP 连接间拷贝数据。因此,我们在 telnet 中发送的数据,将被发往 tcp-upper-server 。我们输入 abc 试试:

1
2
abc
ABC

我们立马收到 tcp-upper-server 响应的大写转换结果,由此我们成功借助代理服务器,实现对 tcp-upper-server 的访问。实际上,CONNECT 方法建立的 TCP 隧道,支持任何使用 TCP 协议的应用。

至此,相信大家应该能够编写程序,自行发起 CONNECT 请求建立 TCP 隧道了。大家可以将其作为一个课后作业,加以练习。笔者提供了几个例子瑾供参考,可结合注释加以理解:

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

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