圖文講透Golang標準庫 net/http實現(xiàn)原理 - 客戶端
客戶端的內(nèi)容將是如何發(fā)送請求和接收響應(yīng),走完客戶端就把整個流程就完整的串聯(lián)起來了!
這次我把調(diào)用的核心方法和流程走讀的函數(shù)也貼出來,這樣看應(yīng)該更有邏輯感,重要部分用紅色標記了一下,可以著重看下。
圖片
先了解下核心數(shù)據(jù)結(jié)構(gòu)Client和Request。
Client結(jié)構(gòu)體
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
四個字段分別是:
- ? Transport:表示 HTTP 事務(wù),用于處理客戶端的請求連接并等待服務(wù)端的響應(yīng);
- ? CheckRedirect:處理重定向的策略
- ? Jar:管理和存儲請求中的 cookie
- ? Timeout:超時設(shè)置
Request結(jié)構(gòu)體
Request字段較多,這里就列舉一下常見的一些字段
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
Host string
Response *Response
...
}
- ? Method:指定的HTTP方法(GET、POST、PUT等)
- ? URL:請求路徑
- ? Header:請求頭
- ? Body:請求體
- ? Host:服務(wù)器主機
- ? Response:響應(yīng)參數(shù)
構(gòu)造請求
var DefaultClient = &Client{}
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
示例HTTP 的 Get方法會調(diào)用到 DefaultClient 的 Get 方法,,然后調(diào)用到 Client 的 Get 方法。
DefaultClient 是 Client 的一個空實例(跟DefaultServeMux有點子相似)
圖片
Client.Get
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
Client.Get() 根據(jù)用戶的入?yún)?,請求參?shù) NewRequest使用上下文包裝NewRequestWithContext ,接著通過 Client.Do 方法,處理這個請求。
NewRequestWithContext
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
...
// 解析url
u, err := urlpkg.Parse(url)
...
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,
}
...
return req, nil
}
NewRequestWithContext 函數(shù)主要是功能是將請求封裝成一個 Request 結(jié)構(gòu)體并返回,這個結(jié)構(gòu)體的名稱是req。
準備發(fā)送請求
構(gòu)造好的Request結(jié)構(gòu)req,會傳入c.Do()方法。
我們看下發(fā)送請求過程調(diào)用了哪些方法,用下圖表示下
圖片
?? 其實不管是Get還是Post請求的調(diào)用流程都是一樣的,只是對外封裝了Post和Get請求
func (c *Client) do(req *Request) (retres *Response, reterr error) {
...
for {
...
resp, didTimeout, err = send(req, deadline)
if err != nil {
return nil, didTimeout, err
}
}
...
}
//Client 調(diào)用 Do 方法處理發(fā)送請求最后會調(diào)用到 send 函數(shù)中
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
resp, didTimeout, err = send(req, c.transport(), deadline)
if err != nil {
return nil, didTimeout, err
}
...
return resp, nil, nil
}
c.transport()方法是為了回去Transport的默認實例 DefaultTransport ,我們看下DefaultTransport長什么樣。
DefaultTransport
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
可以根據(jù)需要建立網(wǎng)絡(luò)連接,并緩存它們以供后續(xù)調(diào)用重用,部分參數(shù)如下:
- ? MaxIdleConns:最大空閑連接數(shù)
- ? IdleConnTimeout:空閑連接超時時間
- ? ExpectContinueTimeout:預(yù)計繼續(xù)超時
注意這里的RoundTripper是個接口,也就是說 Transport 實現(xiàn) RoundTripper 接口,該接口方法接收Request,返回Response。
RoundTripper
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
圖片
雖然還沒看完后面邏輯,不過我們猜測RoundTrip方法可能是實際處理客戶端請求的實現(xiàn)。
我們繼續(xù)追下后面邏輯,看下是否能驗證這個猜想。
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
...
resp, err = rt.RoundTrip(req)
if err != nil {
...
}
..
}
?? 你看send函數(shù)的第二個參數(shù)就是接口類型,調(diào)用層傳遞的Transport的實例DefaultTransport。
而rt.RoundTrip()方法的調(diào)用具體在net/http/roundtrip.go文件中,這也是RoundTrip接口的實現(xiàn),代碼如下:
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}
Transport.roundTrip 方法概況來說干了這些事:
- ? 封裝請求transportRequest
- ? 調(diào)用 Transport 的 getConn 方法獲取連接
- ? 在獲取到連接后,調(diào)用 persistConn 的 roundTrip 方法等待請求響應(yīng)結(jié)果
func (t *Transport) roundTrip(req *Request) (*Response, error) {
...
for {
...
// 請求封裝
treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey}
cm, err := t.connectMethodForRequest(treq)
if err != nil {
...
}
// 獲取連接
pconn, err := t.getConn(treq, cm)
if err != nil {
...
}
// 等待響應(yīng)結(jié)果
var resp *Response
if pconn.alt != nil {
t.setReqCanceler(cancelKey, nil)
resp, err = pconn.alt.RoundTrip(req)
} else {
resp, err = pconn.roundTrip(treq)
}
...
}
}
封裝請求transportRequeste沒啥好說的,因為treq被roundTrip修改,所以這里需要為每次重試重新創(chuàng)建。
獲取連接
獲取連接的方法是 getConn,這里代碼還是比較長的,會有不同的兩種方式去獲取連接:
- 1. 調(diào)用 queueForIdleConn 排隊等待獲取空閑連接
- 2. 如果獲取空閑連接失敗,那么調(diào)用 queueForDial 異步創(chuàng)建一個新的連接,并通過channel來接收readdy信號,來確認連接是否構(gòu)造完成
圖片
getConn
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
...
// 初始化wantConn結(jié)構(gòu)體
w := &wantConn{
cm: cm,
key: cm.key(),
ctx: ctx,
ready: make(chan struct{}, 1),
beforeDial: testHookPrePendingDial,
afterDial: testHookPostPendingDial,
}
...
// 獲取空閑連接
if delivered := t.queueForIdleConn(w); delivered {
...
}
// 異步創(chuàng)建新連接
t.queueForDial(w)
select {
// 阻塞等待獲取到連接完成
case <-w.ready:
...
return w.pc, w.err
...
}
queueForIdleConn獲取空閑連接
獲取成功
成功空閑獲取連接Conn流程如下圖
圖片
- 1. 根據(jù)wantConn的key從 transport.idleConn 這個map中查找,看是否存不存在空閑的 connection 列表
- 2. 獲取到空閑的 connection 列表后,從列表中拿最后一個 connection
- 3. 獲取到連接后會調(diào)用 wantConn.tryDeliver 方法將連接綁定到 wantConn 請求參數(shù)上
獲取失敗
圖片
當不存在該請求的 connection 列表,會將當前 wantConn 加入到名稱為 idleConnWait 的等待空閑map中。
不過此時的idleConnWait這個map的值是個隊列
queueForIdleConn方法
從上面的兩張圖解中差不多能看出是如何獲取空閑連接和如何獲取失敗時如何做的了,這里也貼下代碼體驗下,讓大家更清楚里面的實現(xiàn)邏輯。
//idleConn是map類型,指定key返回切片列表
idleConn map[connectMethodKey][]*persistConn
//idleConnWait,指定key返回隊列
idleConnWait map[connectMethodKey]wantConnQueue
這里將獲取空閑連接的代碼實現(xiàn)多進行注釋,更好理解一些!
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
//參數(shù)判斷
if t.DisableKeepAlives {
return false
}
if w == nil {
return false
}
// 計算空閑連接超時時間
var oldTime time.Time
if t.IdleConnTimeout > 0 {
oldTime = time.Now().Add(-t.IdleConnTimeout)
}
//從idleConn根據(jù)w.key找對應(yīng)的persistConn 列表
if list, ok := t.idleConn[w.key]; ok {
stop := false
delivered := false
for len(list) > 0 && !stop {
// 找到persistConn列表最后一個
pconn := list[len(list)-1]
// 檢查這個 persistConn 是不是過期
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
if tooOld {
//如果過期進行異步清理
go pconn.closeConnIfStillIdle()
}
// 該 persistConn 被標記為 broken 或 閑置太久 continue
if pconn.isBroken() || tooOld {
list = list[:len(list)-1]
continue
}
// 嘗試將該 persistConn 寫入到 wantConn(w)中
delivered = w.tryDeliver(pconn, nil)
if delivered {
// 寫入成功,將persistConn從空閑列表中移除
if pconn.alt != nil {
} else {
t.idleLRU.remove(pconn)
//缺省了最后一個conn
list = list[:len(list)-1]
}
}
stop = true
}
//對被獲取連接后的列表進行判斷
if len(list) > 0 {
t.idleConn[w.key] = list
} else {
// 如果該 key 對應(yīng)的空閑列表不存在,那么將該key從字典中移除
delete(t.idleConn, w.key)
}
if stop {
return delivered
}
}
// 如果找不到空閑的 persistConn
if t.idleConnWait == nil {
t.idleConnWait = make(map[connectMethodKey]wantConnQueue)
}
// 將該 wantConn添加到等待空閑idleConnWait中
q := t.idleConnWait[w.key]
q.cleanFront()
q.pushBack(w)
t.idleConnWait[w.key] = q
return false
}
我們知道了為找到的空閑連接會被放到空閑 idleConnWait 這個等待map中,最后會被Transport.tryPutIdleConn方法將pconne添加到等待新請求的空閑持久連接列表中。
queueForDial創(chuàng)建新連接
queueForDial意思是排隊等待撥號,為什么說是等帶呢,因為最終的結(jié)果是在ready這個channel上進行通知的。
流程如下圖:
圖片
我們先看下Transport結(jié)構(gòu)體的這兩個map,名稱不一樣map的屬性和解釋都是一樣的,其中idleConnWait是在沒查找空閑連接的時候存放當前連接的map。
而connsPerHostWait用在了創(chuàng)建新連接的地方,可以猜測一下創(chuàng)建新鏈接的地方就是將當前的請求放入到 connsPerHostWait 等待map中。
// waiting getConns
idleConnWait map[connectMethodKey]wantConnQueue
// waiting getConns
connsPerHostWait map[connectMethodKey]wantConnQueue
Transport.queueForDial
func (t *Transport) queueForDial(w *wantConn) {
w.beforeDial()
// 小于等于零,意思是限制,直接異步建立連接
if t.MaxConnsPerHost <= 0 {
go t.dialConnFor(w)
return
}
...
//host建立的連接數(shù)沒達到上限,執(zhí)行異步建立連接
if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
if t.connsPerHost == nil {
t.connsPerHost = make(map[connectMethodKey]int)
}
t.connsPerHost[w.key] = n + 1
go t.dialConnFor(w)
return
}
//進入等待隊列
if t.connsPerHostWait == nil {
t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
}
q := t.connsPerHostWait[w.key]
q.cleanFront()
q.pushBack(w)
t.connsPerHostWait[w.key] = q
}
在獲取不到空閑連接之后,會嘗試去建立連接:
- 1. queueForDial 方法的內(nèi)部會先校驗 MaxConnsPerHost 是否未設(shè)置和是否已達上限
- 1.
- 1. 檢驗不通過則將當前的請求放入到 connsPerHostWait 這個等待map中
- 2. 校驗通過那么會異步的調(diào)用 dialConnFor 方法創(chuàng)建連接
??那會不會queueForDial方法中將idleConnWait和connsPerHostWait打包到等待空閑連接idleConn這個map中呢?
我們繼續(xù)看dialConnFor的實現(xiàn),它會給我們這個問題的答案!
dialConnFor
func (t *Transport) dialConnFor(w *wantConn) {
defer w.afterDial()
//創(chuàng)建 persistConn
pc, err := t.dialConn(w.ctx, w.cm)
//綁定到 wantConn
delivered := w.tryDeliver(pc, err)
if err == nil && (!delivered || pc.alt != nil) {
//綁定wantConn失敗
//放到存放空閑連接idleConn的map中
t.putOrCloseIdleConn(pc)
}
if err != nil {
t.decConnsPerHost(w.key)
}
}
- ? dialConnFor 先調(diào)用 dialConn 方法創(chuàng)建 TCP 連接
- ? 調(diào)用 tryDeliver 將連接綁定到 wantConn 上,綁定成功的話,就將該鏈接放到空閑連接的idleConn這個map中
- ? 綁定失敗的話會調(diào)用decConnsPerHost方法,用遞減密鑰的每主機連接計數(shù)方式,繼續(xù)異步調(diào)用Transport.dialConnFor
我們可以追蹤下代碼會發(fā)現(xiàn)Transport.tryPutIdleConn() 方法就是將persistConn添加到等待的空閑持久連接列表中的實現(xiàn)。
Transport.dialConn創(chuàng)建連接
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{}),
}
...
// 創(chuàng)建 tcp 連接,給pconn.conn
conn, err := t.dial(ctx, "tcp", cm.addr())
if err != nil {
return nil, wrapErr(err)
}
pconn.conn = conn
...
//開啟兩個goroutine處理讀寫
go pconn.readLoop()
go pconn.writeLoop()
return pconn, nil
}
?? 看完這個創(chuàng)建persistConn的代碼是不是心里仿佛懂了什么?
上述代碼中HTTP 連接的創(chuàng)建過程是建立 tcp 連接,然后為連接異步處理讀寫數(shù)據(jù),最后將創(chuàng)建好的連接返回。
我們可以看到創(chuàng)建的每個連接會分別創(chuàng)建兩個goroutine循環(huán)地進行進行讀寫的處理,這就是為什么我們連接能接受請求參數(shù)和處理請求的響應(yīng)的關(guān)鍵。
?? 這兩個協(xié)程功能是這樣的!
- 1. persisConn.writeLoop(),通過 persistConn.writech 通道讀取到客戶端提交的請求,將其發(fā)送到服務(wù)端
- 2. persisConn.readLoop(),讀取來自服務(wù)端的響應(yīng),并添加到 persistConn.reqCh 通道中,給persistConn.roundTrip 方法接收
想看這兩個協(xié)程
等待響應(yīng)
persistConn 連接本身創(chuàng)建了兩個讀寫goroutine,而這兩個goroutine就是通過兩個channel進行通信的。
這個通信就是在persistConn.roundTrip()方法中的進行傳遞交互的,其中writech 是用來寫入請求數(shù)據(jù),reqch是用來讀取響應(yīng)數(shù)據(jù)。
圖片
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
...
// 請求數(shù)據(jù)寫入到 writech channel中
pc.writech <- writeRequest{req, writeErrCh, continueCh}
// 接收響應(yīng)的channel
resc := make(chan responseAndError)
// 接收響應(yīng)的結(jié)構(gòu)體 requestAndChan 寫到 reqch channel中
pc.reqch <- requestAndChan{
req: req.Request,
cancelKey: req.cancelKey,
ch: resc,
...
}
...
for {
...
select {
// 接收到響應(yīng)數(shù)據(jù)
case re := <-resc:
...
// return響應(yīng)數(shù)據(jù)
return re.res, nil
...
}
}
1. 連接獲取到之后,會調(diào)用連接的 roundTrip 方法,將請求數(shù)據(jù)寫入到 persisConn.writech channel中,而連接 persisConn 中的協(xié)程 writeLoop() 接收到請求后就會處理請求
2. 響應(yīng)結(jié)構(gòu)體 requestAndChan 寫入到 persisConn.reqch 中
3. 通過readLoop 接受響應(yīng)數(shù)據(jù),然后讀取 resc channel 的響應(yīng)結(jié)果
4. 接受到響應(yīng)數(shù)據(jù)之后循環(huán)結(jié)束,連接處理完成
好了,net/http標準庫的客戶端構(gòu)造請求、發(fā)送請求、接受服務(wù)端的請求數(shù)據(jù)流程就講完了,看完之后是否意欲未盡呢?
還別說,小許也是第一次看是如何實現(xiàn)的,確實還是了解到了點東西呢!