gin-net-http 3

 net/http 中同时包好了 HTTP 客户端和服务端的实现,为了支持更好的扩展性,它引入了 net/http.RoundTripper 和 net/http.Handler 两个接口。net/http.RoundTripper 是用来表示执行 HTTP 请求的接口,调用方将请求作为参数可以获取请求对应的响应,而 net/http.Handler 主要用于 HTTP 服务器响应客户端的请求:

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

HTTP 请求的接收方可以实现 net/http.Handler 接口,其中实现了处理 HTTP 请求的逻辑,处理的过程中会调用 net/http.ResponseWriter 接口的方法构造 HTTP 响应,它提供的三个接口 HeaderWrite 和 WriteHeader 分别会获取 HTTP 响应、将数据写入负载以及写入响应头

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

标准库中的 net/http.RoundTripper 包含如下所示的层级结构:

每个 net/http.RoundTripper 接口的实现都包含了一种向远程发出请求的过程;标准库中也提供了 net/http.Handler 的多种实现为客户端的 HTTP 请求提供不同的服务(看gin server hander)。

客户端

   客户端可以直接通过 net/http.Get 使用默认的客户端 net/http.DefaultClient 发起 HTTP 请求,也可以自己构建新的 net/http.Client 实现自定义的 HTTP 事务,在多数情况下使用默认的客户端都能满足我们的需求,不过需要注意的是使用默认客户端发出的请求没有超时时间,所以在某些场景下会一直等待下去。除了自定义 HTTP 事务之外,我们还可以实现自定义的 net/http.CookieJar 接口管理和使用 HTTP 请求中的 Cookie:

事务和 Cookie 是我们在 HTTP 客户端包为我们提供的两个最重要模块,本节将从 HTTP GET 请求开始,按照构建请求、数据传输、获取连接以及等待响应几个模块分析客户端的实现原理。当我们调用 net/http.Client.Get 发出 HTTP 时,会按照如下的步骤执行:

  1. 调用 net/http.NewRequest 根据方法名、URL 和请求体构建请求;
  2. 调用 net/http.Transport.RoundTrip 开启 HTTP 事务、获取连接并发送请求;
  3. 在 HTTP 持久连接的 net/http.persistConn.readLoop 方法中等待响应;

HTTP 的客户端中包含几个比较重要的结构体,它们分别是 net/http.Clientnet/http.Transport 和 net/http.persistConn

客户端 net/http.Client 是级别较高的抽象,它提供了 HTTP 的一些细节,包括 Cookies 和重定向;而 net/http.Transport 会处理 HTTP/HTTPS 协议的底层实现细节,其中会包含连接重用、构建请求以及发送请求等功能。

构建请求 #

net/http.Request 表示 HTTP 服务接收到的请求或者 HTTP 客户端发出的请求,其中包含 HTTP 请求的方法、URL、协议版本、协议头以及请求体等字段,除了这些字段之外,它还会持有一个指向 HTTP 响应的引用:

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
}

net/http.NewRequest 是标准库提供的用于创建请求的方法,这个方法会校验 HTTP 请求的字段并根据输入的参数拼装成新的请求结构体。

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
}

请求拼装的过程比较简单,它会检查并校验输入的方法、URL 以及负载,然而初始化了新的 net/http.Request 结构,处理负载的过程稍微有一些复杂,我们会根据负载的类型不同,使用不同的方法将它们包装成 io.ReadCloser 类型。

开启事务 #

当我们使用标准库构建了 HTTP 请求之后,会开启 HTTP 事务发送 HTTP 请求并等待远程的响应,经过下面一连串的调用,我们最终来到了标准库实现底层 HTTP 协议的结构体 — net/http.Transport

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

net/http.Transport 实现了 net/http.RoundTripper 接口,也是整个请求过程中最重要并且最复杂的结构体,该结构体会在 net/http.Transport.roundTrip 中发送 HTTP 请求并等待响应,我们可以将该函数的执行过程分成两个部分:

我们可以在标准库的 net/http.Transport 中调用 net/http.Transport.RegisterProtocol 为不同的协议注册 net/http.RoundTripper 的实现,在下面的这段代码中就会根据 URL 中的协议选择对应的实现来替代默认的逻辑:

func (t *Transport) roundTrip(req *Request) (*Response, error) {
	ctx := req.Context()
	scheme := req.URL.Scheme

	if altRT := t.alternateRoundTripper(req); altRT != nil {
		if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol {
			return resp, err
		}
	}
	...
}

在默认情况下,我们都会使用 net/http.persistConn 持久连接处理 HTTP 请求,该方法会先获取用于发送请求的连接,随后调用 net/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
		}
	}
}

net/http.Transport.getConn 是获取连接的方法,该方法会通过两种方法获取用于发送请求的连接:

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: //通过chan来等待连接建立, 当请求建立之后就会关闭w.ready
		...
		return w.pc, w.err
	...
	}
}

func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    // 如果禁用keepalive就不会有空闲连接,因为使用过后就会断开
	if t.DisableKeepAlives {
		return false
	}

	// 取空闲连接的队列并判断是否有空闲连接
	if list, ok := t.idleConn[w.key]; ok {
		// 检查连接是否可用
	}
	return false
}
  1. 调用 net/http.Transport.queueForIdleConn 在队列中等待闲置的连接;
  2. 调用 net/http.Transport.queueForDial 在队列中等待建立新的连接;

连接是一种相对比较昂贵的资源,如果在每次发出 HTTP 请求之前都建立新的连接,可能会消耗比较多的时间,带来较大的额外开销,通过连接池对资源进行分配和复用可以有效地提高 HTTP 请求的整体性能,多数的网络库客户端都会采取类似的策略来复用资源。

当我们调用 net/http.Transport.queueForDial 尝试与远程建立连接时,标准库会在内部启动新的 Goroutine 执行 net/http.Transport.dialConnFor 用于建连,从最终调用的 net/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 连接中读取数据或者向 TCP 连接写入数据,从建立连接的过程我们可以发现,如果我们为每一个 HTTP 请求都创建新的连接并启动 Goroutine 处理读写数据,会占用很多的资源。

 

等待请求 #

持久的 TCP 连接会实现 net/http.persistConn.roundTrip 处理写入 HTTP 请求并在 select 语句中等待响应的返回:

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
	writeErrCh := make(chan error, 1)
   // 通过writech将请求发送给WriteLoop, writeloop负责将请求序列化成二进制数据流发送给对端
	pc.writech <- writeRequest{req, writeErrCh, continueCh}

	resc := make(chan responseAndError)
    //通过reqch将请求发送给ReadLoop, ReadLoop基于请求来解析对端发送过来的二进制数据流
	pc.reqch <- requestAndChan{
		req:        req.Request,
		ch:         resc,
	}

	for {
		select {
		case re := <-resc://等待结果,结果通过ReadLoop协程发送
			if re.err != nil {
				return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
			}
			return re.res, nil
		...
		}
	}
}

每个 HTTP 请求都由另一个 Goroutine 中的 net/http.persistConn.writeLoop 循环写入的,这两个 Goroutine 独立执行并通过 Channel 进行通信。net/http.Request.write 会根据 net/http.Request 结构中的字段按照 HTTP 协议组成 TCP 数据段:

func (pc *persistConn) writeLoop() {
	defer close(pc.writeLoopDone)
	for {
		select {
		case wr := <-pc.writech://WriteLoop接受到请求后开始工作
			startBytesWritten := pc.nwrite
            //发送请求
			wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
			...
		case <-pc.closech:
			return
		}
	}
}

当我们调用 net/http.Request.write 向请求中写入数据时,实际上直接写入了 net/http.persistConnWriter 中的 TCP 连接中,TCP 协议栈会负责将 HTTP 请求中的内容发送到目标服务器上:

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
}

持久连接中的另一个读循环 net/http.persistConn.readLoop 会负责从 TCP 连接中读取数据并将数据发送会 HTTP 请求的调用方,真正负责解析 HTTP 协议的还是 net/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 响应结构的大致框架,其中包含状态码、协议版本、请求头等内容,响应体还是在读取循环 net/http.persistConn.readLoop 中根据 HTTP 协议头进行解析的。

 

client解析:

  1. 设置可选的RoundTripper, 主要是设置协议的切换规则,默认https会尝试使用http2
  2. 看当前协议是否有可选的RoundTripper, 如果是https, 会使用http2Transport
  3. 一个for循环用来处理重试逻辑
  4. 因为roundTrip会修改treq, 所以每次重试都重新创建
  5. 获取缓存的持久化的tcp连接对象,如果没有就创建
  6. 通过持久化的tcp连接对象发送请求,将请求转成二进制数据发送,并读取对方的响应,最后将响应转成Response对象返回
  7. 如果没有问题就跳出循环
  8. http2的重试机制检查
  9. 非http2的重试检查
  10. 重置请求体并重试
// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {
	//设置可选的RoundTripper, 主要是设置协议的切换规则,默认https会尝试使用http2
	t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
	ctx := req.Context()
	trace := httptrace.ContextClientTrace(ctx)
    --------------------------
	scheme := req.URL.Scheme
	isHTTP := scheme == "http" || scheme == "https"
---------------------------
	origReq := req
	cancelKey := cancelKey{origReq}
	req = setupRewindBody(req)

	if altRT := t.alternateRoundTripper(req); altRT != nil {
		//看当前协议是否有可选的RoundTripper, 如果是https, 会使用http2Transport
		if resp, err := altRT.RoundTrip(req); err != ErrSkipAltProtocol {
			return resp, err
		}
		var err error
		req, err = rewindBody(req)
		if err != nil {
			return nil, err
		}
	}
---------------
	for { //一个for循环用来处理重试逻辑
		select {
		case <-ctx.Done():
			req.closeBody()
			return nil, ctx.Err()
		default:
		}

		// treq gets modified by roundTrip, so we need to recreate for each retry.
		//因为roundTrip会修改treq, 所以每次重试都重新创建
		treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			req.closeBody()
			return nil, err
		}

		// Get the cached or newly-created connection to either the
		// host (for http or https), the http proxy, or the http proxy
		// pre-CONNECTed to https server. In any case, we'll be ready
		// to send it requests.
		//获取缓存的持久化的tcp连接对象,如果没有就创建
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			t.setReqCanceler(cancelKey, nil)
			req.closeBody()
			return nil, err
		}

		var resp *Response
		//通过持久化的tcp连接对象发送请求,将请求转成二进制数据发送,并读取对方的响应,最后将响应转成Response对象返回
		if pconn.alt != nil {
			// HTTP/2 path.
			t.setReqCanceler(cancelKey, nil) // not cancelable with CancelRequest
			resp, err = pconn.alt.RoundTrip(req)
		} else {
			resp, err = pconn.roundTrip(treq)
		}
		//如果没有问题就跳出循环
		if err == nil {
			resp.Request = origReq
			return resp, nil
		}

		// Failed. Clean up and determine whether to retry.
		//http2的重试机制检查
		if http2isNoCachedConnError(err) {
			if t.removeIdleConn(pconn) {
				t.decConnsPerHost(pconn.cacheKey)
			}
		} else if !pconn.shouldRetryRequest(req, err) { //非http2的重试检查
			// Issue 16465: return underlying net.Conn.Read error from peek,
			// as we've historically done.
			if e, ok := err.(nothingWrittenError); ok {
				err = e.error
			}
			if e, ok := err.(transportReadFromServerError); ok {
				err = e.err
			}
			if b, ok := req.Body.(*readTrackingBody); ok && !b.didClose {
				// Issue 49621: Close the request body if pconn.roundTrip
				// didn't do so already. This can happen if the pconn
				// write loop exits without reading the write request.
				req.closeBody()
			}
			return nil, err
		}
		testHookRoundTripRetried()

		// Rewind the body if we're able to.
		//重置请求体并重试
		req, err = rewindBody(req)
		if err != nil {
			return nil, err
		}
	}
}

 

posted @ 2024-01-21 15:24  codestacklinuxer  阅读(45)  评论(0编辑  收藏  举报