這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
golang中http協議實現
寫了一個爬蟲,發現出現了socket泄露的情況。百度了一下發現是缺少了Response.Body.Close(),所以導致串連
沒有被正常的關閉。也沒有被gc回收。下面是文檔中的說明
Callers should close resp.Body when done reading from it. If resp.Bodyis not closed, the Client's underlying RoundTripper (typically Transport)may not be able to re-use a persistent TCP connection to the server for asubsequent "keep-alive" request.
解決問題很簡單,不過引起了我想看看源碼中簡單的HTTP請求是如何?的慾望。
- 入口函數
- send函數
- Transport.RoundTrip函數
- Transport.altProto
- Transport.connectMethod
- Transport.getConn函數
- Transport.getIdleConn函數
- Transport.dialConn函數
- persistConn結構體
- persistConn.roundTrip函數
- Transport結構體中空閑串連
- Transport.dial函數
- persistConn.readLoop函數
Do函數(包括Post,Get)
首先我們用NewRequest構建了一個Request,裡麵包含了我們請求的url,如果是post請求還會包含請求的body,
隨後會觸發一個doFollowingRedirects函數,但是這裡我們為了簡化就不展開,直接看沒有重新導向的情況,也就是
通過Client.send函數繼續向下傳遞這個Request
send函數
Client.send函數是對send函數的一個封裝,目的是提取中Client cookie Jar 中的cookie放入Request中,以及
將Response中返回的cookie 裝進Client的cookie Jar。
func send(ireq *Request, rt RoundTripper, deadline time.Time) (*Response, error)
當Client.send調用send的時候會將Transport作為rt參數傳入進去,如果沒有的話則會用Transport.go裡面
預設的DefaultTransport.
隨後send做了一些微小的工作,檢測不完整的Request,setRequestCancel(如果設定了逾時時間Timeout則這個函數會生效,第一次讀的時候
會停止這個Timeout的計時,如果此時Request已經被Cancel了,那麼返回一個error)。
隨後調用rt的RoundTrip函數來獲得Response.
Transport.RoundTrip函數
首先檢測一下Request的資訊完整性,然後看一下altProto裡面有沒有符合Scheme的RoundTrip實現。隨後進入for迴圈,構建一個
connectMethod類型變數,隨後通過Transport.getConn來拿到一個TCP串連,再通過調用persistConn.roundTrip來把
Request寫入TCP中,完成發送請求。如果發送失敗,則調用checkTransportResend來嘗試重新發送這個Request.
Transport.altProto
最開始我也沒有看懂這是在幹嘛,後來找到了一個RegisterProtocol函數,才看明白這是在幹什麼。Transport作為一個可以複用的結構體實際上可以處理不同協議的請求,那麼不同協議的請求就要有不同的實現,諸如ftp,file等。如果出現了這種情況,我們就可以通過RegisterProtocol來註冊一些針對不同協議的實現,從而當Transport發送Request之前就可以通過map來確定到底要使用哪個RoundTrip。
Transport.connectMethod+
結構體中包括了Proxy 位址,協議(HTTP or HTTPS),以及目的地址。需要注意的是,connectMethod類型是很關鍵的,
它不僅是Transport中一些map的索引值,也是很多函數的參數。與其相似的結構體connectMethodKey中包含了和它一樣的內容,只不過結構體
內變數的類型不同(connectMethodKey中的proxy是string,而connectMethod中的proxy是*url.URL)
Transport.getConn函數
首先通過getIdleConn函數來擷取可用的空閑串連,如果有的話,直接返回。如果沒有的話,用go(非同步)的方式建立一個dialConn,然後通過
channel來將其送回getConn函數中。而在getConn中則是用select阻塞,等待返回。整個函數中比較複雜的機制在於情況的判定,譬如請求逾時了
connection仍然沒有返回,這個時候函數會調用handlePendingDial對connection進行處理,放入idle隊列或者將其關閉。又或者是當我們請求的
connection沒有返回而此時出現了一個閒置connection,調用handlePendingDial等待我們申請的那個connection,將這個閒置返回。
Transport.getIdleConn函數
關於空閑串連的在Transport中的兩個map,搜尋idleConn,如果存在多個則返回第一個,沒有則返回nil
Transport.dialConn函數
首先建立一個persistConn類型的變數,然後檢測Scheme,如果是TLS,HTTPS或者是使用了代理,那麼通過DialTLS函數來建立
Conn,在這裡我們不解釋這個過程。如果是普通的HTTP,則通過Transport.dial來獲得這個Conn.我們只看HTTP的處理過程,發現直接
跳過了函數裡面的80行+.隨後建立了persistConn的讀寫緩衝區放入結構體中。以非同步方式開啟persistConn的讀寫函數(readLoop和writeLoop)
persistConn
注釋裡已經寫的非常全面了,我就做個搬運工.
// persistConn wraps a connection, usually a persistent one// (but may be used for non-keep-alive requests as well)type persistConn struct { // alt optionally specifies the TLS NextProto RoundTripper. // This is used for HTTP/2 today and future protocol laters. // If it's non-nil, the rest of the fields are unused. alt RoundTripper t *Transport cacheKey connectMethodKey conn net.Conn tlsState *tls.ConnectionState br *bufio.Reader // from conn sawEOF bool // whether we've seen EOF from conn; owned by readLoop bw *bufio.Writer // to conn reqch chan requestAndChan // written by roundTrip; read by readLoop writech chan writeRequest // written by roundTrip; read by writeLoop closech chan struct{} // closed when conn closed isProxy bool // writeErrCh passes the request write error (usually nil) // from the writeLoop goroutine to the readLoop which passes // it off to the res.Body reader, which then uses it to decide // whether or not a connection can be reused. Issue 7569. writeErrCh chan error lk sync.Mutex // guards following fields numExpectedResponses int closed error // set non-nil when conn is closed, before closech is 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 reused bool // whether conn has had successful request/response and is being reused. // 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)}
persistConn.roundTrip函數
首先調用replaceReqCanceler來探測Request是否已經觸發了刪除行為,如果是,就把persistConn放入putOrCloseIdleConn中處理。
實際上,go在實現HTTP請求的時候是有一個預設的Header,而在Request裡面也實現了一個extraHeaders的方法。也就是說,在這一步的
時候HTTP Header才會真正的被完善。包括Accept-Encoding(gzip),Range,Connection(close).隨後向writech裡面寫入Request,
在persistConn結構體中已經講過,writech的接收者是writeloop,writeloop接收到了之後就會將其寫入緩衝區並調用Flush,將err通過
channel返回。接下來roundTrip向reqch中寫入requestAndChan,reqch的接受者是readloop,接下來函數select掛起幾個管道,
用來監聽一些寫入錯誤,服務逾時,串連關閉(或被刪除),以及readloop傳送回來的response.檢查傳回值沒有問題之後將response返回。
Transport結構體中空閑串連部分
idleConn map[connectMethodKey][]*persistConnidleConnCh map[connectMethodKey]chan *persistConn
第一個idleConn是以MethodKey作為索引值的,為一個persistConn切片建立索引,可以想象的是倘若我們設定最大空閑串連為5(perhost),
那麼我們可以通過MethodKey獲得的最大空閑串連應該就是5個。
idleConnCh是對傳送persistConn的管道建立索引,每次有人等待串連的時候都會建立一個這樣管道。調用tryPutIdleConn的時候
會嘗試著將已經收到的空閑串連放入管道內,如果放入成功則返回,放入失敗則在idleConnCh刪除這個索引。然後將其放入idleConn中。
Transport.dial函數
dial函數是調用的Transport結構體中的Dial func(network, addr string) (net.Conn, error).如果你沒有建立這個函數的話,
預設的就是net.Dial函數。也就是調用底層函數了。
persistConn.readLoop函數
首先用defer註冊一個close函數,用來關閉conn以及關閉persistConn中的closech以通知conn被關閉。然後進入迴圈,
首先用Peek(1)來探測是否發生了IO錯誤。在persistConn.reqch管道中讀出requestAndChan類型變數,這個變數是用來匹配Request,
並且傳入幾個管道作為通訊。隨後調用persistConn.readResponse()來讀出Response。後面做一些容錯性的檢查以及ResponseBody
的訊息管道,最後用select掛起,等到persistConn的關閉或者Request的cancel,又或者是body的關閉,這個時候才會觸發退出迴圈
或者繼續迴圈的指令。那麼最初因為沒有寫Response.Body.Close()所導致的問題就出在這裡了。
persistConn.readResponse的實現;
ReadResponse的實現;
總結
第一次看源碼去解決問題,問題很快就得到解決了。這就正說明了絕大部分問題在源碼中都有說明和注釋。實話實說,我看的蠻吃力的,
自己寫了一圈下來發現自己寫的內容對讀者並不是特別友好,更多的是對源碼的一種簡化版翻譯。水平較低難免出錯,期盼如果有大神
看到可以指出我的錯誤,也歡迎問題的交(gao)流(ji)