Golang net/http 客户端快速入门
golang内置了非常多优秀的库,net/http
是其中之一,net/http
不只是一个http客户端,它还实现了服务端,鉴于它非常贴心的设计,基于net/http
做一个web server是一件非常容易的事情,但是本文主要聚焦于http客户端相关的功能。
快速入门
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://baidu.com")
if err != nil {
panic(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
fmt.Println("关闭Body失败:", err)
}
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("%s", data)
}
如果使用Get
和Post
, 可以直接使用http.Get
或http.Post
。而其他方法如Put
, Delete
等方法就需要自己构造请求了。
请求前
如果要自定义请求前的各项参数,就需要自己创建Request
, 通过这个Request
设置各个字段。
查询参数
查询参数值一般指url中问号后的部分, 比如https://www.baidu.com/s?ie=UTF-8&wd=test
中的ie=UTF-8&wd=test
, 在golang中可以通过以下方式实现
func main() {
endpoint := "http://30.171.78.89:60081"
client := &http.Client{}
request, _ := http.NewRequest(http.MethodGet, endpoint, nil)
query := url.Values{}
query.Add("ie", "UTF-8")
query.Add("wd", "test")
request.URL.RawQuery = query.Encode()
resp, err := client.Do(request)
// 后续省略
}
http请求头参数
常见的http请求头有User-Agent
, 用于简单的反爬以及反反爬。
func main() {
endpoint := "http://30.171.78.89:60081"
client := &http.Client{}
request, _ := http.NewRequest(http.MethodGet, endpoint, nil)
request.Header.Add("User-Agent", "youerning.top")
resp, err := client.Do(request)
// 后续省略
}
请求体参数
既然有了查询参数为啥还需要请求体参数? 因为查询参数在url中,总不可能上传个文件也把文件的编码到url中,那么这个url太长了,并且url的长度有有限制的。
一般来说,常用的请求体参数有以下三种。
- 表单 对应的Content-Type是application/x-www-form-urlencoded
- json 对应的Content-Type是application/json
- 包含文件的表单 对应的Content-Type是multipart/form-data
上传表单
可能是考虑到比较常用,所以net/http
包含了封装好的接口
弱弱的问一句, json请求不是更常见么….
func main() {
endpoint := "http://30.171.78.89:60081"
form := url.Values{}
form.Add("username", "youerning.top")
form.Add("password", "123456")
resp, err := http.PostForm(endpoint, form)
}
你可能注意到了, form表单用的也是url.Values
, 这是因为它们编码后的结果其实是一样的!!!都是
JSON请求
func main() {
endpoint := "http://30.171.78.89:60081"
data := struct {
Username string
Password string
}{
Username: "youerning.top",
Password: "123456",
}
payload, err := json.Marshal(data)
if err != nil {
panic(err)
}
resp, err := http.Post(endpoint, "application/json", bytes.NewReader(payload))
}
上传文件的表单
func main() {
var err error
endpoint := "http://30.171.78.89:60081"
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
// 创建文件文件字段的对应的句柄
file1writer, err := bodyWriter.CreateFormFile("file1", "file1")
if err != nil {
fmt.Println("写入字段名,文件名失败")
panic(err)
}
// 打开文件
fh, err := os.Open("file1.txt")
if err != nil {
fmt.Println("打开file1.txt失败:", err)
panic(err)
}
defer fh.Close()
//复制文件内容
_, err = io.Copy(file1writer, fh)
if err != nil {
fmt.Println("读取file1.txt失败:", err)
panic(err)
}
bodyWriter.WriteField("username", "youerning.top")
// 这里比较重要
bodyWriter.Close()
resp, err := http.Post(endpoint, bodyWriter.FormDataContentType(), bodyBuf)
}
在请求前一定要调用bodyWriter.Close()
, 不然内容不会写到bodyBuf里。
编码后的请求体内容如下:
--f424a574a686c72343e093e7bdbf26d4afdfabc5a73f494226c9104be3cb
Content-Disposition: form-data; name="file1"; filename="file1"
Content-Type: application/octet-stream
test1
--f424a574a686c72343e093e7bdbf26d4afdfabc5a73f494226c9104be3cb
Content-Disposition: form-data; name="username"
youerning.top
--f424a574a686c72343e093e7bdbf26d4afdfabc5a73f494226c9104be3cb--
file1.txt的文件内容是test1.
上传表单带有文件的表单还是比较复杂的。
Cookie
net/http
自带了cookiejar, 不过这个只能保存在内存中,如果需要持久化,需要使用第三方的cookieJar, 比如github.com/juju/persistent-cookiejar
func main() {
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
client := http.Client{
Jar: jar,
}
resp, err := client.Get("http://30.171.78.89:60081")
if err != nil {
panic(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
fmt.Println("关闭Body失败:", err)
}
}()
reader := bufio.NewReader(resp.Body)
content, _ := io.ReadAll(reader)
fmt.Printf("%s\n", content)
}
cookiejar可以设置的值有很多,可以自行搜索并应用。
超时
超时可以很简单的设置,比如
func main() {
endpoint := "http://30.171.78.89:60081"
client := &http.Client{Timeout: 10 * time.Second}
request, _ := http.NewRequest(http.MethodGet, endpoint, nil)
resp, err := client.Do(request)
}
这里设置了一个总的超时时间。
但是超时也可以设置的更精细一些,比如下面这样
func main() {
endpoint := "http://30.171.78.89:60081"
client := &http.Client{
Transport: &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
request, _ := http.NewRequest(http.MethodGet, endpoint, nil)
resp, err := client.Do(request)
}
客户端的各复杂的超时设置可以参考这张图片
SSL证书
对于自签名证书最常见的就是不验证证书,代码如下
func main() {
endpoint := "https://30.171.78.89:6443"
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
resp, err := http.Get(endpoint)
printBody(resp, err)
}
不验证证书肯定是不安全的,所以可以加载自签名证书, 代码如下
var (
CACertFilePath = "ca-cert.pem"
CertFilePath = "client-cert.pem"
KeyFilePath = "client-key.pem"
)
func main() {
// 加载证书
clientTLSCert, err := tls.LoadX509KeyPair(CertFilePath, KeyFilePath)
if err != nil {
fmt.Println("加载证书失败:", err)
panic(err)
}
// 注入自签名证书
certPool, err := x509.SystemCertPool()
if err != nil {
panic(err)
}
if caCertPEM, err := ioutil.ReadFile(CACertFilePath); err != nil {
panic(err)
} else if ok := certPool.AppendCertsFromPEM(caCertPEM); !ok {
panic("非法的ca证书")
}
tlsConfig := &tls.Config{
RootCAs: certPool,
Certificates: []tls.Certificate{clientTLSCert},
}
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://30.171.78.89:6443")
}
代理
常见的代理协议就两种, http
, socks5
, 这里以http为例
func main() {
proxyUrl, err := url.Parse("http://127.0.0.1:1080")
if err != nil {
fmt.Println("解析代理地址失败:", err)
panic(err)
}
client := &http.Client{Transport: &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
}}
resp, err := client.Get("http://30.171.78.89:60081")
}
重定向
有时候可以限制重定向的测试来避免重定向次数过多。
func main() {
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) > 5 {
return errors.New("重试次数超过5次")
}
return nil
}}
resp, err := client.Get("http://30.171.78.89:60081")
printBody(resp, err)
}
net/http 默认的重定向检查是10次。
请求后
请求后会获得一个http.Response
, 这个结构体有许多比较有用的字段。
响应头信息/状态码
响应头信息可以用于一些特殊字段的判断,比如字符集,而状态码可以简单的判断请求是否成功,
func main() {
resp, err := http.Get("http://30.171.78.89:60081")
printBody(resp, err)
fmt.Println("status:", resp.StatusCode)
fmt.Printf("%v\n", resp.Header)
}
响应体
如果默认响应体的编码是utf8或者以字节流的方式保存,就可以直接处理响应体。
func main() {
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) > 5 {
return errors.New("重试次数超过5次")
}
return nil
}}
resp, err := client.Get("http://30.171.78.89:60081")
if err != nil {
panic(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
fmt.Println("关闭Body失败:", err)
}
}()
data, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("%s", data)
}
编码
一般来说大家都是用utf-8了,但是总有一些例外情况,所以为了安全的解析响应体内容,需要检测响应体的编码格式。
响应体的内容检测需要一个额外的包: golang.org/x/net/html/
// https-client-mtls.go
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
)
func main() {
resp, err := http.Get("http://30.171.78.89:60081")
if err != nil {
panic(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
fmt.Println("关闭Body失败:", err)
}
}()
reader := bufio.NewReader(resp.Body)
// charset最多只会读取前1024个字节
test, err := reader.Peek(1024)
if err != nil {
fmt.Println("读取前1024字节失败:", err)
panic(err)
}
//resp.Header.Get("content-type")
e, _, _ := charset.DetermineEncoding(test, resp.Header.Get("content-type"))
eReader := transform.NewReader(reader, e.NewDecoder())
content, _ := io.ReadAll(eReader)
fmt.Printf("%s\n", content)
}
golang.org/x/net/html/charset
的检测效果并不是那么好, 因为它比较依赖Content-Type
, 如果需要更通用的检测方式,可以使用github.com/saintfish/chardet
, 代码如下:
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"strings"
"github.com/saintfish/chardet"
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
)
func main() {
resp, err := http.Get("http://30.171.78.89:60081")
if err != nil {
panic(err)
}
defer func() {
err := resp.Body.Close()
if err != nil {
fmt.Println("关闭Body失败:", err)
}
}()
reader := bufio.NewReader(resp.Body)
test, err := reader.Peek(1024)
if err != nil {
fmt.Println("读取前1024字节失败:", err)
panic(err)
}
decoder := chardet.NewTextDetector()
r, err := decoder.DetectBest(test)
if err != nil {
panic(err)
}
// chardet GB-18030 对应 go编码中的 gb18030
// Lookup方法会进行大小写转换
_charset := strings.Replace(r.Charset, "-", "", 1)
encoder, _ := charset.Lookup(_charset)
if encoder == nil {
panic("没有找到对应的转码对象")
}
eReader := transform.NewReader(reader, encoder.NewDecoder())
content, _ := io.ReadAll(eReader)
fmt.Printf("%s\n", content)
}
值得注意的是chardet的Charset使用的是IANA格式,而go不知道是啥格式, 比如前者的GB-18030
格式对应的是后者的gb18030
,所以上面的代码把-
去掉。
总结
net/http
自然是比较强大的,提供了各种接口用于扩展,但是相对于一些场景来说用户体验并不是那么好,所以自己写项目,往往需要再封装一层,其实http里面还有一个非常重要的部分没有讲,那就是Transport
, 这个比较复杂,可以单独写一篇文章。
参考链接:
https://www.oschina.net/translate/the-complete-guide-to-golang-net-http-timeouts?cmp