go http請求流程分析

zhouweixin發表於2020-09-11

前言

golang作為常駐程式, 請求第三方服務或者資源(http, mysql, redis等)完畢後, 需要手動關閉連線, 否則連線會一直存在;

連線池是用來管理連線的, 請求之前從連線池裡獲取連線, 請求完畢後再將連線歸還給連線池;

連線池做了連線的建立, 複用以及回收工作;

本檔案僅介紹http請求的連線池http.Transport;

net/http 的工作流程

http請求示例程式碼

func main() {
	url := "http://localhost:8080/login?name=zhouwei1&password=123456"

	// 1.建立client, 這裡使用的預設值
	client := http.DefaultClient

	// 2.建立請求
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		panic(err)
	}

	// 3.傳送請求
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}

	// 4.關閉
	if resp != nil && resp.Body != nil {
		defer resp.Body.Close()
	}

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	fmt.Printf("請求成功, data: %s\n", data)
}

http請求流程

  1. 建立http.Client物件client
  2. 建立http.Request物件req
  3. 傳送請求client.do(req)
  4. 關閉resp.Body.Close()

即使直接呼叫client.Get()client.Post(), 內部同樣建立了request, 且最終總是通過client.Do()方法呼叫私有的client.do()方法, 執行請求;

go-http

http請求核心類

  1. http.Client
  2. http.Request
  3. http.Transport

http.Client

該類主要功能:

  1. Cookie
  2. Timeout
  3. Redirect
  4. Transport
type Client struct {
	Transport RoundTripper
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}

http.Request

type Request struct {
	Method string
	URL *url.URL
	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0
	Header Header
	Body io.ReadCloser
	GetBody func() (io.ReadCloser, error)
	ContentLength int64
	TransferEncoding []string

    // true: 不重用此tcp連線
	Close bool
	Host string
	Form url.Values
	PostForm url.Values
	MultipartForm *multipart.Form
	Trailer Header
	RemoteAddr string
	RequestURI string
	TLS *tls.ConnectionState
	Cancel <-chan struct{}
	Response *Response
	ctx context.Context
}

http.Transport

  1. Transport用來快取連線, 以供將來重用, 而不是根據需要建立
  2. Transport是併發安全的
  3. Transport僅是用來傳送HTTP或HTTPS的低階功能, 像cookie和redirect等高階功能是http.Client實現的
type Transport struct {
	// 操作空閒連線池(idleConn)的鎖
	idleMu sync.Mutex
	// true: 關閉所有空閒連線; false: 不關閉
	wantIdle bool
	// 空閒連線池(最近使用完的連線)
	idleConn map[connectMethodKey][]*persistConn
    // 等待空閒連線的佇列, 基於chan實現
	idleConnCh map[connectMethodKey]chan *persistConn
    // 雙向佇列
	idleLRU    connLRU

    // 請求鎖
	reqMu       sync.Mutex
    // 請求取消器(如: 超時取消)
	reqCanceler map[*Request]func(error)

    // altProto的鎖
	altMu    sync.Mutex
    // 儲存的map[string]RoundTripper, key為URI的scheme(如http, https)
	altProto atomic.Value

    // 連線數量鎖
	connCountMu          sync.Mutex
    // 每臺主機連線的數量
	connPerHostCount     map[connectMethodKey]int
    // 每臺主機可用的連線
	connPerHostAvailable map[connectMethodKey]chan struct{}

	// Proxy指定一個函式來返回給定Request的代理
	// 代理型別由URL scheme確定。支援http, https等。 預設為http
    // 如果Proxy為空或返回空的url,則不使用任何代理。
	Proxy func(*Request) (*url.URL, error)

	// DialContext指定用於建立未加密的TCP連線的撥號功能。 
    // 如果DialContext為nil(並且下面不建議使用的Dial也為nil),則傳輸使用程式包net進行撥號。
	// DialContext與RoundTrip的呼叫同時執行。 
    // 當較早的連線在以後的DialContext完成之前處於空閒狀態時,
    // 發起撥號的RoundTrip呼叫可能會使用先前撥打的連線結束。
	DialContext func(ctx context.Context, network, addr string) (net.Conn, error)

	// Dial指定用於建立未加密的TCP連線的撥號功能。
    // 撥號與RoundTrip的呼叫同時執行。
    // 當較早的連線在之後的撥號完成之前變為空閒時,發起撥號的RoundTrip呼叫可能會使用先前撥打的連線結束。
	// 不推薦使用:改用DialContext,它使傳輸器在不再需要撥號時立即取消它們。 
    // 如果兩者都設定,則DialContext優先。
	Dial func(network, addr string) (net.Conn, error)

	// DialTLS指定用於為非代理HTTPS請求建立TLS連線的可選撥號功能。
	// 如果DialTLS為nil,則使用Dial和TLSClientConfig。
	// 如果設定了DialTLS,則Dial Hook不用於HTTPS請求,
    // 並且TLSClientConfig和TLSHandshakeTimeout將被忽略。 
    // 假定返回的net.Conn已通過TLS握手。
	DialTLS func(network, addr string) (net.Conn, error)

	// TLSClientConfig指定要與tls.Client一起使用的TLS配置。
	// 如果為nil,則使用預設配置。
    // 如果為非nil,則預設情況下可能不會啟用HTTP / 2支援。
	TLSClientConfig *tls.Config

	// TLSHandshakeTimeout指定等待TLS握手的最大時間。 零表示沒有超時。
	TLSHandshakeTimeout time.Duration

    // true: 將禁用HTTP保持活動狀態,並且僅將與伺服器的連線用於單個HTTP請求。
	// 這與類似命名的TCP保持活動無關。
	DisableKeepAlives bool

    // true: 當請求不包含現有的Accept-Encoding值時,
    // 阻止傳輸使用“ Accept-Encoding:gzip”請求標頭請求壓縮。 
    // 如果傳輸本身請求gzip並獲得gzip壓縮的響應,則會在Response.Body中對其進行透明解碼。 
    // 但是,如果使用者明確請求gzip,則不會自動將其解壓縮。
	DisableCompression bool

	// MaxIdleConns控制所有主機之間的最大空閒(保持活動)連線數。 零表示無限制。
	MaxIdleConns int

	// MaxIdleConnsPerHost控制最大空閒(保持活動)連線以保留每個主機。 
    // 如果為零,則使用DefaultMaxIdleConnsPerHost=2。
	MaxIdleConnsPerHost int

	// MaxConnsPerHost可以選擇限制每個主機的連線總數,包括處於撥號,活動和空閒狀態的連線。 
    // 超出限制時,撥號將阻塞。
	// 零表示無限制。
    // 對於HTTP / 2,當前僅控制一次建立的新連線數,而不是總數。 
    // 實際上,使用HTTP / 2的主機只有大約一個空閒連線。
	MaxConnsPerHost int

    // IdleConnTimeout是空閒(保持活動狀態)連線在關閉自身之前將保持空閒狀態的最長時間。
	// 零表示無限制。
	IdleConnTimeout time.Duration

	//(如果非零)指定在完全寫入請求(包括其body(如果有))之後等待伺服器的響應頭的時間。 
    // 該時間不包括讀取響應正文的時間。
	ResponseHeaderTimeout time.Duration

	//(如果非零)指定如果請求具有“期望:100-連續”標頭,
    // 則在完全寫入請求標頭之後等待伺服器的第一個響應標頭的時間。 
    // 零表示沒有超時,並導致正文立即傳送,而無需等待伺服器批准。
	// 此時間不包括髮送請求標頭的時間。
	ExpectContinueTimeout time.Duration

	// TLSNextProto指定在TLS NPN / ALPN協議協商之後,傳輸方式如何切換到備用協議(例如HTTP / 2)。 
    // 如果傳輸使用非空協議名稱撥打TLS連線,並且TLSNextProto包含該鍵的對映條目(例如“ h2”),
    // 則將以請求的許可權(例如“ example.com”或“ example .com:1234“)和TLS連線。 
    // 該函式必須返回RoundTripper,然後再處理請求。 
    // 如果TLSNextProto不為nil,則不會自動啟用HTTP / 2支援。
	TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper

	// 可以選擇指定在CONNECT請求期間傳送到代理的header。
	ProxyConnectHeader Header

	// 指定對伺服器的響應標頭中允許的響應位元組數的限制。
	// 零表示使用預設限制。
	MaxResponseHeaderBytes int64

	// nextProtoOnce防止TLSNextProto和h2transport的初始化(通過OnceSetNextProtoDefaults)
	nextProtoOnce sync.Once
    // 如果http2已連線,則為非null
	h2transport   h2Transport
}

原始碼分析

1. Client.do

該方法主要實現了:

  1. 引數檢查
  2. 預設值設定
  3. 多跳請求
  4. 計算超時時間點deadline
  5. 呼叫c.send(req, deadline)
func (c *Client) do(req *Request) (retres *Response, reterr error) {
    ...
    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()
}

2. Client.send

該方法主要實現了:

  1. Cookie的裝載
  2. Transport物件的獲取
  3. 呼叫send(req, c.transport(), deadline)
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	if c.Jar != nil {
		for _, cookie := range c.Jar.Cookies(req.URL) {
			req.AddCookie(cookie)
		}
	}
	resp, didTimeout, err = send(req, c.transport(), deadline)
	if err != nil {
		return nil, didTimeout, err
	}
	if c.Jar != nil {
		if rc := resp.Cookies(); len(rc) > 0 {
			c.Jar.SetCookies(req.URL, rc)
		}
	}
	return resp, nil, nil
}

Transport的預設值

var DefaultTransport RoundTripper = &Transport{
	Proxy: ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
		DualStack: true,
	}).DialContext,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

3. http.send

該方法主要實現了:

  1. 引數校驗: URL, header, RoundTripper
  2. 超時取消: setRequestCancel(req, rt, deadline)
  3. 請求事務: rt.RoundTrip(req)
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
	...
    
    // 請求是否超時的監控
	stopTimer, didTimeout := setRequestCancel(req, rt, deadline)

    // 真正傳送請求
	resp, err = rt.RoundTrip(req)
	if err != nil {
		stopTimer()
		if resp != nil {
			log.Printf("RoundTripper returned a response & error; ignoring response")
		}
		if tlsErr, ok := err.(tls.RecordHeaderError); ok {
			// If we get a bad TLS record header, check to see if the
			// response looks like HTTP and give a more helpful error.
			// See golang.org/issue/11111.
			if string(tlsErr.RecordHeader[:]) == "HTTP/" {
				err = errors.New("http: server gave HTTP response to HTTPS client")
			}
		}
		return nil, didTimeout, err
	}
	if !deadline.IsZero() {
		resp.Body = &cancelTimerBody{
			stop:          stopTimer,
			rc:            resp.Body,
			reqDidTimeout: didTimeout,
		}
	}
	return resp, nil, nil
}

4. client.setRequestCancel

該方法主要實現了:

建立一個協程利用select chan機制阻塞等待取消請求

func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) {
	...

	doCancel := func() {
		// The newer way (the second way in the func comment):
		close(cancel)
        
		type canceler interface {
			CancelRequest(*Request)
		}
		switch v := rt.(type) {
		case *Transport, *http2Transport:
			// Do nothing. The net/http package's transports
			// support the new Request.Cancel channel
		case canceler:
			v.CancelRequest(req)
		}
	}

	stopTimerCh := make(chan struct{})
	var once sync.Once
	stopTimer = func() { once.Do(func() { close(stopTimerCh) }) }

	timer := time.NewTimer(time.Until(deadline))
	var timedOut atomicBool

	go func() {
		select {
		case <-initialReqCancel: // 使用者傳來的取消請求
			doCancel()
			timer.Stop()
		case <-timer.C: // 超時取消請求
			timedOut.setTrue()
			doCancel()
		case <-stopTimerCh:
			timer.Stop()
		}
	}()

	return stopTimer, timedOut.isSet
}

5. Transport.RoundTrip

該方法主要實現了

  1. 引數校驗: scheme, host, method, protocol...
  2. 獲取快取的或新建的連線
func (t *Transport) roundTrip(req *Request) (*Response, error) {
	...

	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.
		treq := &transportRequest{Request: req, trace: trace}
		cm, err := t.connectMethodForRequest(treq)
		if err != nil {
			req.closeBody()
			return nil, err
		}

		// 獲取快取的或新建的連線
		pconn, err := t.getConn(treq, cm)
		if err != nil {
			t.setReqCanceler(req, nil)
			req.closeBody()
			return nil, err
		}

		var resp *Response
		if pconn.alt != nil {
			// HTTP/2 path.
			t.decHostConnCount(cm.key()) // don't count cached http2 conns toward conns per host
			t.setReqCanceler(req, nil)   // not cancelable with CancelRequest
			resp, err = pconn.alt.RoundTrip(req)
		} else {
			resp, err = pconn.roundTrip(treq)
		}
		if err == nil {
			return resp, nil
		}
		if !pconn.shouldRetryRequest(req, err) {
			// Issue 16465: return underlying net.Conn.Read error from peek,
			// as we've historically done.
			if e, ok := err.(transportReadFromServerError); ok {
				err = e.err
			}
			return nil, err
		}
		testHookRoundTripRetried()

		// Rewind the body if we're able to.
		if req.GetBody != nil {
			newReq := *req
			var err error
			newReq.Body, err = req.GetBody()
			if err != nil {
				return nil, err
			}
			req = &newReq
		}
	}
}

6. Transport.getConn

  1. 首先從連線池中獲取連線t.getIdleConn(cm), 獲取成功即返回
  2. 撥號建立新連線
    1. 如果達到了最大數量則阻塞, 等待空閒
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
   req := treq.Request
   trace := treq.trace
   ctx := req.Context()
   if trace != nil && trace.GetConn != nil {
      trace.GetConn(cm.addr())
   }
    
   // 從連線池中取空閒的連線
   if pc, idleSince := t.getIdleConn(cm); pc != nil {
      if trace != nil && trace.GotConn != nil {
         trace.GotConn(pc.gotIdleConnTrace(idleSince))
      }
      // set request canceler to some non-nil function so we
      // can detect whether it was cleared between now and when
      // we enter roundTrip
      t.setReqCanceler(req, func(error) {})
      return pc, nil
   }

   // 連線池中沒有空閒的連線, 建立新連線
   // 撥號
   type dialRes struct {
      pc  *persistConn
      err error
   }
   dialc := make(chan dialRes)
   cmKey := cm.key()

   // Copy these hooks so we don't race on the postPendingDial in
   // the goroutine we launch. Issue 11136.
   testHookPrePendingDial := testHookPrePendingDial
   testHookPostPendingDial := testHookPostPendingDial

   handlePendingDial := func() {
      testHookPrePendingDial()
      go func() {
         if v := <-dialc; v.err == nil {
            t.putOrCloseIdleConn(v.pc)
         } else {
            t.decHostConnCount(cmKey)
         }
         testHookPostPendingDial()
      }()
   }

   cancelc := make(chan error, 1)
   t.setReqCanceler(req, func(err error) { cancelc <- err })

   // 如果沒有空閒的連線或已達到最大數量會阻塞
   if t.MaxConnsPerHost > 0 {
      select {
      case <-t.incHostConnCount(cmKey):
         // count below conn per host limit; proceed
      case pc := <-t.getIdleConnCh(cm):
         if trace != nil && trace.GotConn != nil {
            trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
         }
         return pc, nil
      case <-req.Cancel:
         return nil, errRequestCanceledConn
      case <-req.Context().Done():
         return nil, req.Context().Err()
      case err := <-cancelc:
         if err == errRequestCanceled {
            err = errRequestCanceledConn
         }
         return nil, err
      }
   }

   go func() {
      // 撥號建立連線
      pc, err := t.dialConn(ctx, cm)
      dialc <- dialRes{pc, err}
   }()

   idleConnCh := t.getIdleConnCh(cm)
   select {
   case v := <-dialc: // 撥號成功
      // Our dial finished.
      if v.pc != nil {
         if trace != nil && trace.GotConn != nil && v.pc.alt == nil {
            trace.GotConn(httptrace.GotConnInfo{Conn: v.pc.conn})
         }
         return v.pc, nil
      }
      // Our dial failed. See why to return a nicer error
      // value.
      t.decHostConnCount(cmKey)
      select {
      case <-req.Cancel:
         // It was an error due to cancelation, so prioritize that
         // error value. (Issue 16049)
         return nil, errRequestCanceledConn
      case <-req.Context().Done():
         return nil, req.Context().Err()
      case err := <-cancelc:
         if err == errRequestCanceled {
            err = errRequestCanceledConn
         }
         return nil, err
      default:
         // It wasn't an error due to cancelation, so
         // return the original error message:
         return nil, v.err
      }
   case pc := <-idleConnCh:
      // Another request finished first and its net.Conn
      // became available before our dial. Or somebody
      // else's dial that they didn't use.
      // But our dial is still going, so give it away
      // when it finishes:
      handlePendingDial()
      if trace != nil && trace.GotConn != nil {
         trace.GotConn(httptrace.GotConnInfo{Conn: pc.conn, Reused: pc.isReused()})
      }
      return pc, nil
   case <-req.Cancel:
      handlePendingDial()
      return nil, errRequestCanceledConn
   case <-req.Context().Done():
      handlePendingDial()
      return nil, req.Context().Err()
   case err := <-cancelc:
      handlePendingDial()
      if err == errRequestCanceled {
         err = errRequestCanceledConn
      }
      return nil, err
   }
}

7. roundTrip

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
   testHookEnterRoundTrip()
   if !pc.t.replaceReqCanceler(req.Request, pc.cancelRequest) {
      pc.t.putOrCloseIdleConn(pc)
      return nil, errRequestCanceled
   }
   pc.mu.Lock()
   pc.numExpectedResponses++
   headerFn := pc.mutateHeaderFunc
   pc.mu.Unlock()

   if headerFn != nil {
      headerFn(req.extraHeaders())
   }

   // Ask for a compressed version if the caller didn't set their
   // own value for Accept-Encoding. We only attempt to
   // uncompress the gzip stream if we were the layer that
   // requested it.
   requestedGzip := false
   if !pc.t.DisableCompression &&
      req.Header.Get("Accept-Encoding") == "" &&
      req.Header.Get("Range") == "" &&
      req.Method != "HEAD" {
      // Request gzip only, not deflate. Deflate is ambiguous and
      // not as universally supported anyway.
      // See: https://zlib.net/zlib_faq.html#faq39
      //
      // Note that we don't request this for HEAD requests,
      // due to a bug in nginx:
      //   https://trac.nginx.org/nginx/ticket/358
      //   https://golang.org/issue/5522
      //
      // We don't request gzip if the request is for a range, since
      // auto-decoding a portion of a gzipped document will just fail
      // anyway. See https://golang.org/issue/8923
      requestedGzip = true
      req.extraHeaders().Set("Accept-Encoding", "gzip")
   }

   var continueCh chan struct{}
   if req.ProtoAtLeast(1, 1) && req.Body != nil && req.expectsContinue() {
      continueCh = make(chan struct{}, 1)
   }

   if pc.t.DisableKeepAlives && !req.wantsClose() {
      req.extraHeaders().Set("Connection", "close")
   }

   gone := make(chan struct{})
   defer close(gone)

   defer func() {
      if err != nil {
         pc.t.setReqCanceler(req.Request, nil)
      }
   }()

   const debugRoundTrip = false

   // Write the request concurrently with waiting for a response,
   // in case the server decides to reply before reading our full
   // request body.
   startBytesWritten := pc.nwrite
   writeErrCh := make(chan error, 1)
   pc.writech <- writeRequest{req, writeErrCh, continueCh}

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

   var respHeaderTimer <-chan time.Time
   cancelChan := req.Request.Cancel
   ctxDoneChan := req.Context().Done()
   for {
      testHookWaitResLoop()
      select {
      case err := <-writeErrCh:
         if debugRoundTrip {
            req.logf("writeErrCh resv: %T/%#v", err, err)
         }
         if err != nil {
            pc.close(fmt.Errorf("write error: %v", err))
            return nil, pc.mapRoundTripError(req, startBytesWritten, err)
         }
         if d := pc.t.ResponseHeaderTimeout; d > 0 {
            if debugRoundTrip {
               req.logf("starting timer for %v", d)
            }
            timer := time.NewTimer(d)
            defer timer.Stop() // prevent leaks
            respHeaderTimer = timer.C
         }
      case <-pc.closech:
         if debugRoundTrip {
            req.logf("closech recv: %T %#v", pc.closed, pc.closed)
         }
         return nil, pc.mapRoundTripError(req, startBytesWritten, pc.closed)
      case <-respHeaderTimer:
         if debugRoundTrip {
            req.logf("timeout waiting for response headers.")
         }
         pc.close(errTimeout)
         return nil, errTimeout
      case re := <-resc:
         if (re.res == nil) == (re.err == nil) {
            panic(fmt.Sprintf("internal error: exactly one of res or err should be set; nil=%v", re.res == nil))
         }
         if debugRoundTrip {
            req.logf("resc recv: %p, %T/%#v", re.res, re.err, re.err)
         }
         if re.err != nil {
            return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)
         }
         return re.res, nil
      case <-cancelChan:
         pc.t.CancelRequest(req.Request)
         cancelChan = nil
      case <-ctxDoneChan:
         pc.t.cancelRequest(req.Request, req.Context().Err())
         cancelChan = nil
         ctxDoneChan = nil
      }
   }
}

參考

  1. golang 標準庫 http 的 client 為什麼必須手動關閉 resp.Body
  2. [Go Http包解析:為什麼需要response.Body.Close()](https://www.cnblogs.com/lovezbs/p/13197587.html)

相關文章