深入探索Go语言net/http包源码:从爬虫的视角解析HTTP客户端

9 篇文章 2 订阅
4 篇文章 0 订阅

大家好,我是TheWeiJun,欢迎来到我的公众号。HTTP是现代互联网中最重要的通信协议之一,而在Go语言中,net/http包则是处理HTTP请求与响应的核心库。无论是构建Web服务器还是编写爬虫,net/http包都是不可或缺的工具。本文将带你深入探索net/http包的源码,从爬虫的角度解析其内部工作原理,为你揭示Go语言中HTTP客户端的奥秘。

特别声明:本公众号文章只作为学术研究,不作为其他不法用途;如有侵权请联系作者删除。

 

目录

一、net/http包简介

二、HTTP客户端使用

三、http.Get源码解析

四、NewRequest源码解析

五、Client.Do源码解析

图片

 

一、net/http包简介

Go语言中的net/http包是一个强大而灵活的HTTP客户端和服务器实现,广泛应用于各种Web开发和网络通信场景。在开发过程中,使用net/http包,我们可以轻松地发送HTTP请求、处理响应、构建HTTP服务器以及实现各种Web服务功能。无论是初学者还是资深开发者,net/http包都为我们提供了简单易用且高效可靠的HTTP通信解决方案。


二、HTTP客户端使用

1、在Go语言中,使用net/http包发送HTTP请求非常简单。我们只需要导入包,然后调用相应的函数即可。以下是一个简单的例子:

package mainimport (    "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语言开发、爬虫技术等感兴趣,欢迎关注本公众号,我们将继续分享更多有趣的技术文章和实用的开发经验。谢谢大家的支持与关注!

逆向与爬虫的故事

专注于网络爬虫、JS逆向、APP逆向、安全攻防实战经验分享及总结。

往期推荐

数据解码:挑战不常见爬虫逆向分析,揭开数据迷雾的面纱

革新之路:重新设计Scrapy调度器,让爬虫速度翻倍

猿人学逆向比赛第四题-gRPC题解 | Go版本

DX滑块验证码别乱捅!一不小心就反爬了。

某游戏社区App | So层逆向分析

作者简介

我是TheWeiJun,有着执着的追求,信奉终身成长,不定义自己,热爱技术但不拘泥于技术,爱好分享,喜欢读书和乐于结交朋友,欢迎扫我微信与我交朋友💕

分享日常学习中关于爬虫及逆向分析的一些思路,文中若有错误的地方,欢迎大家多多交流指正💕

原文链接:深入探索Go语言net/http包源码:从爬虫的视角解析HTTP客户端

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆向与爬虫的故事

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值