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

这是「进击的Coder」的第 860 篇技术分享

作者:TheWeiJun

来源:逆向与爬虫的故事

阅读本文大概需要 7 分钟。

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

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

b5fac3419f773c586846c6840f648495.gif

立即加星标

e123d68ea50a332c158a3f35d74608d6.png

每月看好文

 目录


一、net/http 包简介

二、HTTP 客户端使用

三、http.Get 源码解析

四、NewRequest 源码解析

五、Client.Do 源码解析

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


一、net/http 包简介

Go 语言中的 net/http 包是一个强大而灵活的 HTTP 客户端和服务器实现,广泛应用于各种 Web 开发和网络通信场景。在开发过程中,使用 net/http 包,我们可以轻松地发送 HTTP 请求、处理响应、构建HTTP服务器以及实现各种 Web 服务功能。无论是初学者还是资深开发者,net/http 包都为我们提供了简单易用且高效可靠的 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 语言开发、爬虫技术等感兴趣,欢迎关注本公众号,我们将继续分享更多有趣的技术文章和实用的开发经验。谢谢大家的支持与关注!

欢迎大家加入【ChatGPT&AI 变现圈】,零门槛掌握 AI 神器!我们带你从小白到高手,解锁智能问答、自动化创作、技术变现的无限可能。与我们共同成长,开启 AI 新征程!立即行动,未来已来!(详情请戳:知识星球:ChatGPT&AI 变现圈,正式上线!)

扫码加入:

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

好文和朋友一起看~

你可能感兴趣的:(golang,http,爬虫,xcode,开发语言)