這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
使用golang net/http庫發送http請求,最後都是調用 transport的 RoundTrip方法
type RoundTripper interface { RoundTrip(*Request) (*Response, error)}
RoundTrip executes a single HTTP transaction, returning the Response for the request req.
(RoundTrip 代表一個http事務,給一個請求返回一個響應)
說白了,就是你給它一個request,它給你一個response
下面我們來看一下他的實現,對應源檔案net/http/transport.go
,我感覺這裡是http package裡面的精髓所在,go裡面一個struct就跟一個類一樣,transport這個類長這樣的
type Transport struct { idleMu sync.Mutex wantIdle bool // user has requested to close all idle conns idleConn map[connectMethodKey][]*persistConn idleConnCh map[connectMethodKey]chan *persistConn reqMu sync.Mutex reqCanceler map[*Request]func() altMu sync.RWMutex altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper //Dial擷取一個tcp 串連,也就是net.Conn結構,你就記住可以往裡面寫request //然後從裡面搞到response就行了 Dial func(network, addr string) (net.Conn, error)}
篇幅所限, https和代理相關的我就忽略了, 兩個 map
為 idleConn
、idleConnCh
,idleConn
是儲存從 connectMethodKey (代表著不同的協議 不同的host,也就是不同的請求)到 persistConn 的映射, idleConnCh
用來在並發http請求的時候在多個 goroutine 裡面相互發送持久串連,也就是說, 這些持久串連是可以重複利用的, 你的http請求用某個persistConn
用完了,通過這個channel
發送給其他http請求使用這個persistConn
,然後我們找到transport
的RoundTrip
方法
func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { ... pconn, err := t.getConn(req, cm) if err != nil { t.setReqCanceler(req, nil) req.closeBody() return nil, err } return pconn.roundTrip(treq)}
前面對輸入的錯誤處理部分我們忽略, 其實就2步,先擷取一個TCP長串連,所謂TCP長串連就是三向交握建立串連後不close
而是一直保持重複使用(節約環保) 然後調用這個持久串連persistConn 這個struct的roundTrip方法
我們跟蹤第一步
func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) { if pc := t.getIdleConn(cm); pc != nil { // 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() {}) return pc, nil } type dialRes struct { pc *persistConn err error } dialc := make(chan dialRes) //定義了一個發送 persistConn的channel prePendingDial := prePendingDial postPendingDial := postPendingDial handlePendingDial := func() { if prePendingDial != nil { prePendingDial() } go func() { if v := <-dialc; v.err == nil { t.putIdleConn(v.pc) } if postPendingDial != nil { postPendingDial() } }() } cancelc := make(chan struct{}) t.setReqCanceler(req, func() { close(cancelc) }) // 啟動了一個goroutine, 這個goroutine 擷取裡面調用dialConn搞到 // persistConn, 然後發送到上面建立的channel dialc裡面, go func() { pc, err := t.dialConn(cm) dialc <- dialRes{pc, err} }() idleConnCh := t.getIdleConnCh(cm) select { case v := <-dialc: // dialc 我們的 dial 方法先搞到通過 dialc通道發過來了 return v.pc, v.err case pc := <-idleConnCh: // 這裡代表其他的http請求用完了歸還的persistConn通過idleConnCh這個 // channel發送來的 handlePendingDial() return pc, nil case <-req.Cancel: handlePendingDial() return nil, errors.New("net/http: request canceled while waiting for connection") case <-cancelc: handlePendingDial() return nil, errors.New("net/http: request canceled while waiting for connection") }}
這裡面的代碼寫的很有講究 , 上面代碼裡面我也注釋了, 定義了一個發送 persistConn
的channel dialc
, 啟動了一個goroutine
, 這個goroutine
擷取裡面調用dialConn
搞到persistConn
, 然後發送到dialc
裡面,主協程goroutine
在 select
裡面監聽多個channel
,看看哪個通道裡面先發過來 persistConn
,就用哪個,然後return
。
這裡要注意的是 idleConnCh
這個通道裡面發送來的是其他的http請求用完了歸還的persistConn
, 如果從這個通道裡面搞到了,dialc
這個通道也等著發呢,不能浪費,就通過handlePendingDial
這個方法把dialc
通道裡面的persistConn
也發到idleConnCh
,等待後續給其他http請求使用。
還有就是,讀者可以翻一下代碼,每個建立的persistConn的時候都把tcp串連裡地輸入資料流,和輸出資料流用br(br *bufio.Reader
),和bw(bw *bufio.Writer
)封裝了一下,往bw寫就寫到tcp輸入資料流裡面了,讀輸出資料流也是通過br讀,並啟動了讀迴圈和寫迴圈
pconn.br = bufio.NewReader(noteEOFReader{pconn.conn, &pconn.sawEOF})pconn.bw = bufio.NewWriter(pconn.conn)go pconn.readLoop()go pconn.writeLoop()
我們跟蹤第二步pconn.roundTrip
調用這個持久串連persistConn 這個struct的roundTrip
方法。
先瞄一下 persistConn
這個struct
type persistConn struct { t *Transport cacheKey connectMethodKey conn net.Conn tlsState *tls.ConnectionState br *bufio.Reader // 從tcp輸出資料流裡面讀 sawEOF bool // whether we've seen EOF from conn; owned by readLoop bw *bufio.Writer // 寫到tcp輸入資料流 reqch chan requestAndChan // 主goroutine 往channnel裡面寫,讀迴圈從 // channnel裡面接受 writech chan writeRequest // 主goroutine 往channnel裡面寫 // 寫迴圈從channel裡面接受 closech chan struct{} // 通知關閉tcp串連的channel writeErrCh chan error lk sync.Mutex // guards following fields numExpectedResponses int closed bool // whether conn has been closed broken bool // an error has happened on this connection; marked broken so it's not reused. canceled bool // whether this conn was broken due a CancelRequest // mutateHeaderFunc is an optional func to modify extra // headers on each outbound request before it's written. (the // original Request given to RoundTrip is not modified) mutateHeaderFunc func(Header)}
裡面是各種channel, 用的是出神入化, 各位要好好理解一下, 我這裡畫一下
這裡有三個goroutine,分別用三個圓圈表示, channel用箭頭表示
有兩個channel writeRequest
和 requestAndChan
type writeRequest struct { req *transportRequest ch chan<- error}
主goroutine 往writeRequest裡面寫,寫迴圈從writeRequest裡面接受
type responseAndError struct { res *Response err error}type requestAndChan struct { req *Request ch chan responseAndError addedGzip bool}
主goroutine 往requestAndChan裡面寫,讀迴圈從requestAndChan裡面接受。
注意這裡的channel都是雙向channel,也就是channel 的struct裡面有一個chan類型的欄位, 比如 reqch chan requestAndChan
這裡的 requestAndChan 裡面的 ch chan responseAndError
。
這個是很牛叉,主 goroutine 通過 reqch 發送requestAndChan 給讀迴圈,然後讀迴圈搞到response後通過 requestAndChan 裡面的通道responseAndError把response返給主goroutine,所以我畫了一個雙向箭頭。
我們研究一下代碼,我理解下來其實就是三個goroutine通過channel互相協作的過程。
主迴圈:
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) { ... 忽略 // Write the request concurrently with waiting for a response, // in case the server decides to reply before reading our full // request body. writeErrCh := make(chan error, 1) pc.writech <- writeRequest{req, writeErrCh} //把request發送給寫迴圈 resc := make(chan responseAndError, 1) pc.reqch <- requestAndChan{req.Request, resc, requestedGzip} //發送給讀迴圈 var re responseAndError var respHeaderTimer <-chan time.Time cancelChan := req.Request.CancelWaitResponse: for { select { case err := <-writeErrCh: if isNetWriteError(err) { //寫迴圈通過這個channel報告錯誤 select { case re = <-resc: pc.close() break WaitResponse case <-time.After(50 * time.Millisecond): // Fall through. } } if err != nil { re = responseAndError{nil, err} pc.close() break WaitResponse } if d := pc.t.ResponseHeaderTimeout; d > 0 { timer := time.NewTimer(d) defer timer.Stop() // prevent leaks respHeaderTimer = timer.C } case <-pc.closech: // 如果長串連掛了, 這裡的channel有資料, 進入這個case, 進行處理 select { case re = <-resc: if fn := testHookPersistConnClosedGotRes; fn != nil { fn() } default: re = responseAndError{err: errClosed} if pc.isCanceled() { re = responseAndError{err: errRequestCanceled} } } break WaitResponse case <-respHeaderTimer: pc.close() re = responseAndError{err: errTimeout} break WaitResponse // 如果timeout,這裡的channel有資料, break掉for迴圈 case re = <-resc: break WaitResponse // 擷取到讀迴圈的response, break掉 for迴圈 case <-cancelChan: pc.t.CancelRequest(req.Request) cancelChan = nil } } if re.err != nil { pc.t.setReqCanceler(req.Request, nil) } return re.res, re.err}
這段代碼主要就幹了三件事
主goroutine ->requestAndChan -> 讀迴圈goroutine
主goroutine ->writeRequest-> 寫迴圈goroutine
主goroutine 通過select 監聽各個channel上的資料, 比如請求取消, timeout,長串連掛了,寫流出錯,讀流出錯, 都是其他goroutine 發送過來的, 跟中斷一樣,然後相應處理,上面也提到了,有些channel是主goroutine通過channel發送給其他goroutine的struct裡麵包含的channel, 比如 case err := <-writeErrCh:
case re = <-resc:
讀迴圈代碼:
func (pc *persistConn) readLoop() { ... 忽略 alive := true for alive { ... 忽略 rc := <-pc.reqch var resp *Response if err == nil { resp, err = ReadResponse(pc.br, rc.req) if err == nil && resp.StatusCode == 100 { //100 Continue 初始的請求已經接受,客戶應當繼續發送請求的其 // 餘部分 resp, err = ReadResponse(pc.br, rc.req) // 讀pc.br(tcp輸出資料流)中的資料,這裡的代碼在response裡面 //解析statusCode,頭欄位, 轉成標準的記憶體中的response 類型 // http在tcp資料流裡面,head和body以 /r/n/r/n分開, 各個頭 // 欄位 以/r/n分開 } } if resp != nil { resp.TLS = pc.tlsState } ...忽略 //上面處理一些http協議的一些邏輯行為, rc.ch <- responseAndError{resp, err} //把讀到的response返回給 //主goroutine .. 忽略 //忽略部分, 處理cancel req中斷, 發送idleConnCh歸還pc(持久串連)到持久串連池中(map) pc.close()}
無關代碼忽略,這段代碼主要幹了一件事情
讀迴圈goroutine 通過channel requestAndChan 接受主goroutine發送的request(rc := <-pc.reqch
), 並從tcp輸出資料流中讀取response, 然後還原序列化到結構體中, 最後通過channel 返給主goroutine (rc.ch <- responseAndError{resp, err}
)
func (pc *persistConn) writeLoop() { for { select { case wr := <-pc.writech: //接受主goroutine的 request if pc.isBroken() { wr.ch <- errors.New("http: can't write HTTP request on broken connection") continue } err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra) //寫入tcp輸入資料流 if err == nil { err = pc.bw.Flush() } if err != nil { pc.markBroken() wr.req.Request.closeBody() } pc.writeErrCh <- err wr.ch <- err // 出錯的時候返給主goroutineto case <-pc.closech: return } }}
寫迴圈就更簡單了,select channel中主gouroutine的request,然後寫入tcp輸入資料流,如果出錯了,channel 通知調用者。
整體看下來,過程都很簡單,但是代碼中有很多值得我們學習的地方,比如高並發請求如何複用tcp串連,這裡是串連池的做法,如果使用多個 goroutine相互協作完成一個http請求,出現錯誤的時候如何通知調用者中斷錯誤,代碼風格也有很多可以借鑒的地方。
我打算寫一個系列,全面剖析go標準庫裡面的精彩之處,分享給大家。