Go中的HTTP請求之——HTTP1.1請求流程分析

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來自公衆號:新世界雜貨鋪"}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http是目前應用最爲廣泛, 也是程序員接觸最多的協議之一。今天筆者站在GoPher的角度對http1.1的請求流程進行全面的分析。希望讀者讀完此文後, 能夠有以下幾個收穫:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"對http1.1的請求流程有一個大概的瞭解"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在平時的開發中能夠更好地重用底層TCP連接"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"對http1.1的線頭阻塞能有一個更清楚的認識"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"HTTP1.1流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"今天內容較多, 廢話不多說, 直接上乾貨。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f0/f03408c0795f6067928d91c8e3959e74.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來, 筆者將根據流程圖,對除了NewRequest以外的函數進行逐步的展開和分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Client).do"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Client).do方法的核心代碼是一個沒有結束條件的for循環。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"for {\n\t// For all but the first request, create the next\n\t// request hop and replace req.\n\tif len(reqs) > 0 {\n\t\tloc := resp.Header.Get(\"Location\")\n\t\t// ...此處省略代碼...\n\t\terr = c.checkRedirect(req, reqs)\n\t\t// ...此處省略很多代碼...\n\t}\n\n\treqs = append(reqs, req)\n\tvar err error\n\tvar didTimeout func() bool\n\tif resp, didTimeout, err = c.send(req, deadline); err != nil {\n\t\t// c.send() always closes req.Body\n\t\treqBodyClosed = true\n\t\t// ...此處省略代碼...\n\t\treturn nil, uerr(err)\n\t}\n\n\tvar shouldRedirect bool\n\tredirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])\n\tif !shouldRedirect {\n\t\treturn resp, nil\n\t}\n\n\treq.closeBody()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼中, 請求第一次進入會調用"},{"type":"codeinline","content":[{"type":"text","text":"c.send"}]},{"type":"text","text":", 得到響應後會判斷請求是否需要重定向, 如果需要重定向則繼續循環, 否則返回響應。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入重定向流程後, 這裏筆者簡單介紹一下"},{"type":"codeinline","content":[{"type":"text","text":"checkRedirect"}]},{"type":"text","text":"函數:"}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func defaultCheckRedirect(req *Request, via []*Request) error {\n\tif len(via) >= 10 {\n\t\treturn errors.New(\"stopped after 10 redirects\")\n\t}\n\treturn nil\n}\n// ...\nfunc (c *Client) checkRedirect(req *Request, via []*Request) error {\n\tfn := c.CheckRedirect\n\tif fn == nil {\n\t\tfn = defaultCheckRedirect\n\t}\n\treturn fn(req, via)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知, 用戶可以自己定義重定向的檢查規則。如果用戶沒有自定義檢查規則, 則"},{"type":"text","marks":[{"type":"strong"}],"text":"重定向次數不能超過10次"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Client).send"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\*Client).send方法邏輯較爲簡單, 主要看用戶有沒有爲"},{"type":"text","marks":[{"type":"strong"}],"text":"http.Client的Jar"},{"type":"text","text":"字段實現"},{"type":"codeinline","content":[{"type":"text","text":"CookieJar"}]},{"type":"text","text":"接口。主要流程如下:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果實現了CookieJar接口, 爲Request添加保存的cookie信息。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"調用"},{"type":"codeinline","content":[{"type":"text","text":"send"}]},{"type":"text","text":"函數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果實現了CookieJar接口, 將Response中的cookie信息保存下來。"}]}]}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"// didTimeout is non-nil only if err != nil.\nfunc (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {\n\tif c.Jar != nil {\n\t\tfor _, cookie := range c.Jar.Cookies(req.URL) {\n\t\t\treq.AddCookie(cookie)\n\t\t}\n\t}\n\tresp, didTimeout, err = send(req, c.transport(), deadline)\n\tif err != nil {\n\t\treturn nil, didTimeout, err\n\t}\n\tif c.Jar != nil {\n\t\tif rc := resp.Cookies(); len(rc) > 0 {\n\t\t\tc.Jar.SetCookies(req.URL, rc)\n\t\t}\n\t}\n\treturn resp, nil, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外, 我們還需要關注"},{"type":"codeinline","content":[{"type":"text","text":"c.transport()"}]},{"type":"text","text":"的調用。如果用戶未對http.Client指定Transport則會使用go默認的DefaultTransport。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該Transport實現RoundTripper接口。在go中RoundTripper的定義爲“"},{"type":"text","marks":[{"type":"strong"}],"text":"執行單個HTTP事務的能力,獲取給定請求的響應"},{"type":"text","text":"”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (c *Client) transport() RoundTripper {\n\tif c.Transport != nil {\n\t\treturn c.Transport\n\t}\n\treturn DefaultTransport\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"send"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"send函數會檢查request的URL,以及參數的rt, 和header值。如果URL和rt爲nil則直接返回錯誤。同時, 如果請求中設置了用戶信息, 還會檢查並設置basic的驗證頭信息,最後調用"},{"type":"codeinline","content":[{"type":"text","text":"rt.RoundTrip"}]},{"type":"text","text":"得到請求的響應。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {\n\treq := ireq // req is either the original request, or a modified fork\n\t// ...此處省略代碼...\n\tif u := req.URL.User; u != nil && req.Header.Get(\"Authorization\") == \"\" {\n\t\tusername := u.Username()\n\t\tpassword, _ := u.Password()\n\t\tforkReq()\n\t\treq.Header = cloneOrMakeHeader(ireq.Header)\n\t\treq.Header.Set(\"Authorization\", \"Basic \"+basicAuth(username, password))\n\t}\n\n\tif !deadline.IsZero() {\n\t\tforkReq()\n\t}\n\tstopTimer, didTimeout := setRequestCancel(req, rt, deadline)\n\n\tresp, err = rt.RoundTrip(req)\n\tif err != nil {\n // ...此處省略代碼...\n\t\treturn nil, didTimeout, err\n\t}\n\t// ...此處省略代碼...\n\treturn resp, nil, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"(*Transport).RoundTrip"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\"},{"type":"text","marks":[{"type":"italic"}],"text":"Transport).RoundTrip的邏輯很簡單,它會調用(\\"},{"type":"text","text":"Transport).roundTrip方法,因此本節實際上是對(\\*Transport).roundTrip方法的分析。"}]},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) RoundTrip(req *Request) (*Response, error) {\n\treturn t.roundTrip(req)\n}\nfunc (t *Transport) roundTrip(req *Request) (*Response, error) {\n\t// ...此處省略校驗header頭和headervalue的代碼以及其他代碼...\n\n\tfor {\n\t\tselect {\n\t\tcase 0 && !stop {\n\t\t\tpconn := list[len(list)-1]\n\n\t\t\t// See whether this connection has been idle too long, considering\n\t\t\t// only the wall time (the Round(0)), in case this is a laptop or VM\n\t\t\t// coming out of suspend with previously cached idle connections.\n\t\t\ttooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)\n\t\t\t// ...此處省略代碼...\n\t\t\tdelivered = w.tryDeliver(pconn, nil)\n\t\t\tif delivered {\n\t\t\t\t// ...此處省略代碼...\n\t\t\t}\n\t\t\tstop = true\n\t\t}\n\t\tif len(list) > 0 {\n\t\t\tt.idleConn[w.key] = list\n\t\t} else {\n\t\t\tdelete(t.idleConn, w.key)\n\t\t}\n\t\tif stop {\n\t\t\treturn delivered\n\t\t}\n\t}\n\n\t// Register to receive next connection that becomes idle.\n\tif t.idleConnWait == nil {\n\t\tt.idleConnWait = make(map[connectMethodKey]wantConnQueue)\n\t}\n\tq := t.idleConnWait[w.key]\n\tq.cleanFront()\n\tq.pushBack(w)\n\tt.idleConnWait[w.key] = q\n\treturn false\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中"},{"type":"codeinline","content":[{"type":"text","text":"w.tryDeliver"}]},{"type":"text","text":"方法主要作用是將連接協程安全的賦值給"},{"type":"codeinline","content":[{"type":"text","text":"w.pc"}]},{"type":"text","text":",並關閉"},{"type":"codeinline","content":[{"type":"text","text":"w.ready"}]},{"type":"text","text":"管道。此時我們便可以和(*Transport).getConn中調用queueForIdleConn成功後的返回值對應上。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(*Transport).queueForDial"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).queueForDial方法包含三個步驟:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果t.MaxConnsPerHost小於等於0,執行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"並返回。其中MaxConnsPerHost代表着每個host的最大連接數,小於等於0表示不限制。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果當前host的連接數不超過t.MaxConnsPerHost,對當前host的連接數+1,然後執行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"並返回。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果當前host的連接數等於t.MaxConnsPerHost,則將"},{"type":"codeinline","content":[{"type":"text","text":"wantConn"}]},{"type":"text","text":"結構體變量放入"},{"type":"codeinline","content":[{"type":"text","text":"t.connsPerHostWait[w.key]"}]},{"type":"text","text":"等待隊列,此處wantConn結構體變量就是前面提到的"},{"type":"codeinline","content":[{"type":"text","text":"w"}]},{"type":"text","text":"。另外在放入等待隊列前會先清除隊列中已經失效或者不再等待的變量。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) queueForDial(w *wantConn) {\n\tw.beforeDial()\n\tif t.MaxConnsPerHost <= 0 {\n\t\tgo t.dialConnFor(w)\n\t\treturn\n\t}\n\n\tt.connsPerHostMu.Lock()\n\tdefer t.connsPerHostMu.Unlock()\n\n\tif n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {\n\t\tif t.connsPerHost == nil {\n\t\t\tt.connsPerHost = make(map[connectMethodKey]int)\n\t\t}\n\t\tt.connsPerHost[w.key] = n + 1\n\t\tgo t.dialConnFor(w)\n\t\treturn\n\t}\n\n\tif t.connsPerHostWait == nil {\n\t\tt.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)\n\t}\n\tq := t.connsPerHostWait[w.key]\n\tq.cleanFront()\n\tq.pushBack(w)\n\tt.connsPerHostWait[w.key] = q\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"(*Transport).dialConnFor"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(\\*Transport).dialConnFor方法調用"},{"type":"text","marks":[{"type":"strong"}],"text":"t.dialConn"},{"type":"text","text":"獲取一個真正的"},{"type":"codeinline","content":[{"type":"text","text":"*persistConn"}]},{"type":"text","text":"。並將這個連接傳遞給w, 如果w已經獲取到了連接,則會傳遞失敗,此時調用"},{"type":"codeinline","content":[{"type":"text","text":"t.putOrCloseIdleConn"}]},{"type":"text","text":"將連接放回空閒連接池。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果連接獲取錯誤則會調用"},{"type":"codeinline","content":[{"type":"text","text":"t.decConnsPerHost"}]},{"type":"text","text":"減少當前host的連接數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) dialConnFor(w *wantConn) {\n\tdefer w.afterDial()\n\n\tpc, err := t.dialConn(w.ctx, w.cm)\n\tdelivered := w.tryDeliver(pc, err)\n\tif err == nil && (!delivered || pc.alt != nil) {\n\t\t// pconn was not passed to w,\n\t\t// or it is HTTP/2 and can be shared.\n\t\t// Add to the idle connection pool.\n\t\tt.putOrCloseIdleConn(pc)\n\t}\n\tif err != nil {\n\t\tt.decConnsPerHost(w.key)\n\t}\n}"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).putOrCloseIdleConn方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {\n\tif err := t.tryPutIdleConn(pconn); err != nil {\n\t\tpconn.close(err)\n\t}\n}\nfunc (t *Transport) tryPutIdleConn(pconn *persistConn) error {\n\tif t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 {\n\t\treturn errKeepAlivesDisabled\n\t}\n\t// ...此處省略代碼...\n\tt.idleMu.Lock()\n\tdefer t.idleMu.Unlock()\n\t// ...此處省略代碼...\n\t\n\t// Deliver pconn to goroutine waiting for idle connection, if any.\n\t// (They may be actively dialing, but this conn is ready first.\n\t// Chrome calls this socket late binding.\n\t// See https://insouciant.org/tech/connection-management-in-chromium/.)\n\tkey := pconn.cacheKey\n\tif q, ok := t.idleConnWait[key]; ok {\n\t\tdone := false\n\t\tif pconn.alt == nil {\n\t\t\t// HTTP/1.\n\t\t\t// Loop over the waiting list until we find a w that isn't done already, and hand it pconn.\n\t\t\tfor q.len() > 0 {\n\t\t\t\tw := q.popFront()\n\t\t\t\tif w.tryDeliver(pconn, nil) {\n\t\t\t\t\tdone = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// HTTP/2.\n\t\t\t// Can hand the same pconn to everyone in the waiting list,\n\t\t\t// and we still won't be done: we want to put it in the idle\n\t\t\t// list unconditionally, for any future clients too.\n\t\t\tfor q.len() > 0 {\n\t\t\t\tw := q.popFront()\n\t\t\t\tw.tryDeliver(pconn, nil)\n\t\t\t}\n\t\t}\n\t\tif q.len() == 0 {\n\t\t\tdelete(t.idleConnWait, key)\n\t\t} else {\n\t\t\tt.idleConnWait[key] = q\n\t\t}\n\t\tif done {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif t.closeIdle {\n\t\treturn errCloseIdle\n\t}\n\tif t.idleConn == nil {\n\t\tt.idleConn = make(map[connectMethodKey][]*persistConn)\n\t}\n\tidles := t.idleConn[key]\n\tif len(idles) >= t.maxIdleConnsPerHost() {\n\t\treturn errTooManyIdleHost\n\t}\n\t// ...此處省略代碼...\n\tt.idleConn[key] = append(idles, pconn)\n\tt.idleLRU.add(pconn)\n\t// ...此處省略代碼...\n\t// Set idle timer, but only for HTTP/1 (pconn.alt == nil).\n\t// The HTTP/2 implementation manages the idle timer itself\n\t// (see idleConnTimeout in h2_bundle.go).\n\tif t.IdleConnTimeout > 0 && pconn.alt == nil {\n\t\tif pconn.idleTimer != nil {\n\t\t\tpconn.idleTimer.Reset(t.IdleConnTimeout)\n\t\t} else {\n\t\t\tpconn.idleTimer = time.AfterFunc(t.IdleConnTimeout, pconn.closeConnIfStillIdle)\n\t\t}\n\t}\n\tpconn.idleAt = time.Now()\n\treturn nil\n}\nfunc (t *Transport) maxIdleConnsPerHost() int {\n\tif v := t.MaxIdleConnsPerHost; v != 0 {\n\t\treturn v\n\t}\n\treturn DefaultMaxIdleConnsPerHost // 2\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,將連接放入t.idleConn前,先檢查t.idleConnWait的數量。如果有請求在等待空閒連接, 則將連接複用,沒有空閒連接時,纔將連接放入t.idleConn。連接放入t.idleConn後,還會重置連接的可空閒時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外在t.putOrCloseIdleConn函數中還需要注意兩點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"如果用戶自定義了http.client,且將DisableKeepAlives設置爲true,或者將MaxIdleConnsPerHost設置爲負數,則連接不會放入t.idleConn即連接不能複用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在判斷已有空閒連接數量時, 如果MaxIdleConnsPerHost 不等於0, 則返回用戶設置的數量,否則返回默認值2,詳見上面的"},{"type":"codeinline","content":[{"type":"text","text":"(*Transport).maxIdleConnsPerHost"}]},{"type":"text","text":" 函數。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"綜上, 我們知道對於部分有連接數限制的業務, 我們可以爲http.Client自定義一個Transport, 並設置Transport的"},{"type":"codeinline","content":[{"type":"text","text":"MaxConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"MaxIdleConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"IdleConnTimeout"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"DisableKeepAlives"}]},{"type":"text","text":"從而達到即限制連接數量,又能保證一定的併發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*Transport).decConnsPerHost方法"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) decConnsPerHost(key connectMethodKey) {\n\t// ...此處省略代碼...\n\tt.connsPerHostMu.Lock()\n\tdefer t.connsPerHostMu.Unlock()\n\tn := t.connsPerHost[key]\n\t// ...此處省略代碼...\n\n\t// Can we hand this count to a goroutine still waiting to dial?\n\t// (Some goroutines on the wait list may have timed out or\n\t// gotten a connection another way. If they're all gone,\n\t// we don't want to kick off any spurious dial operations.)\n\tif q := t.connsPerHostWait[key]; q.len() > 0 {\n\t\tdone := false\n\t\tfor q.len() > 0 {\n\t\t\tw := q.popFront()\n\t\t\tif w.waiting() {\n\t\t\t\tgo t.dialConnFor(w)\n\t\t\t\tdone = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif q.len() == 0 {\n\t\t\tdelete(t.connsPerHostWait, key)\n\t\t} else {\n\t\t\t// q is a value (like a slice), so we have to store\n\t\t\t// the updated q back into the map.\n\t\t\tt.connsPerHostWait[key] = q\n\t\t}\n\t\tif done {\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Otherwise, decrement the recorded count.\n\tif n--; n == 0 {\n\t\tdelete(t.connsPerHost, key)\n\t} else {\n\t\tt.connsPerHost[key] = n\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知, decConnsPerHost方法主要乾了兩件事:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"判斷是否有請求在等待撥號, 如果有則執行"},{"type":"codeinline","content":[{"type":"text","text":"go t.dialConnFor(w)"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有請求在等待撥號, 則減少當前host的連接數量。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"###### (*Transport).dialConn"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據http.Client的默認配置和實際的debug結果,(*Transport).dialConn方法主要邏輯如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"調用"},{"type":"codeinline","content":[{"type":"text","text":"t.dial(ctx, \"tcp\", cm.addr())"}]},{"type":"text","text":"創建TCP連接。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果是https的請求, 則對請求建立安全的tls傳輸通道。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"爲persistConn創建讀寫buffer, 如果用戶沒有自定義讀寫buffer的大小, 根據writeBufferSize和readBufferSize方法可知, 讀寫bufffer的大小默認爲4096。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"執行"},{"type":"codeinline","content":[{"type":"text","text":"go pconn.readLoop()"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"go pconn.writeLoop()"}]},{"type":"text","text":"開啓讀寫循環然後返回連接。 "}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {\n\tpconn = &persistConn{\n\t\tt: t,\n\t\tcacheKey: cm.key(),\n\t\treqch: make(chan requestAndChan, 1),\n\t\twritech: make(chan writeRequest, 1),\n\t\tclosech: make(chan struct{}),\n\t\twriteErrCh: make(chan error, 1),\n\t\twriteLoopDone: make(chan struct{}),\n\t}\n\t// ...此處省略代碼...\n\tif cm.scheme() == \"https\" && t.hasCustomTLSDialer() {\n\t\t// ...此處省略代碼...\n\t} else {\n\t\tconn, err := t.dial(ctx, \"tcp\", cm.addr())\n\t\tif err != nil {\n\t\t\treturn nil, wrapErr(err)\n\t\t}\n\t\tpconn.conn = conn\n\t\tif cm.scheme() == \"https\" {\n\t\t\tvar firstTLSHost string\n\t\t\tif firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil {\n\t\t\t\treturn nil, wrapErr(err)\n\t\t\t}\n\t\t\tif err = pconn.addTLS(firstTLSHost, trace); err != nil {\n\t\t\t\treturn nil, wrapErr(err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Proxy setup.\n\tswitch { // ...此處省略代碼... }\n\n\tif cm.proxyURL != nil && cm.targetScheme == \"https\" {\n\t\t// ...此處省略代碼...\n\t}\n\n\tif s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != \"\" {\n\t\t// ...此處省略代碼...\n\t}\n\n\tpconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())\n\tpconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())\n\n\tgo pconn.readLoop()\n\tgo pconn.writeLoop()\n\treturn pconn, nil\n}\nfunc (t *Transport) writeBufferSize() int {\n\tif t.WriteBufferSize > 0 {\n\t\treturn t.WriteBufferSize\n\t}\n\treturn 4 << 10\n}\n\nfunc (t *Transport) readBufferSize() int {\n\tif t.ReadBufferSize > 0 {\n\t\treturn t.ReadBufferSize\n\t}\n\treturn 4 << 10\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"(*persistConn).roundTrip"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(*persistConn).roundTrip方法是http1.1請求的核心之一,該方法在這裏獲取真實的Response並返回給上層。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {\n\t// ...此處省略代碼...\n\n\tgone := make(chan struct{})\n\tdefer close(gone)\n\t// ...此處省略代碼...\n\tconst debugRoundTrip = false\n\n\t// Write the request concurrently with waiting for a response,\n\t// in case the server decides to reply before reading our full\n\t// request body.\n\tstartBytesWritten := pc.nwrite\n\twriteErrCh := make(chan error, 1)\n\tpc.writech "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">上述流程中筆者對很多細節並未詳細提及或者僅一筆帶過,希望讀者酌情參考。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在go中發起http1.1的請求時, 如果遇到不關心響應的請求,請務必完整讀取響應內容以保證連接的複用性。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果遇到對連接數有限制的業務,可以通過自定義http.Client的Transport, 並設置Transport的"},{"type":"codeinline","content":[{"type":"text","text":"MaxConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"MaxIdleConnsPerHost"}]},{"type":"text","text":","},{"type":"codeinline","content":[{"type":"text","text":"IdleConnTimeout"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"DisableKeepAlives"}]},{"type":"text","text":"的值,來控制連接數。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"如果對於重定向業務邏輯有需求,可以自定義http.Client的"},{"type":"codeinline","content":[{"type":"text","text":"CheckRedirect"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"在http1.1,中一個連接上的請求,只有等前一個請求處理完之後才能繼續下一個請求。如果前面的請求處理較慢, 則後面的請求必須等待, 這就是http1.1中的線頭阻塞。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注: 寫本文時, 筆者所用go版本爲: go1.14.2 "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"生命不息, 探索不止, 後續將持續更新有關於go的技術探索 "}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原創不易, 卑微求關注收藏二連."}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章