[Go 入门] 第十八章 创建 HTTP 客户端

Go 入门系列参考于互联网资料与 人民邮电出版社 《Go 语言入门经典》 与 《Effective Go》,编写目的在于学习交流,如有侵权,请联系删除

超文本传输协议 (Hypertext Transfer Protocol, HTTP)是一种在互联网上收发资源的网络协议, 用于传输图像、HTML文档和Json等 , 本文内容:

理解 HTTP

要理解 HTTP 请求的结构,可以使用工具 curl。

script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ curl -s -o /dev/null -v http://blog.forgiveher.cn
* Trying 119.84.129.199:80...
* TCP_NODELAY set
* Connected to blog.forgiveher.cn (119.84.129.199) port 80 (#0)
> GET / HTTP/1.1
> Host: blog.forgiveher.cn
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Server: Tengine
< Date: Sun, 02 Feb 2020 17:33:39 GMT
< Content-Type: text/html
< Content-Length: 278
< Connection: keep-alive
< Location: https://blog.forgiveher.cn/
< Via: kunlun8.cn1492[,0]
< Timing-Allow-Origin: *
< EagleId: 7754811c15806648198814536e
<
{ [278 bytes data]
* Connection #0 to host blog.forgiveher.cn left intact
  • 以字符 > 打头的行描述了客户端发送的请求
  • 以字符 < 打头的行描述收到的请求
  • 请求的详细信息描述了随请求发送的一些报头,如客户端的一些信息
  • 响应详细描述了一些报头,这些报头指出了响应的内容类型、长度和发送时间

以上是简单的 HTTP 结构,如果需要进行的是更复杂的HTTP交互,就还必须对 HTTP 规范有更深入的认识

发送 GET 请求

Go 语言在 net/http 包中提供了一个快捷方法,可用于发出简单的GET请求。使用这个方法意味着不需要考虑如何配置 HTTP 客户端以及如何设置请求报头。如果只是要从远程获取一些数据,那么默认配置完全够用

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
response, err := http.Get("https://ipconfig.io")
if err != nil {
log.Fatal(err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}

执行将看到

112.64.233.*

发送 POST 请求

标准库中的 net/http 包也提供了用于发送简单的 POST 请求的快捷方法 —— Post,它支持设置内容以及发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)

func main() {
postData := strings.NewReader(`{"some": "json"}`)
response, err := http.Post("https://httpbin.org/post", "application/json", postData)
if err != nil {
log.Fatal(err)
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}

执行后,从 httpbin 返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"args": {},
"data": "{\"some\": \"json\"}",
"files": {},
"form": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "16",
"Content-Type": "application/json",
"Host": "httpbin.org",
"User-Agent": "Go-http-client/2.0",
"X-Amzn-Trace-Id": "Root=1-5e371094-70fca52004157328c068ccbc"
},
"json": {
"some": "json"
},
"origin": "112.64.233.*",
"url": "https://httpbin.org/post"
}

进一步控制 HTTP 请求

要进一步控制 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
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
client := &http.Client{}
request, err := http.NewRequest("GET", "https://ipconfig.io", nil)
if err != nil {
log.Fatal(err)
}
response, err := client.Do(request)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}

对应使用自定 HTTP 客户端解读如下:

  • 不使用 net/http 包的快捷方法 Get,而创建一个 HTTP 客户端
  • 使用方法 NewRequests 向 https://ifconfig.io 发出 GET 请求
  • 使用方法 Do 发送请求并处理响应

使用自定的HTTP客户端意味着可对请求设置报头,基本身份验证和cookies。鉴于使用快捷方法和自定义HTTP客户端,发出请求所需代码的差别很小,建议除非要完成的任务非常简单,否则都使用自定义 HTTP 客户端

调试 HTTP 请求

创建 HTTP 客户端时,了解收发请求和响应的报头和数据对整个流程很有用。为此使用fmt来输出各项数据,但net/http/httputil/ 也提供了能够轻松调试 HTTP 客户端和服务器的方法。这个包中的方法 DumpRequestOutDumpResponse 能够让您查看请求和响应。

您可在调试完成后删除它,或者使用 os.Getenv("DEBUG") 设置环境变量来进行调试

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
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"os"
)

func main() {
debug := os.Getenv("DEBUG")
client := &http.Client{}
request, err := http.NewRequest("GET", "https://ipconfig.io", nil)
if err != nil {
log.Fatal(err)
}
if debug == "1" {
debugRequest, err := httputil.DumpRequestOut(request, true)
if err != nil{
log.Fatal(err)
}
fmt.Printf("%s\n", debugRequest)
}
response, err := client.Do(request)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}
script
1
2
3
4
5
6
7
8
9
10
> set DEBUG=1

> go run dirname.go
GET / HTTP/1.1
Host: ipconfig.io
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip


112.64.233.*

如果需要调试响应,则使用 DumpResponse 方法,如果想要获取 json 数据,则使用 request.Header.Set("Accept", "application/json")

处理超时数据

HTTP 事务会为接收响应等待一些时间。客户端向服务器发送请求后,完全无法知道响应会在多长时间内返回。在底层,有大量影响响应速度的变数:

  • DNS 查找速度
  • 打开到服务器 IP 地址的 TCP 套接字速度
  • 建立 TCP 连接的速度
  • TLS 握手的速度(如果连接是 TLS 的)
  • 向服务器发送数据的速度
  • 重定向的速度
  • Web 服务器返回响应的速度
  • 将数据传输到客户端的速度

使用默认的 HTTP 客户端时,没有对请求设置超时时间,这意味着如果服务器没有响应,就会一直等待或挂起。对于任何请求,都应该设置超时请求,这样如果请求在没有规定的时间内完成则返回错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
client := &http.Client{Timeout: 1 * time.Second}
request, err := http.NewRequest("GET", "https://google.com", nil)
request.Header.Set("Accept", "application/json")
if err != nil {
log.Fatal(err)
}
response, err := client.Do(request)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s", body)
}

还可以通过创建一个传输(transport)并将其传递给客户端,可更细致地控制超时:控制 HTTP 连接的各个阶段。在大多数情况下,使用 Timeout 就足以控制整个 HTTP事务,但在 Go 语言中,还可以通过创建传输来控制 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
tr := &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 0,
Deadline: time.Time{},
LocalAddr: nil,
FallbackDelay: 0,
KeepAlive: 0,
Resolver: nil,
Control: nil,
}).DialContext,
DialTLS: nil,
TLSClientConfig: nil,
TLSHandshakeTimeout: 0,
DisableKeepAlives: false,
DisableCompression: false,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 0,
MaxConnsPerHost: 0,
IdleConnTimeout: 0,
ResponseHeaderTimeout: 0,
ExpectContinueTimeout: 0,
TLSNextProto: nil,
ProxyConnectHeader: nil,
MaxResponseHeaderBytes: 0,
WriteBufferSize: 0,
ReadBufferSize: 0,
ForceAttemptHTTP2: false,
}
client := &http.Client{Transport: tr}

问题列表

  • 能否同时发出多个 HTTP 请求

    可以,使用 Goroutine 即可

  • 能根据返回 HTTP 状态码调整程序采取的措施吗

    可以。可通过 Response.StatusCode 来访问响应的状态码