前言
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請求流程
- 建立http.Client物件
client
- 建立http.Request物件
req
- 傳送請求
client.do(req)
- 關閉
resp.Body.Close()
即使直接呼叫client.Get()
或client.Post()
, 內部同樣建立了request
, 且最終總是通過client.Do()
方法呼叫私有的client.do()
方法, 執行請求;
http請求核心類
- http.Client
- http.Request
- http.Transport
http.Client
該類主要功能:
- Cookie
- Timeout
- Redirect
- 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
- Transport用來快取連線, 以供將來重用, 而不是根據需要建立
- Transport是併發安全的
- 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
該方法主要實現了:
- 引數檢查
- 預設值設定
- 多跳請求
- 計算超時時間點deadline
- 呼叫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
該方法主要實現了:
- Cookie的裝載
- Transport物件的獲取
- 呼叫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
該方法主要實現了:
- 引數校驗: URL, header, RoundTripper
- 超時取消: setRequestCancel(req, rt, deadline)
- 請求事務: 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
該方法主要實現了
- 引數校驗: scheme, host, method, protocol...
- 獲取快取的或新建的連線
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
- 首先從連線池中獲取連線
t.getIdleConn(cm)
, 獲取成功即返回 - 撥號建立新連線
- 如果達到了最大數量則阻塞, 等待空閒
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
}
}
}