golang中http協議實現

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.