立即加星标
每月看好文
一、net/http包简介
二、HTTP客户端使用
1、在Go语言中,使用net/http包发送HTTP请求非常简单。我们只需要导入包,然后调用相应的函数即可。以下是一个简单的例子:
package main
import (
"fmt"
"net/http"
)
func main() {
response, err := http.Get("https://example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
defer response.Body.Close()
fmt.Println("Status Code:", response.StatusCode)
}
三、http.Get源码解析
1、在上述示例中,我们使用了http.Get函数发送GET请求。那么,让我们深入源码,看看它是如何实现的。首先,http.Get函数的声明如下:
func Get(url string) (resp *Response, err error)
从函数签名可以看出,http.Get函数接受一个URL作为参数,并返回*http.Response与error。它负责发送HTTP GET请求并返回服务器的响应。
2、接下来,我们进入源码中找到http.Get函数的实现。该函数的实现位于src/net/http/client.go文件中。
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
总结:这里的DefaultClient是http包中的默认HTTP客户端,它是一个全局变量,类型为*Client。DefaultClient提供了全局的HTTP客户端,可以在大多数情况下直接使用。
DefaultClient.Get方法的实现如下:
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
四、NewRequest函数的源码解析
1、NewRequest函数是http包中的另一个重要函数,它用于创建HTTP请求实例。让我们来看看它的实现。
func NewRequest(method, url string, body io.Reader) (*Request, error) {
u, err := Parse(url)
if err != nil {
return nil, err
}
return &Request{
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: body,
Host: u.Host,
}, nil
}
-
NewRequest函数接受请求的方法、URL和请求主体作为参数,并返回一个*http.Request实例。 -
在函数内部,它首先调用Parse函数解析URL,将其转换为一个*url.URL实例。接着,使用传入的参数构建一个*http.Request实例并返回。 -
Request结构体包含了HTTP请求的各种信息,包括请求方法、URL、HTTP版本、请求头、请求主体等。
2、在NewRequest函数中,要设置请求头(header),我们可以在Header字段上操作。Header是Request结构体中的一个字段,它是一个http.Header类型,表示HTTP请求头部的键值对。Header字段的声明如下:
type Request struct {
// ...
Header Header
// ...
}
Header类型是一个map[string][]string,它允许一个键对应多个值,这是为了支持HTTP头部中的多值字段。
在NewRequest函数中,Header字段已经通过make(Header)进行了初始化,因此你可以直接在该字段上添加或修改请求头部的内容。例如,设置一个User-Agent头部:
func NewRequest(method, url string, body io.Reader) (*Request, error) {
u, err := Parse(url)
if err != nil {
return nil, err
}
req := &Request{
Method: method,
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(Header),
Body: body,
Host: u.Host,
}
req.Header.Set("User-Agent", "My-User-Agent")
return req, nil
}
在上面的例子中,我们通过req.Header.Set("User-Agent", "My-User-Agent")将User-Agent请求头部设置为My-User-Agent。
除了使用Set方法设置单个值,还可以使用Add方法添加多个值,或者直接通过Header字段的索引操作进行设置。例如:
// 使用Add方法添加多个值
req.Header.Add("Accept-Language", "en-US")
req.Header.Add("Accept-Language", "zh-CN")
// 直接通过索引设置
req.Header["Authorization"] = []string{"Bearer XXXXX"}
总结:从NewRequest函数源码我们可以看出net/http只支持HTTP/1.1协议,不支持HTTP/2.0。如果遇到HTTP/2.0协议的网站,我们可以使用Go语言社区提供的net/http2包来支持HTTP/2.0。
五、Client.Do方法的源码解析
1、Client.Do方法是http包中的一个核心函数,它负责执行HTTP请求,并返回响应结果。接下来,我们看看它的实现。
func (c *Client) Do(req *Request) (resp *Response, err error) {
if req.URL == nil {
return nil, errors.New("http: nil Request.URL")
}
var (
deadline = c.deadline()
reqs []*Request
resp *Response
copyHeaders = c.makeHeadersCopier(req)
reqBodyClosed = false // have we closed the current req.Body?
// Redirect behavior:
redirectMethod string
includeBody bool
)
uerr := func(err error) error {
// the body may have been closed already by c.send()
if !reqBodyClosed {
req.closeBody()
}
var urlStr string
if resp != nil && resp.Request != nil {
urlStr = stripPassword(resp.Request.URL)
} else {
urlStr = stripPassword(req.URL)
}
return &url.Error{
Op: urlErrorOp(reqs[0].Method),
URL: urlStr,
Err: err,
}
}
for {
// For all but the first request, create the next
// request hop and replace req.
if len(reqs) > 0 {
loc := resp.Header.Get("Location")
if loc == "" {
resp.closeBody()
return nil, uerr(fmt.Errorf("%d response missing Location header", resp.StatusCode))
}
u, err := req.URL.Parse(loc)
if err != nil {
resp.closeBody()
return nil, uerr(fmt.Errorf("failed to parse Location header %q: %v", loc, err))
}
host := ""
if req.Host != "" && req.Host != req.URL.Host {
// If the caller specified a custom Host header and the
// redirect location is relative, preserve the Host header
// through the redirect. See issue #22233.
if u, _ := url.Parse(loc); u != nil && !u.IsAbs() {
host = req.Host
}
}
ireq := reqs[0]
req = &Request{
Method: redirectMethod,
Response: resp,
URL: u,
Header: make(Header),
Host: host,
Cancel: ireq.Cancel,
ctx: ireq.ctx,
}
if includeBody && ireq.GetBody != nil {
req.Body, err = ireq.GetBody()
if err != nil {
resp.closeBody()
return nil, uerr(err)
}
req.ContentLength = ireq.ContentLength
}
// Copy original headers before setting the Referer,
// in case the user set Referer on their first request.
// If they really want to override, they can do it in
// their CheckRedirect func.
copyHeaders(req)
// Add the Referer header from the most recent
// request URL to the new one, if it's not https->http:
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
req.Header.Set("Referer", ref)
}
err = c.checkRedirect(req, reqs)
// Sentinel error to let users select the
// previous response, without closing its
// body. See Issue 10069.
if err == ErrUseLastResponse {
return resp, nil
}
// Close the previous response's body. But
// read at least some of the body so if it's
// small the underlying TCP connection will be
// re-used. No need to check for errors: if it
// fails, the Transport won't reuse it anyway.
const maxBodySlurpSize = 2 << 10
if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
io.CopyN(io.Discard, resp.Body, maxBodySlurpSize)
}
resp.Body.Close()
if err != nil {
// Special case for Go 1 compatibility: return both the response
// and an error if the CheckRedirect function failed.
// See https://golang.org/issue/3795
// The resp.Body has already been closed.
ue := uerr(err)
ue.(*url.Error).URL = loc
return resp, ue
}
}
reqs = append(reqs, req)
var err error
var didTimeout func() bool
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// c.send() always closes req.Body
reqBodyClosed = true
if !deadline.IsZero() && didTimeout() {
err = &httpError{
err: err.Error() + " (Client.Timeout exceeded while awaiting headers)",
timeout: true,
}
}
return nil, uerr(err)
}
var shouldRedirect bool
redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
if !shouldRedirect {
return resp, nil
}
req.closeBody()
}
}
解释:首先,Do方法会检查请求的URL是否为nil。如果URL为nil,它会返回一个错误。接下来,Do方法会执行一系列的处理,包括连接管理、代理设置、重定向、超时控制等,最终发送HTTP请求并获取响应。由于篇幅限制,我们无法一一展示这些细节,但你可以通过阅读源码了解其中的实现细节。
2、在上述示例中,我们通过http.Get函数获取了服务器的响应。为了释放相关资源,我们使用了defer response.Body.Close()来确保响应主体在使用后被关闭。
在http包中,响应主体是一个ReadCloser接口,它包装了底层的网络连接。在响应主体被关闭时,它会释放连接资源,以便在后续的请求中重用。这种优雅的资源管理是net/http包的一个重要特性。
六、结语
通过本文的分析,我们深入了解了Go语言中net/http包的源码,并从爬虫的角度解析了HTTP客户端的工作原理。在实际开发中,net/http包为我们提供了简单易用的HTTP客户端和服务器实现,让我们能够更加高效地处理HTTP通信。
如果你对Go语言开发、爬虫技术等感兴趣,欢迎关注本公众号,我们将继续分享更多有趣的技术文章和实用的开发经验。谢谢大家的支持与关注!
往期推荐
数据解码:挑战不常见爬虫逆向分析,揭开数据迷雾的面纱
革新之路:重新设计Scrapy调度器,让爬虫速度翻倍
猿人学逆向比赛第四题-gRPC题解 | Go版本
DX滑块验证码别乱捅!一不小心就反爬了。
某游戏社区App | So层逆向分析
如果想要获得更多精彩内容可以关注我朋友:
END
作者简介
原文始发于微信公众号(逆向与爬虫的故事):深入探索Go语言net/http包源码:从爬虫的视角解析HTTP客户端
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论