Go 语言编程 — net/http — HTTP 客户端

目录

net/http

HTTP 是典型的 C/S 架构,客户端构建请求并等待响应,服务端处理请求并返回响应。Golang 的 net/http 标准库就同时封装了 HTTP 客户端和服务端的实现,是生产级别的 HTTP 实现。

官方文档:

  • 英文:https://golang.org/pkg/net/http/
  • 中文:https://cloud.tencent.com/developer/section/1143633

源码:

  • https://github.com/golang/go/tree/master/src/net/http

为了让 net/http 的客户端和服务端具有更好的可扩展性,net/http 实现了两个关键接口 http.RoundTripper(客户端发起请求)和 http.Handler(服务端处理请求):

  • http.RoundTripper:用于发出一个 HTTP 请求,客户端将 request 对象作为参数传入,就会发出该 request 并从服务端获取对应的 response 或 error。故称之为:“往返者”。
type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}
  • http.Handler:用于响应客户端发出的 request,实现了处理 request 的实际业务逻辑,最后还会调用 http.ResponseWriter 接口的方法来构造一个相应的 response 或 error。故称之为:“处理器”。
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
  • http.ResponseWriter:提供了三个方法 Header、Write 和 WriteHeader 分别用于获取 HTTP 响应、并将数据写入响应负载(Payload)和响应头(Header)。
type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

HTTP 客户端

net/http 的 Client 提供了 Get、Head、Post 和 PostForm 函数来发起 HTTP/HTTPS 请求。下面先看几个示例来感受。

  • 发出一个简单的 http.Get 请求:
package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "reflect"
)

func main() {

    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
        log.Println(err)
        return
    }
    defer resp.Body.Close()

    headers := resp.Header
    for k, v := range headers {
        fmt.Printf("k=%v, v=%v\n", k, v)
    }

    fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode)
    fmt.Printf("resp Proto %s\n", resp.Proto)
    fmt.Printf("resp content length %d\n", resp.ContentLength)
    fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding)
    fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed)
    fmt.Println(reflect.TypeOf(resp.Body)) // *http.gzipReader

    buf := bytes.NewBuffer(make([]byte, 0, 512))
    length, _ := buf.ReadFrom(resp.Body)
    fmt.Println(len(buf.Bytes()))
    fmt.Println(length)
    fmt.Println(string(buf.Bytes()))
}
  • 使用 http.NewRequest 来创建 request 对象,再通过 http.Client 执行这个 request 对象:
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
)

func main() {
    v := url.Values{}
    v.Set("username", "xxxx")
    v.Set("password", "xxxx")
    body := ioutil.NopCloser(strings.NewReader(v.Encode()))
    req, err := http.NewRequest("POST", "http://xxx.com/logindo", body)
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
    }
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value")

    client := &http.Client{}
    resp, err := client.Do(req)
    defer resp.Body.Close()

    content, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
    }

    fmt.Println(string(content))
}
  • 忽略 TLS 证书认证的请求:
package main

import (
    "crypto/tls"
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    client := &http.Client{Transport: tr}

    seedUrl := "https://www.douban.com/"
    resp, err := client.Get(seedUrl)
    if err != nil {
        fmt.Errorf("get https://www.douban.com/ error")
        panic(err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Errorf("get https://www.douban.com/ error")
        panic(err)
    }

    fmt.Printf("%s\n", body)
}

实现原理

首先,我们可以设想:作为客户端发出一个请求需要经历几个步骤?

  1. 发出什么?
  2. 怎么发?
  3. 结果是什么?

基于这个原始的思维逻辑,net/http 客户端发出 HTTP 请求时,会执行以下 3 个步骤:

  1. 构建请求:调用 http.NewRequest 或 http.NewRequestWithContext 函数根据传入的 Context(可选)、Method、URL 和 Request Body 等实参构建一个请求。
  2. 开始事务:调用 http.Transport.RoundTrip 开启 HTTP 事务、获取连接并发送请求。
  3. 等待响应:在 HTTP 持久连接(默认是长连接)的 http.persistConn.writeLoop 中等待响应。

相应的,net/http 客户端实现了几个重要的结构体:

  • http.Client:表示 HTTP 客户端,实现了包括 Cookies 和重定向等协议内容。默认使用 http.DefaultTransport,开发者也可以自定义一个 Client,但并不常见。
  • http.Transport:实现了 http.RoundTripper 接口,包含:连接重用、构建请求、发送请求和 HTTP Proxy 等功能。
  • http.persistConn:是 TCP 协议长连接功能的封装,作为客户端与服务端交换 HTTP Message(消息)的句柄。

从下面分别对上述 3 个过程进行暂开。

构建请求

http.NewRequestWithContext 函数根据传入的 Context、Method、URL 和 Request Body 等实参组成一个 http.Request 结构体:

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
	if method == "" {
		method = "GET"
	}
	if !validMethod(method) {
		return nil, fmt.Errorf("net/http: invalid method %q", method)
	}
	u, err := urlpkg.Parse(url)
	if err != nil {
		return nil, err
	}
	rc, ok := body.(io.ReadCloser)
	if !ok && body != nil {
		rc = ioutil.NopCloser(body)
	}
	u.Host = removeEmptyPort(u.Host)
	req := &Request{
		ctx:        ctx,
		Method:     method,
		URL:        u,
		Proto:      "HTTP/1.1",
		ProtoMajor: 1,
		ProtoMinor: 1,
		Header:     make(Header),
		Body:       rc,
		Host:       u.Host,
	}
	if body != nil {
		...
	}
	return req, nil
}

http.Request 表示服务端接收到的请求或者是客户端发出的请求,包含了作为一个 HTTP 协议定义的 Request 所应该具有的全部内容,包括:请求行、请求体(Request Header)、报文主体(Request Body)。其中,请求行又包括了:Request Method、URL、协议版本。它还会额外的持有一个指向 HTTP 响应的引用,这是 net/http 的私有设计,用于索引一个 Response 实例:

type Request struct {
	Method string
	URL *url.URL

	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0

	Header Header
	Body io.ReadCloser

	...
	Response *Response
}

开启事务

构建了一个 http.Request 之后,就要开启一个 HTTP 事务来发送这个请求并等待响应,经过了下面一连串的函数或方法调用:

  1. http.Client.Do
  2. http.Client.do
  3. http.Client.send
  4. http.send
  5. http.Transport.RoundTrip

上文也提到,最后的 http.Transport 结构体实现了 http.RoundTripper 的接口,是整个请求过程中最重要且最复杂的结构体。

http.Transport 结构体会在 http.Transport.roundTrip 方法中发送一个 Request 并等待 Response,我们可以将该方法的执行过程分成两个部分:

  1. 根据 URL 的协议(http/https)查找并执行默认的、或自定义的 http.RoundTripper 函数。这里就体现了 net/http 客户端的可扩展性,开发者可自定义 http.RoundTripper,并调用 http.Transport.RegisterProtocol 方法为不同的协议注册预期的 http.RoundTripper 实现。

  2. 从连接池中获取、或初始化新的持久连接(默认为长连接,术语:Keep-Alive 或 Persistent Connection),并调用连接实例的 http.persistConn.roundTrip 方法发出请求。

默认的,会使用 http.persistConn 方法来处理 HTTP 请求,该方法首先会获取用于发送请求的连接。所谓 “连接” 实际上是一个 TCP 连接,因为 HTTP 协议建立在 TCP 协议之上,所以首先需要完成 TCP 三次握手新建一个连接。随后,http.persistConn 再调用 http.persistConn.roundTrip 方法使用这个连接发送请求:

func (t *Transport) roundTrip(req *Request) (*Response, error) {
	...
	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}

		treq := &transportRequest{Request: req, trace: trace}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			return nil, err
		}

		pconn, err := t.getConn(treq, cm)
		if err != nil {
			return nil, err
		}

		resp, err := pconn.roundTrip(treq)
		if err == nil {
			return resp, nil
		}
	}
}

其中,http.Transport.getConn 是获取连接的方法,具有两种方式:

  1. 调用 http.Transport.queueForIdleConn 在队列中等待闲置的连接。
  2. 调用 http.Transport.queueForDial 在队列中等待建立新的连接。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
	req := treq.Request
	ctx := req.Context()

	w := &wantConn{
		cm:         cm,
		key:        cm.key(),
		ctx:        ctx,
		ready:      make(chan struct{}, 1),
	}

	if delivered := t.queueForIdleConn(w); delivered {
		return w.pc, nil
	}

	t.queueForDial(w)
	select {
	case <-w.ready:
		...
		return w.pc, w.err
	...
	}
}

需要注意的是,连接对于操作系统而言,是一种昂贵的资源。如果对每个请求都新建一个连接,会带来很大的开销。所以,理想的方式是通过连接池机制来实现资源的分配和复用,继而有效地提高网络通信的整体性能。这是大多数的网络库客户端都会采取的设计。

非理想的情况,就调用 http.Transport.queueForDial(拨号连接)方法来新建一个连接。首先会在内部启动新的 Goroutine 并执行 http.Transport.dialConnFor 方法,在 http.Transport.dialConn 方法中可以看到到 TCP 连接和 net 库的身影:

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
	pconn = &persistConn{
		t:             t,
		cacheKey:      cm.key(),
		reqch:         make(chan requestAndChan, 1),
		writech:       make(chan writeRequest, 1),
		closech:       make(chan struct{}),
		writeErrCh:    make(chan error, 1),
		writeLoopDone: make(chan struct{}),
	}

	conn, err := t.dial(ctx, "tcp", cm.addr())
	if err != nil {
		return nil, err
	}
	pconn.conn = conn

	pconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())
	pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())

	go pconn.readLoop()
	go pconn.writeLoop()
	return pconn, nil
}

如上所示,在新建 TCP 连接时,会在后台创建两个绑定的 Goroutine,分别用于从 TCP 连接中读取(Read)数据或者向 TCP 连接写入(Write)数据。可见,连接是一种宝贵的资源。作为网络开发者,因为对连接池抱有强烈的意识。

等待响应

TCP 持久连接的封装 http.persistConn 的 roundTrip 在发送 HTTP 请求之后会在 select 语句中等待响应的返回:

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
	writeErrCh := make(chan error, 1)
	pc.writech <- writeRequest{req, writeErrCh, continueCh}

	resc := make(chan responseAndError)
	pc.reqch <- requestAndChan{
		req:        req.Request,
		ch:         resc,
	}

	for {
		select {
		case re := <-resc:
			if re.err != nil {
				return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
			}
			return re.res, nil
		...
		}
	}
}

每个 HTTP 请求都由 http.persistConn.writeLoop 的 Goroutine 循环写入的,http.Request.write 方法会根据 http.Request 结构体中的成员按照 HTTP 协议组成 TCP 数据段:

func (pc *persistConn) writeLoop() {
	defer close(pc.writeLoopDone)
	for {
		select {
		case wr := <-pc.writech:
			startBytesWritten := pc.nwrite
			wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
			...
		case <-pc.closech:
			return
		}
	}
}

当调用 http.Request.write 方法向 write channel 中的 Request 写入数据时,实际上就写入了 http.persistConnWriter 中的 TCP 连接,TCP 协议传输的是字节流([]byte 类型),所需要需要完成 HTTP 请求报文到 TCP 数据段的一次封装:

type persistConnWriter struct {
	pc *persistConn
}

func (w persistConnWriter) Write(p []byte) (n int, err error) {
	n, err = w.pc.conn.Write(p)
	w.pc.nwrite += int64(n)
	return
}

Read/Write 两个 Goroutine 独立执行并通过 Channel 进行通信。作为持久连接中的另一个 Goroutine 则进行着读循环 http.persistConn.readLoop,负责从 TCP 连接中读取数据。在将数据最终呈现给客户端之前,肯定还需要进行 TCP 字节流到 HTTP 响应报文的一次解封装,完成这个的就是 http.ReadResponse 方法:

func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {
	tp := textproto.NewReader(r)
	resp := &Response{
		Request: req,
	}

	line, _ := tp.ReadLine()
	if i := strings.IndexByte(line, ' '); i == -1 {
		return nil, badStringError("malformed HTTP response", line)
	} else {
		resp.Proto = line[:i]
		resp.Status = strings.TrimLeft(line[i+1:], " ")
	}

	statusCode := resp.Status
	if i := strings.IndexByte(resp.Status, ' '); i != -1 {
		statusCode = resp.Status[:i]
	}
	resp.StatusCode, err = strconv.Atoi(statusCode)

	resp.ProtoMajor, resp.ProtoMinor, _ = ParseHTTPVersion(resp.Proto)

	mimeHeader, _ := tp.ReadMIMEHeader()
	resp.Header = Header(mimeHeader)

	readTransfer(resp, r)
	return resp, nil
}

从上述方法可以看到 HTTP 响应报文的大致内容,表现为 http.Response 结构体,包含了响应行、响应头(Response Header)、报文主体(Response Body)。其中,响应行又包含:State Code、协议版本等内容。

参考文档

https://draveness.me/golang/docs/part4-advanced/ch09-stdlib/golang-net-http

猜你喜欢

转载自blog.csdn.net/Jmilk/article/details/107475006