使用curl发网络请求

2,517 阅读11分钟

前端离不开网络请求,那有哪些方式可以发送请求呢?首先,在代码中使用XHR和fetch、构造Img等都是比较常用的手段,开发者工具也提供了重发请求的功能用于调试;类库方面,axios,request,node-fetch等都能做得很好,特别是axios,在日常工作中经常用到;另外,一些调试工具如fiddler、whistle等都有composer功能,可以用于构造并发送请求,postman则是专门的客户端请求代理软件。除此之外,linux层面其实有一个原生的客户端请求软件 —— curl(client URL)。

curl可以支持SMTP, FTP, POP3, HTTP, HTTPS等多种协议,我们在此只讨论HTTP家族的协议。

为了测试curl发送的请求头和请求体,可以在本地启动一个回声服务,原样发回请求报文文本:

const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('set-cookie', 'mycookie=1234567');
  res.write(`
from port: ${req.socket.remotePort}
request method: ${req.method}
request url: ${req.url}
${JSON.stringify(req.headers)}
  `);
  req.on('data', (chunk) => res.write(String(chunk)));
  req.on('end', () => res.end());
});

server.listen(3001, () => console.log('server running on port 3001'));

简单用法

使用curl发送一个get请求:curl localhost:3001可以得到返回结果:

from port: 63574
request method: GET
request url: /
{"host":"localhost:3001","user-agent":"curl/7.64.1","accept":"*/*"}

可见发送的请求会默认带上host, user-agent, accept这几个请求头,那怎么增加请求头呢?可以用上下面这些选项:

  • -A --user-agent指定user-agent头字段
  • -b --cookie增加cookie字段,如果值以@开头则会找到对应的cookie文件读取cookie(cookie由服务器设置,可以保存在本地,详见下方"输出"相关的内容)
  • -e --referer携带referer头字段
  • -H --header添加/更改/删除头字段

-H遵循下面的使用规则:

# 添加/修改请求头
--header "user-agent:myagent/super1.0"
# 删除请求头
--header "user-agent:" 
# 增加空请求头
--header "x-custom-header;"

重定向

有了上面的基础,让我们构造一个请求:curl -A "Mozilla/5.0 whatever" -i -H "X-my-option:000" www.qq.com(此处使用了-i打印出响应头), 可以在命令行得到如下输出结果:

HTTP/1.1 302 Moved Temporarily
Server: stgw/1.3.12.4_1.13.5
Date: Sat, 14 Nov 2020 12:58:19 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: https://www.qq.com/

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>stgw/1.3.12.4_1.13.5</center>
</body>
</html>

curl默认使用http协议进行请求,访问http://www.qq.com会返回一个302的重定向消息,但是curl默认并不会自动跟踪重定向指向的网络地址https://www.qq.com,此时可以添加-L --location选项开启自动跟踪重定向,但要注意的一点是,网络重定向可能形成环状跳转,比如urlA重定向到urlB,urlB再重定向回urlA,这种跳转会一直持续下去,造成资源的浪费,可以使用--max-redirs选项指定链接最大可跳转次数。

加上重定向,再请求一次curl -A "Mozilla/5.0 whatever" -i -H "X-my-option:000" -L www.qq.com命令行打印如下,可见成功进行了跳转。

HTTP/1.1 302 Moved Temporarily
Server: stgw/1.3.12.4_1.13.5
Date: Sat, 14 Nov 2020 13:01:56 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive
Location: https://www.qq.com/

HTTP/2 200 
date: Sat, 14 Nov 2020 13:01:56 GMT
content-type: text/html; charset=GB2312
server: squid/3.5.24
vary: Accept-Encoding
vary: Accept-Encoding
expires: Sat, 14 Nov 2020 13:02:56 GMT
cache-control: max-age=60
x-cache: HIT from shenzhen.qq.com

<...省略html文本>

发送post请求

上面讲的都是发送get请求,那怎么发post请求呢?curl中可以指定-d --data选项添加post数据,如:curl -d "name=可莉" --data "age=8" http://localhost:3001/, 命令行打印:

from port: 64823
request method: POST
request url: /
{"host":"localhost:3001","user-agent":"curl/7.64.1","accept":"*/*","content-length":"17","content-type":"application/x-www-form-urlencoded"}

  name=可莉&age=8

可见data选项原封不动地发送了我们提供的数据,在数据条目之间添加了&,但一般post请求是需要对字符进行编码处理的,我们可以使用--data-urlencode选项代替--data,比如curl --data-urlencode "name=可莉" --data "age=8" http://localhost:3001/,请求体就会进行编码:

from port: 65459
request method: POST
request url: /
{"host":"localhost:3001","user-agent":"curl/7.64.1","accept":"*/*","content-length":"29","content-type":"application/x-www-form-urlencoded"}

  name=%E5%8F%AF%E8%8E%89&age=8

如果想发送文件,就需要用到另外一个选项-F --form,开启这个选项curl使用内置的一个type为multipart/form-data的form表单模拟提交操作,如:curl -F name=romio -F profile=@myprofile http://localhost:3001/

from port: 49288
request method: POST
request url: /
{"host":"localhost:3001","user-agent":"curl/7.64.1","accept":"*/*","content-length":"356","content-type":"multipart/form-data; boundary=------------------------1db05266264c6354"}

  --------------------------1db05266264c6354
Content-Disposition: form-data; name="name"

romio
--------------------------1db05266264c6354
Content-Disposition: form-data; name="profile"; filename="myprofile"
Content-Type: application/octet-stream

###oooo### <文本内容>
##o####o##
#o##o#o#o#
##o####o##
###oooo###
--------------------------1db05266264c6354--

需要注意的一点是-d-F不能混用,原因是比较清晰的,-d选项发送的content-typeapplication/x-www-form-urlencoded, 与后者不同。

输出

如何把返回结果保存为文件呢?

  • -o --output指定输出文件地址,指定为-输出到标准输出
  • -O解析目标url,自动命名保存,如http://a.c.com/a.html保存为a.html
  • --create-dirs 如果输出文件的路径不存在则创建
  • -c --cookie-jar 存储服务端返回的cookie, 指定为-输出到标准输出
  • -D --dump-header 指定存储响应头的文件

比如使用curl -o file -c baiducookie -b @baiducookie http://www.baidu.com, 则请求对应地址并保存到file文件,cookie保存到baiducookie文件。

指定输出格式

有时并不需要获取对应url对应的文本内容,只需要获取状态码、content-type等信息,这需要指定-w --write-out选项,该选项使用一个格式化字符串作为参数,如curl -w '%{http_code} %{content_type}' www.baidu.com 输出的末尾将打印200 text/html,进一步,我们可以定义一个检测url是否可达的bash函数:

is_url_reachable() {
  local RET_CODE=$(curl -o /dev/null -IsfLw '%{http_code}\n' --max-time 3 "$1" 2>&1)
  if [ -n "$RET_CODE" ] && [ $(echo $RET_CODE | cut -c 1-1) = "2" ]; then
    return 0
  else
    return 1
  fi
}

配置

超时和重试

  • limit-rate 参数是/\d+[KMG]/i的形式,可以限制网络速度为多少K/M/G每秒
  • --connect-timeout 指定超时时间(s)
  • -m --max-time 指定最大持续时间(s),包含此命令执行所有阶段的时间
  • --retry 重试次数
  • --retry-delay 重试时间间隔(s)

代理

  • --proxy-header 为代理添加请求头
  • -p --proxy 指定代理

如给请求添加代理:curl -p 8.8.8.8 www.baidu.com

请求方式

  • -G --get 置换请求方式为get
  • -I --head 置换请求方式为head

当请求中添加-d之后,curl默认发post请求,可以添加-G-H将请求类型置换为get请求,c此时-d中的内容会以?<params>的形式附加到url中,如curl -G --data-url "name=可莉" -d age=8 localhost:3001打印结果是:

from port: 50660
request method: GET
request url: /?name=%E5%8F%AF%E8%8E%89&age=8
{"host":"localhost:3001","user-agent":"curl/7.64.1","accept":"*/*"}

输出控制

这里的选项一般用在bash脚本文件中,可以更好地控制curl的输出。

  • -# --progress-bar显示进度条
  • -f --fail 如果服务端错误则静默失败,并返回error code 22
  • --fail-early 一般curl都是顺序请求给定的url,并返回最后一个请求的错误码,开启这个选项可以在curl失败后立即返回
  • -s --silent 不显示错误信息和进度条, 一般配合-S --show-error使用,使用这个选项会显示错误信息
  • -v --verbose显示通信细节

配置文件

使用-K --config <filename>可以指定一个文件作为curl的配置文件,该配置文件每一行描述一条规则,使用url=<request_url>指定请求的url地址,格式如下:

# curlfile
# 指定请求地址
url=http://www.baidu.com
# 开启重定向
location
# 设置user-agent请求头
user-agent="Mozilla/5.0 whatever"
# cookie文件
cookie=@baiducookie
# 保存cookie的文件
cookie-jar=baiducookie
# 最大请求时间
max-time=10
# 请求速度
limit-rate=80K
# 显示进度条
progress-bar
# 输出
output=/dev/null

可以在同一个文件中描述多条请求,描述下一条请求需要使用-: --next选项,上一条请求中指定的选项将重置,比如在上面的文件后面增加如下内容,即可发送两次请求:

# --------------------------- #
# the next request

next

url="http://localhost:3001/"

从控制台的打印结果看,两个请求是串行发送的,即先发送第一个请求,然后发送第二个,curl默认进行串行请求,添加-Z --parallel之后可以并行请求(7.66.0版本开始支持,查看版本curl --version)。如果curl版本不支持,可以使用类似下面的操作代替:

echo "-A \"Mozilla/5.0 whatever\" -b @baiducookie -L --limit-rate 20k -o /dev/null www.baidu.com \n -o /dev/null --limit-rate 20k -L www.qq.com" | xargs -L 1 -P 2 curl

调试

使用--trace <file>选项可以开启网络调试,它会将请求过程中传输的二进制数据、数据的描述、请求阶段描述等信息保存为文件,也可以指定file为-输出到标准输出。

如使用curl --trace tracefile --location www.qq.com,网络请求信息会被保存到tracefile中,从其中可以清晰地分辨出请求头、请求体、重定向、TLS握手等信息:

== Info:   Trying 61.241.49.173...
== Info: TCP_NODELAY set
== Info: Connected to www.qq.com (127.0.0.1) port 80 (#0)
=> Send header, 74 bytes (0x4a) # 请求头
0000: 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a GET / HTTP/1.1..
0010: 48 6f 73 74 3a 20 77 77 77 2e 71 71 2e 63 6f 6d Host: www.qq.com
0020: 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 63 75 ..User-Agent: cu
0030: 72 6c 2f 37 2e 36 34 2e 31 0d 0a 41 63 63 65 70 rl/7.64.1..Accep
0040: 74 3a 20 2a 2f 2a 0d 0a 0d 0a                   t: */*....
<= Recv header, 32 bytes (0x20) # 响应头
0000: 48 54 54 50 2f 31 2e 31 20 33 30 32 20 4d 6f 76 HTTP/1.1 302 Mov
0010: 65 64 20 54 65 6d 70 6f 72 61 72 69 6c 79 0d 0a ed Temporarily..
<= Recv header, 30 bytes (0x1e)
0000: 53 65 72 76 65 72 3a 20 73 74 67 77 2f 31 2e 33 Server: stgw/1.3
0010: 2e 31 32 2e 34 5f ...
<= Recv header, 2 bytes (0x2) # 响应头结束
0000: 0d 0a                                           ..
== Info: Ignoring the response-body
<= Recv data, 169 bytes (0xa9) # 获取body
0000: 3c 68 74 6d 6c 3e 0d 0a 3c 68 65 61 64 3e 3c 74 <html>..<head><t
0010: 69 74 6c 65 3e 33 30 32 20 46 6f 75 6e 64 3c 2f itle>302 Found</
0020: 74 69 74 6c 65 3e 3c 2f 68 65 61 64 3e 0d 0a 3c title></head>..<
0030: 62 6f 64 79 20 62 67 63 6f 6c 6f 72 3d 22 77 68 body bgcolor="wh
0040: 69 74 65 22 3e 0d 0a 3c 63 65 6e 74 65 72 3e 3c ite">..<center><
0050: 68 31 3e 33 30 32 20 46 6f 75 6e 64 3c 2f 68 31 h1>302 Found</h1
0060: 3e 3c 2f 63 65 6e 74 65 72 3e 0d 0a 3c 68 72 3e ></center>..<hr>
0070: 3c 63 65 6e 74 65 72 3e 73 74 67 77 2f 31 2e 33 <center>stgw/1.3
0080: 2e 31 32 2e 34 5f 31 2e 31 33 2e 35 3c 2f 63 65 .12.4_1.13.5</ce
0090: 6e 74 65 72 3e 0d 0a 3c 2f 62 6f 64 79 3e 0d 0a nter>..</body>..
00a0: 3c 2f 68 74 6d 6c 3e 0d 0a                      </html>..
== Info: Connection #0 to host www.qq.com left intact
== Info: Issue another request to this URL: 'https://www.qq.com/' # 重定向到这个链接
== Info:   Trying 61.241.49.173...
== Info: TCP_NODELAY set # TCP连接
== Info: Connected to www.qq.com (127.0.0.1) port 443 (#1)
== Info: ALPN, offering h2 # 应用层协议协商
== Info: ALPN, offering http/1.1
== Info: successfully set certificate verify locations: # 检查网站证书
== Info:   CAfile: /etc/ssl/cert.pem 
  CApath: none
# 下面是TLS握手的消息
== Info: TLSv1.2 (OUT), TLS handshake, Client hello (1): # 客户端发送hello消息, 包含客户端支持的 TLS 版本、支持的密码套件,以及称为“客户端随机数”的一串随机字节。
=> Send SSL data, 224 bytes (0xe0)
0000: 01 00 00 dc 03 03 e5 d8 07 6e ...
== Info: TLSv1.2 (IN), TLS handshake, Server hello (2): # 服务端回复hello消息,内含服务器选择的密码套件,以及“服务器随机数”,即由服务器生成的另一串随机字节。
<= Recv SSL data, 102 bytes (0x66)
0000: 02 00 00 62 03 03 b7 41 b9 d7 ...
== Info: TLSv1.2 (IN), TLS handshake, Certificate (11): # 服务端发送 SSL 证书
<= Recv SSL data, 5882 bytes (0x16fa)
0000: 0b 00 16 f6 00 16 f3 00 11 d3 30 82 11 cf 30 82 ..........0...0.
0010: 10 b7 a0 03 02 01 02 02 10 07 88 51 f4 87 ...
== Info: TLSv1.2 (IN), TLS handshake, Server key exchange (12): # 服务端发送经加密的算法公钥(Server Random),需要客户端用网站证书中的公钥解密
<= Recv SSL data, 333 bytes (0x14d)
0000: 0c 00 01 49 03 00 17 41 04 28 4a 76 3e ...
== Info: TLSv1.2 (IN), TLS handshake, Server finished (14): # 服务端Hello完成
<= Recv SSL data, 4 bytes (0x4)
0000: 0e 00 00 00                                     ....
== Info: TLSv1.2 (OUT), TLS handshake, Client key exchange (16): # 客户端发送经加密的算法公钥(Client Random),需要服务端使用客户端的公钥解密,之后服务端和客户端分别通过秘钥生成算法生成主秘钥和会话秘钥
=> Send SSL data, 70 bytes (0x46)
0000: 10 00 00 42 41 04 b9 7c 5b 07 08 4c 49 ...
== Info: TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): # 客户端之后使用会话秘钥(client_write_key)加密通信
=> Send SSL data, 1 bytes (0x1)
0000: 01                                              .
== Info: TLSv1.2 (OUT), TLS handshake, Finished (20): # 客户端握手成功
=> Send SSL data, 16 bytes (0x10)
0000: 14 00 00 0c 08 0e f2 19 12 1b 47 ff df 4a 19 1d ..........G..J..
== Info: TLSv1.2 (IN), TLS change cipher, Change cipher spec (1): # 服务端之后使用会话秘钥(server_write_key)加密通信
<= Recv SSL data, 1 bytes (0x1)
0000: 01                                              .
== Info: TLSv1.2 (IN), TLS handshake, Finished (20): # 服务端握手成功
<= Recv SSL data, 16 bytes (0x10)
0000: 14 00 00 0c b1 18 18 41 8a 46 9f e3 07 2c 16 99 .......A.F...,..
# TLS握手成功,开始进行HTTP通信
== Info: SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
== Info: ALPN, server accepted to use h2
== Info: Server certificate:
== Info:  subject: C=CN; ST=Guangdong Province; L=Shenzhen; O=Shenzhen Tencent Computer Systems Company Limited; OU=R&D; CN=www.qq.com
== Info:  start date: Jun 22 00:00:00 2020 GMT
== Info:  expire date: Sep 22 12:00:00 2021 GMT
== Info:  subjectAltName: host "www.qq.com" matched cert's "www.qq.com"
== Info:  issuer: C=US; O=DigiCert Inc; OU=www.digicert.com; CN=Secure Site CA G2
== Info:  SSL certificate verify ok.
== Info: Using HTTP2, server supports multi-use
== Info: Connection state changed (HTTP/2 confirmed)
== Info: Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
== Info: Using Stream ID: 1 (easy handle 0x7f93c800fc00)
=> Send header, 72 bytes (0x48) # 发送请求头
0000: 47 45 54 20 2f 20 48 54 54 50 2f 32 0d 0a 48 6f GET / HTTP/2..Ho
0010: 73 74 3a 20 77 77 77 2e 71 71 2e 63 6f 6d 0d 0a st: www.qq.com..
0020: 55 73 65 72 2d 41 67 65 6e 74 3a 20 63 75 72 6c User-Agent: curl
0030: 2f 37 2e 36 34 2e 31 0d 0a 41 63 63 65 70 74 3a /7.64.1..Accept:
0040: 20 2a 2f 2a 0d 0a 0d 0a                          */*....
== Info: Connection state changed (MAX_CONCURRENT_STREAMS == 128)! # 接收响应头和响应体
<= Recv header, 13 bytes (0xd)
0000: 48 54 54 50 2f 32 20 32 30 30 20 0d 0a          HTTP/2 200 ..
<= Recv header, 37 bytes (0x25)
0000: 64 61 74 65 3a 20 53 75 6e 2c 20 31 35 20 4e 6f date: Sun, 15 No
0010: 76 20 32 30 32 ...

总结

本文分析了curl这个客户端请求软件的基础用法,它的功能还是很强大的,在诸如接口调试、调用机器人API、下载文件(推荐直接使用wget)等场合都可以发挥一些作用,写一个curl配置文件对定时任务也很有帮助,调试选项可以提供类似抓包工具的请求细节,对于分析、诊断网络请求有一定的作用。

更多选项请查看在线文档