之前准备写个简单的 api 的调用,百度和问了很多人后发现基本对于 http 的客户端熟悉的人非常少。或者对包的不了解自己造了效率很低的轮子,而且官方一些包里有坑,被坑过,这里简单科普下
简单的get和post
http 包里下列可以直接使用的请求方法
1 | func Head(url string) (resp *Response, err error) |
变量 DefaultClient
是用于包函数 Get、Head 和 Post 的默认 Client。
1 | var DefaultClient = &Client{} |
例如简单的直接调用
1 | package main |
如果 get 下载文件直接字节写文件打开是损坏或者乱的,尝试大小端写二进制流试试
设置header
看源码,发现默认的 http.Get
是调用默认客户端的 Get
方法
1 | func Get(url string) (resp *Response, err error) { |
而 Client
的 Get
方法里是先 new 了一个 http.Request
对象后调用 Client.Do
方法来发请求
1 | func (c *Client) Get(url string) (resp *Response, err error) { |
http.NewRequest
返回一个*Request
,Request
结构体里有 header
属性
1 | type Request struct { |
而 Header
类型实现了以下方法来设置和获取发请求时候的请求头
1 | type Header map[string][]string |
所以自定制header
可以这样写,用http.NewRequest
来 new 一个请求,然后用请求的Header.Set
去设置 header ,然后最后去调用客户端的Do(req)
发起请求
1 | package main |
例如发起一个post请求
1 | jar, _ := cookiejar.New(nil) |
自定义客户端
上面都是使用的包里定义的默认客户端,例如有些网站的证书不是权威证书,我们得关闭客户端的权威证书检查,类似于curl -k
那样。或者设置客户端超时时间
1 | client = &http.Client{ |
cookies
上面都是些不需要登陆的,或者说不是那种接口式的网站,接口的网站一般是先 basicAuth
或者 oAuth2
啥的请求了获取了一个 token,后续调接口带上 token 请求就行了,不需要设置 header 啥的。但是也有网站不提供接口的,所以一般需要 http 客户端的记录 session 模拟人为登陆。
而 session 就是体现在 http 的 header 的cookie: xxx=yyy; aaa=bbb; session_id=93728560xxxx; .....
里(http的 header 的 key 不区分大小写),客户端请求后,服务器端回应的时候会带上 Set-cookie
然后客户端会自行去把键值对写到 cookie 里。有的网站把 token 放在 cookies 里作为认证的身份,例如以前的百度贴吧的自动签到和 pandownload
的登陆下载都是叫用户自己找 cookie 里的那几个字段的值写进去,程序会带着它去请求。cookie
里的很多字段看各个 web server 的控制了,这里不细致讨论。
我们使用浏览器的时候,浏览器会自动响应Set-cookie
,那么 net/http 源码里肯定也有对应的代码段, Do
方法最后调用的send
方法发送请求
1 | func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) { |
如果客户端的 .Jar
不为空就会去 SetCookies
,所以我们使用 cookies 也可以自行在 header 里自动去写,这样做法是浏览器登录后 F12
打开 network
抓包,点击到请求里找
找到后自行req.Header.Set("Cookie", "xxxxxx")
或者req.AddCookie(xxx)
,这样非常繁琐,所以一般我们是新建一个客户端把客户端的Jar不设置为空就行了
1 | jar, _ := cookiejar.New(nil) |
这样后续使用这个客户端的时候就和我们使用浏览器一样会自动处理服务端发的 cookie 操作了,会保持住 session
multipart/form-data
http 上传文件的时候是把文件分段上传的,会生成一个随机字符(boundary)来分割每段
boundary 是各自的http客户端生成的,chrome 好像和其他的不一样,总之上传文件的 type 为
1 | Content-Type: multipart/form-data; boundary=分割文件时候的随机字符 |
type里的 boundary 是随机的,所以我们得用包"mime/multipart"
处理
1 | import ( |
digest auth
先看一段curl
的 digest auth 的过程
1 | curl -svX GET --digest -u admin:'xxxxxxx' http://100.64.16.10:8080/cas/casrs/operator/getAuthUrl -H 'Accept: application/json' |
http digest auth过程是:
- 初次请求后
server
端返回401
请求,并发送curl
一个里header
为WWW-Authenticate
的请求,里面拥有三个字段Digest realm
、qop
、nonce
- curl 回复请求,header 为
Authorization
,内容为:Digest username
为用户名realm
为Digest realm
的值,nonce
、qop
为server端返回uri
为去掉host字段的url部分nc
就是nonceCount
,用于标记,计数,防止重放攻击,所以这次为1cnonce
客户端发给服务器的随机字符串response
的值是由俩个hash加密的,加密的表达式决于qop
字段,这里直接写伪代码吧- 如果
algorithm
未定义或者值为MD5
:1
HA1 = MD5(fmt.Sprintf("%s:%s:%s", username, realm, password))
- 如果
algorithm
值为MD5-sess
(和上面差不多,只不过多了:nonce:cnonce
):1
HA1 = MD5(fmt.Sprintf("%s:%s:%s", MD5(fmt.Sprintf("%s:%s:%s", username, realm, password)), nonce, cnonce)
- 如果
qop
未定义或者值为auth
:1
HA2 = MD5(fmt.Sprintf("%s:%s", method, digestURI))
- 如果
qop
值为auth-int
:1
HA2 = MD5(fmt.Sprintf("%s:%s:%s", method, digestURI, MD5(entityBody)))
- 如果
qop
值为auth-int
,response为:1
response = MD5(fmt.Sprintf("%s:%s:%s:%s:%s:%s", HA1, nonce, nonceCount, cnonce, qop, HA2))
- 如果
qop
未定义1
response = MD5(fmt.Sprintf("%s:%s:%s", HA1, nonce, HA2))
- 如果
所以可以写代码
1 | package cas |
entityBody还没搞清楚是啥,所在qop
是auth-int
的没写
一些坑
header的host字段
之前把 curl 写的一套逻辑尝试写到 go 里,发现一直不对,最后发现了host
字段的锅。抓包的接口host
字段和请求的url
不一样(这种情况虽然不是很热门,但是是存在的,例如我们命令行访问ip,设置header模拟访问域名),后面没办法去掉 host 的 header 设定就可以了
具体移步 https://github.com/golang/go/issues/7682
上传文件的type
官方函数 CreateFormFile
限制了 Content-Type
为 application/octet-stream
而且并不打算改,很多时候后端的时候会重视这个 type。可以看到之前我的浏览器抓包的 type 是application/octet-binary
所以我们可以写个下面的函数处理
1 | func createAudioFormFile(w *multipart.Writer, fieldname, filename string) (io.Writer, error) { |
我们可以这样用
1 | fileWriter, err := bodyWriter.CreateFormFile("fwimage", filepath.Base(filename)) |
json的坑
下面这张图可能看不出啥问题,但是是问题的一部分,调用的接口的数据经过了 cdn
,int类型经常出现.0
的数字导致我写错类型 json.Unmarshal
报错,jq
也会把.0
的去掉取整
参考: