使用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.Cancel
WaitResponse:
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標準庫裏面的精彩之處,分享給大家。