Go 標準庫剖析 1(transport http 請求的承載者)

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

使用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和代理相關的我就忽略了, 兩個 mapidleConnidleConnChidleConn 是儲存從 connectMethodKey (代表著不同的協議 不同的host,也就是不同的請求)到 persistConn 的映射, idleConnCh 用來在並發http請求的時候在多個 goroutine 裡面相互發送持久串連,也就是說, 這些持久串連是可以重複利用的, 你的http請求用某個persistConn用完了,通過這個channel發送給其他http請求使用這個persistConn,然後我們找到transportRoundTrip方法

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裡面,主協程goroutineselect裡面監聽多個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 writeRequestrequestAndChan

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標準庫裡面的精彩之處,分享給大家。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.