1. Go發(fā)起HTTP2.0請(qǐng)求流程分析(前篇)

        共 5253字,需瀏覽 11分鐘

         ·

        2022-04-14 12:20

        點(diǎn)擊上方“Go語(yǔ)言進(jìn)階學(xué)習(xí)”,進(jìn)行關(guān)注

        回復(fù)“Go語(yǔ)言”即可獲贈(zèng)從入門(mén)到進(jìn)階共10本電子書(shū)

        遠(yuǎn)芳侵古道,晴翠接荒城。

        前言

        Go中的HTTP請(qǐng)求之——HTTP1.1請(qǐng)求流程分析之后,中間斷斷續(xù)續(xù),歷時(shí)近一月,終于才敢開(kāi)始碼字寫(xiě)下本文。

        閱讀建議

        HTTP2.0在建立TCP連接和安全的TLS傳輸通道與HTTP1.1的流程基本一致。所以筆者建議沒(méi)有看過(guò)Go中的HTTP請(qǐng)求之——HTTP1.1請(qǐng)求流程分析這篇文章的先去補(bǔ)一下課,本文會(huì)基于前一篇文章僅介紹和HTTP2.0相關(guān)的邏輯。

        (*Transport).roundTrip

        (*Transport).roundTrip方法會(huì)調(diào)用t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)初始化TLSClientConfig以及h2transport,而這兩者都和HTTP2.0有著緊密的聯(lián)系。

        TLSClientConfig: 初始化client支持的http協(xié)議, 并在tls握手時(shí)告知server。

        h2transport: 如果本次請(qǐng)求是http2,那么h2transport會(huì)接管連接,請(qǐng)求和響應(yīng)的處理邏輯。

        下面看看源碼:

        func (t *Transport) onceSetNextProtoDefaults() {
        // ...此處省略代碼...
        t2, err := http2configureTransport(t)
        if err != nil {
        log.Printf("Error enabling Transport HTTP/2 support: %v", err)
        return
        }
        t.h2transport = t2

        // ...此處省略代碼...
        }
        func http2configureTransport(t1 *Transport) (*http2Transport, error) {
        connPool := new(http2clientConnPool)
        t2 := &http2Transport{
        ConnPool: http2noDialClientConnPool{connPool},
        t1: t1,
        }
        connPool.t = t2
        if err := http2registerHTTPSProtocol(t1, http2noDialH2RoundTripper{t2}); err != nil {
        return nil, err
        }
        if t1.TLSClientConfig == nil {
        t1.TLSClientConfig = new(tls.Config)
        }
        if !http2strSliceContains(t1.TLSClientConfig.NextProtos, "h2") {
        t1.TLSClientConfig.NextProtos = append([]string{"h2"}, t1.TLSClientConfig.NextProtos...)
        }
        if !http2strSliceContains(t1.TLSClientConfig.NextProtos, "http/1.1") {
        t1.TLSClientConfig.NextProtos = append(t1.TLSClientConfig.NextProtos, "http/1.1")
        }
        upgradeFn := func(authority string, c *tls.Conn) RoundTripper {
        addr := http2authorityAddr("https", authority)
        if used, err := connPool.addConnIfNeeded(addr, t2, c); err != nil {
        go c.Close()
        return http2erringRoundTripper{err}
        } else if !used {
        // Turns out we don't need this c.
        // For example, two goroutines made requests to the same host
        // at the same time, both kicking off TCP dials. (since protocol
        // was unknown)
        go c.Close()
        }
        return t2
        }
        if m := t1.TLSNextProto; len(m) == 0 {
        t1.TLSNextProto = map[string]func(string, *tls.Conn) RoundTripper{
        "h2": upgradeFn,
        }
        } else {
        m["h2"] = upgradeFn
        }
        return t2, nil
        }

        筆者將上述的源碼簡(jiǎn)單拆解為以下幾個(gè)步驟:

        1. 新建一個(gè)http2clientConnPool并復(fù)制給t2,以后http2的請(qǐng)求會(huì)優(yōu)先從該連接池中獲取連接。

        2. 初始化TLSClientConfig,并將支持的h2http1.1協(xié)議添加到TLSClientConfig.NextProtos中。

        3. 定義一個(gè)h2upgradeFn存儲(chǔ)到t1.TLSNextProto里。

        鑒于前一篇文章對(duì)新建連接前的步驟有了較為詳細(xì)的介紹,所以這里直接看和server建立連接的部分源碼,即(*Transport).dialConn方法:

        func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
        // ...此處省略代碼...
        if cm.scheme() == "https" && t.hasCustomTLSDialer() {
        // ...此處省略代碼...
        } else {
        conn, err := t.dial(ctx, "tcp", cm.addr())
        if err != nil {
        return nil, wrapErr(err)
        }
        pconn.conn = conn
        if cm.scheme() == "https" {
        var firstTLSHost string
        if firstTLSHost, _, err = net.SplitHostPort(cm.addr()); err != nil {
        return nil, wrapErr(err)
        }
        if err = pconn.addTLS(firstTLSHost, trace); err != nil {
        return nil, wrapErr(err)
        }
        }
        }

        // Proxy setup.
        // ...此處省略代碼...

        if s := pconn.tlsState; s != nil && s.NegotiatedProtocolIsMutual && s.NegotiatedProtocol != "" {
        if next, ok := t.TLSNextProto[s.NegotiatedProtocol]; ok {
        return &persistConn{t: t, cacheKey: pconn.cacheKey, alt: next(cm.targetAddr, pconn.conn.(*tls.Conn))}, nil
        }
        }

        // ...此處省略代碼...
        }

        筆者對(duì)上述的源碼描述如下:

        1. 調(diào)用t.dial(ctx, "tcp", cm.addr())創(chuàng)建TCP連接。

        2. 如果是https的請(qǐng)求, 則對(duì)請(qǐng)求建立安全的tls傳輸通道。

        3. 檢查tls的握手狀態(tài),如果和server協(xié)商的NegotiatedProtocol協(xié)議不為空,且client的t.TLSNextProto有該協(xié)議,則返回alt不為空的持久連接(HTTP1.1不會(huì)進(jìn)入if條件里)。

        筆者對(duì)上述的第三點(diǎn)進(jìn)行展開(kāi)。經(jīng)筆者在本地debug驗(yàn)證,當(dāng)client和server都支持http2時(shí),s.NegotiatedProtocol的值為h2s.NegotiatedProtocolIsMutual的值為true。

        在上面分析http2configureTransport函數(shù)時(shí),我們知道TLSNextProto注冊(cè)了一個(gè)key為h2的函數(shù),所以調(diào)用next實(shí)際就是調(diào)用前面的upgradeFn函數(shù)。

        upgradeFn會(huì)調(diào)用connPool.addConnIfNeeded向http2的連接池添加一個(gè)tls傳輸通道,并最終返回前面已經(jīng)創(chuàng)建好的t2http2Transport。

        func (p *http2clientConnPool) addConnIfNeeded(key string, t *http2Transport, c *tls.Conn) (used bool, err error) {
        p.mu.Lock()
        // ...此處省略代碼...
        // 主要用于判斷是否有必要像連接池添加新的連接
        // 判斷連接池中是否已有同host連接,如果有且該鏈接能夠處理新的請(qǐng)求則直接返回
        call, dup := p.addConnCalls[key]
        if !dup {
        // ...此處省略代碼...
        call = &http2addConnCall{
        p: p,
        done: make(chan struct{}),
        }
        p.addConnCalls[key] = call
        go call.run(t, key, c)
        }
        p.mu.Unlock()

        <-call.done
        if call.err != nil {
        return false, call.err
        }
        return !dup, nil
        }
        func (c *http2addConnCall) run(t *http2Transport, key string, tc *tls.Conn) {
        cc, err := t.NewClientConn(tc)

        p := c.p
        p.mu.Lock()
        if err != nil {
        c.err = err
        } else {
        p.addConnLocked(key, cc)
        }
        delete(p.addConnCalls, key)
        p.mu.Unlock()
        close(c.done)
        }

        分析上述的源碼我們能夠得到兩點(diǎn)結(jié)論:

        1. 執(zhí)行完upgradeFn之后,(*Transport).dialConn返回的持久化連接中alt字段已經(jīng)不是nil了。

        2. t.NewClientConn(tc)新建出來(lái)的連接會(huì)保存在http2的連接池即http2clientConnPool中,下一小結(jié)將對(duì)NewClientConn展開(kāi)分析。

        最后我們回到(*Transport).roundTrip方法并分析其中的關(guān)鍵源碼:

        func (t *Transport) roundTrip(req *Request) (*Response, error) {
        t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
        // ...此處省略代碼...
        for {
        select {
        case <-ctx.Done():
        req.closeBody()
        return nil, ctx.Err()
        default:
        }

        // ...此處省略代碼...
        pconn, err := t.getConn(treq, cm)
        if err != nil {
        t.setReqCanceler(req, nil)
        req.closeBody()
        return nil, err
        }

        var resp *Response
        if pconn.alt != nil {
        // HTTP/2 path.
        t.setReqCanceler(req, nil) // not cancelable with CancelRequest
        resp, err = pconn.alt.RoundTrip(req)
        } else {
        resp, err = pconn.roundTrip(treq)
        }
        if err == nil {
        return resp, nil
        }

        // ...此處省略代碼...
        }
        }

        結(jié)合前面的分析,pconn.alt在server和client都支持http2協(xié)議的情況下是不為nil的。所以,http2的請(qǐng)求會(huì)走pconn.alt.RoundTrip(req)分支,也就是說(shuō)http2的請(qǐng)求流程就被http2Transport接管啦。

        (*http2Transport).NewClientConn

        (*http2Transport).NewClientConn內(nèi)部會(huì)調(diào)用t.newClientConn(c, t.disableKeepAlives())。

        因?yàn)楸竟?jié)內(nèi)容較多,所以筆者不再一次性貼出源碼,而是按關(guān)鍵步驟分析并分塊兒貼出源碼。

        1、初始化一個(gè)http2ClientConn

        cc := &http2ClientConn{
        t: t,
        tconn: c,
        readerDone: make(chan struct{}),
        nextStreamID: 1,
        maxFrameSize: 16 << 10, // spec default
        initialWindowSize: 65535, // spec default
        maxConcurrentStreams: 1000, // "infinite", per spec. 1000 seems good enough.
        peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
        streams: make(map[uint32]*http2clientStream),
        singleUse: singleUse,
        wantSettingsAck: true,
        pings: make(map[[8]byte]chan struct{}),
        }

        上面的源碼新建了一個(gè)默認(rèn)的http2ClientConn。

        initialWindowSize:初始化窗口大小為65535,這個(gè)值之后會(huì)初始化每一個(gè)數(shù)據(jù)流可發(fā)送的數(shù)據(jù)窗口大小。

        maxConcurrentStreams:表示每個(gè)連接上允許最多有多少個(gè)數(shù)據(jù)流同時(shí)傳輸數(shù)據(jù)。

        streams:當(dāng)前連接上的數(shù)據(jù)流。

        singleUse: 控制http2的連接是否允許多個(gè)數(shù)據(jù)流共享,其值由t.disableKeepAlives()控制。

        2、創(chuàng)建一個(gè)條件鎖并且新建Writer&Reader。

        cc.cond = sync.NewCond(&cc.mu)
        cc.flow.add(int32(http2initialWindowSize))
        cc.bw = bufio.NewWriter(http2stickyErrWriter{c, &cc.werr})
        cc.br = bufio.NewReader(c)

        新建Writer&Reader沒(méi)什么好說(shuō)的,需要注意的是cc.flow.add(int32(http2initialWindowSize))

        cc.flow.add將當(dāng)前連接的可寫(xiě)流控制窗口大小設(shè)置為http2initialWindowSize,即65535。

        3、新建一個(gè)讀寫(xiě)數(shù)據(jù)幀的Framer。

        cc.fr = http2NewFramer(cc.bw, cc.br)
        cc.fr.ReadMetaHeaders = hpack.NewDecoder(http2initialHeaderTableSize, nil)
        cc.fr.MaxHeaderListSize = t.maxHeaderListSize()

        4、向server發(fā)送開(kāi)場(chǎng)白,并發(fā)送一些初始化數(shù)據(jù)幀。

        initialSettings := []http2Setting{
        {ID: http2SettingEnablePush, Val: 0},
        {ID: http2SettingInitialWindowSize, Val: http2transportDefaultStreamFlow},
        }
        if max := t.maxHeaderListSize(); max != 0 {
        initialSettings = append(initialSettings, http2Setting{ID: http2SettingMaxHeaderListSize, Val: max})
        }

        cc.bw.Write(http2clientPreface)
        cc.fr.WriteSettings(initialSettings...)
        cc.fr.WriteWindowUpdate(0, http2transportDefaultConnFlow)
        cc.inflow.add(http2transportDefaultConnFlow + http2initialWindowSize)
        cc.bw.Flush()

        client向server發(fā)送的開(kāi)場(chǎng)白內(nèi)容如下:

        const (
        // client首先想server發(fā)送以PRI開(kāi)頭的一串字符串。
        http2ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
        )
        var (
        http2clientPreface = []byte(http2ClientPreface)
        )

        發(fā)送完開(kāi)場(chǎng)白后,client向server發(fā)送SETTINGS數(shù)據(jù)幀。

        http2SettingEnablePush: 告知server客戶(hù)端是否開(kāi)啟push功能。

        http2SettingInitialWindowSize:告知server客戶(hù)端可接受的最大數(shù)據(jù)窗口是http2transportDefaultStreamFlow(4M)。

        發(fā)送完SETTINGS數(shù)據(jù)幀后,發(fā)送WINDOW_UPDATE數(shù)據(jù)幀, 因?yàn)榈谝粋€(gè)參數(shù)為0即streamID為0,則是告知server此連接可接受的最大數(shù)據(jù)窗口為http2transportDefaultConnFlow(1G)。

        發(fā)送完WINDOW_UPDATE數(shù)據(jù)幀后,將client的可讀流控制窗口大小設(shè)置為http2transportDefaultConnFlow + http2initialWindowSize

        5、開(kāi)啟讀循環(huán)并返回

        go cc.readLoop()

        (*http2Transport).RoundTrip

        (*http2Transport).RoundTrip只是一個(gè)入口函數(shù),它會(huì)調(diào)用(*http2Transport). RoundTripOpt方法。

        (*http2Transport). RoundTripOpt有兩個(gè)步驟比較關(guān)鍵:

        t.connPool().GetClientConn(req, addr): 在http2的連接池里面獲取一個(gè)可用連接,其中連接池的類(lèi)型為http2noDialClientConnPool,參考http2configureTransport函數(shù)。

        cc.roundTrip(req): 通過(guò)獲取到的可用連接發(fā)送請(qǐng)求并返回響應(yīng)。

        (http2noDialClientConnPool).GetClientConn

        根據(jù)實(shí)際的debug結(jié)果(http2noDialClientConnPool).GetClientConn最終會(huì)調(diào)用(*http2clientConnPool).getClientConn(req *Request, addr string, dialOnMiss bool)。

        通過(guò)(http2noDialClientConnPool).GetClientConn獲取連接時(shí)傳遞給(*http2clientConnPool).getClientConn方法的第三個(gè)參數(shù)始終為false,該參數(shù)為false時(shí)代表著即使無(wú)法正常獲取可用連接,也不在這個(gè)環(huán)節(jié)重新發(fā)起撥號(hào)流程。

        在(*http2clientConnPool).getClientConn中會(huì)遍歷同地址的連接,并判斷連接的狀態(tài)從而獲取一個(gè)可以處理請(qǐng)求的連接。

        for _, cc := range p.conns[addr] {
        if st := cc.idleState(); st.canTakeNewRequest {
        if p.shouldTraceGetConn(st) {
        http2traceGetConn(req, addr)
        }
        p.mu.Unlock()
        return cc, nil
        }
        }

        cc.idleState()判斷當(dāng)前連接池中的連接能否處理新的請(qǐng)求:

        1、當(dāng)前連接是否能被多個(gè)請(qǐng)求共享,如果僅單個(gè)請(qǐng)求使用且已經(jīng)有一個(gè)數(shù)據(jù)流,則當(dāng)前連接不能處理新的請(qǐng)求。

        if cc.singleUse && cc.nextStreamID > 1 {
        return
        }

        2、以下幾點(diǎn)均為true時(shí),才代表當(dāng)前連接能夠處理新的請(qǐng)求:

        • 連接狀態(tài)正常,即未關(guān)閉并且不處于正在關(guān)閉的狀態(tài)。

        • 當(dāng)前連接正在處理的數(shù)據(jù)流小于maxConcurrentStreams。

        • 下一個(gè)要處理的數(shù)據(jù)流 + 當(dāng)前連接處于等待狀態(tài)的請(qǐng)求*2 < math.MaxInt32。

        • 當(dāng)前連接沒(méi)有長(zhǎng)時(shí)間處于空閑狀態(tài)(主要通過(guò)cc.tooIdleLocked()判斷)。

        st.canTakeNewRequest = cc.goAway == nil && !cc.closed && !cc.closing && maxConcurrentOkay &&
        int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 &&
        !cc.tooIdleLocked()

        當(dāng)從鏈接池成功獲取到一個(gè)可以處理請(qǐng)求的連接,就可以和server進(jìn)行數(shù)據(jù)交互,即(*http2ClientConn).roundTrip流程。

        (*http2ClientConn).roundTrip

        1、在真正開(kāi)始處理請(qǐng)求前,還要進(jìn)行header檢查,http2對(duì)http1.1的某些header是不支持的,筆者就不對(duì)這個(gè)邏輯進(jìn)行分析了,直接上源碼:

        func http2checkConnHeaders(req *Request) error {
        if v := req.Header.Get("Upgrade"); v != "" {
        return fmt.Errorf("http2: invalid Upgrade request header: %q", req.Header["Upgrade"])
        }
        if vv := req.Header["Transfer-Encoding"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && vv[0] != "chunked") {
        return fmt.Errorf("http2: invalid Transfer-Encoding request header: %q", vv)
        }
        if vv := req.Header["Connection"]; len(vv) > 0 && (len(vv) > 1 || vv[0] != "" && !strings.EqualFold(vv[0], "close") && !strings.EqualFold(vv[0], "keep-alive")) {
        return fmt.Errorf("http2: invalid Connection request header: %q", vv)
        }
        return nil
        }
        func http2commaSeparatedTrailers(req *Request) (string, error) {
        keys := make([]string, 0, len(req.Trailer))
        for k := range req.Trailer {
        k = CanonicalHeaderKey(k)
        switch k {
        case "Transfer-Encoding", "Trailer", "Content-Length":
        return "", &http2badStringError{"invalid Trailer key", k}
        }
        keys = append(keys, k)
        }
        if len(keys) > 0 {
        sort.Strings(keys)
        return strings.Join(keys, ","), nil
        }
        return "", nil
        }

        2、調(diào)用(*http2ClientConn).awaitOpenSlotForRequest,一直等到當(dāng)前連接處理的數(shù)據(jù)流小于maxConcurrentStreams, 如果此函數(shù)返回錯(cuò)誤,則本次請(qǐng)求失敗。

        2.1、double check當(dāng)前連接可用。

        if cc.closed || !cc.canTakeNewRequestLocked() {
        if waitingForConn != nil {
        close(waitingForConn)
        }
        return http2errClientConnUnusable
        }

        2.2、如果當(dāng)前連接處理的數(shù)據(jù)流小于maxConcurrentStreams則直接返回nil。筆者相信大部分邏輯走到這兒就返回了。

        if int64(len(cc.streams))+1 <= int64(cc.maxConcurrentStreams) {
        if waitingForConn != nil {
        close(waitingForConn)
        }
        return nil
        }

        2.3、如果當(dāng)前連接處理的數(shù)據(jù)流確實(shí)已經(jīng)達(dá)到上限,則開(kāi)始進(jìn)入等待流程。

        if waitingForConn == nil {
        waitingForConn = make(chan struct{})
        go func() {
        if err := http2awaitRequestCancel(req, waitingForConn); err != nil {
        cc.mu.Lock()
        waitingForConnErr = err
        cc.cond.Broadcast()
        cc.mu.Unlock()
        }
        }()
        }
        cc.pendingRequests++
        cc.cond.Wait()
        cc.pendingRequests--

        通過(guò)上面的邏輯知道,當(dāng)前連接處理的數(shù)據(jù)流達(dá)到上限后有兩種情況,一是等待請(qǐng)求被取消,二是等待其他請(qǐng)求結(jié)束。如果有其他數(shù)據(jù)流結(jié)束并喚醒當(dāng)前等待的請(qǐng)求,則重復(fù)2.1、2.2和2.3的步驟。

        3、調(diào)用cc.newStream()在連接上創(chuàng)建一個(gè)數(shù)據(jù)流(創(chuàng)建數(shù)據(jù)流是線程安全的,因?yàn)樵创a中在調(diào)用awaitOpenSlotForRequest之前先加鎖,直到寫(xiě)入請(qǐng)求的header之后才釋放鎖)。

        func (cc *http2ClientConn) newStream() *http2clientStream {
        cs := &http2clientStream{
        cc: cc,
        ID: cc.nextStreamID,
        resc: make(chan http2resAndError, 1),
        peerReset: make(chan struct{}),
        done: make(chan struct{}),
        }
        cs.flow.add(int32(cc.initialWindowSize))
        cs.flow.setConnFlow(&cc.flow)
        cs.inflow.add(http2transportDefaultStreamFlow)
        cs.inflow.setConnFlow(&cc.inflow)
        cc.nextStreamID += 2
        cc.streams[cs.ID] = cs
        return cs
        }

        筆者對(duì)上述代碼簡(jiǎn)單描述如下:

        • 新建一個(gè)http2clientStream,數(shù)據(jù)流ID為cc.nextStreamID,新建數(shù)據(jù)流后,cc.nextStreamID +=2

        • 數(shù)據(jù)流通過(guò)http2resAndError管道接收請(qǐng)求的響應(yīng)。

        • 初始化當(dāng)前數(shù)據(jù)流的可寫(xiě)流控制窗口大小為cc.initialWindowSize,并保存連接的可寫(xiě)流控制指針。

        • 初始化當(dāng)前數(shù)據(jù)流的可讀流控制窗口大小為http2transportDefaultStreamFlow,并保存連接的可讀流控制指針。

        • 最后將新建的數(shù)據(jù)流注冊(cè)到當(dāng)前連接中。

        4、調(diào)用cc.t.getBodyWriterState(cs, body)會(huì)返回一個(gè)http2bodyWriterState結(jié)構(gòu)體。通過(guò)該結(jié)構(gòu)體可以知道請(qǐng)求body是否發(fā)送成功。

        func (t *http2Transport) getBodyWriterState(cs *http2clientStream, body io.Reader) (s http2bodyWriterState) {
        s.cs = cs
        if body == nil {
        return
        }
        resc := make(chan error, 1)
        s.resc = resc
        s.fn = func() {
        cs.cc.mu.Lock()
        cs.startedWrite = true
        cs.cc.mu.Unlock()
        resc <- cs.writeRequestBody(body, cs.req.Body)
        }
        s.delay = t.expectContinueTimeout()
        if s.delay == 0 ||
        !httpguts.HeaderValuesContainsToken(
        cs.req.Header["Expect"],
        "100-continue") {
        return
        }
        // 此處省略代碼,因?yàn)榻^大部分請(qǐng)求都不會(huì)設(shè)置100-continue的標(biāo)頭
        return
        }

        s.fn: 標(biāo)記當(dāng)前數(shù)據(jù)流開(kāi)始寫(xiě)入數(shù)據(jù),并且將請(qǐng)求body的發(fā)送結(jié)果寫(xiě)入s.resc管道(本文暫不對(duì)writeRequestBody展開(kāi)分析,下篇文章會(huì)對(duì)其進(jìn)行分析)。

        5、因?yàn)槭嵌鄠€(gè)請(qǐng)求共享一個(gè)連接,那么向連接寫(xiě)入數(shù)據(jù)幀時(shí)需要加鎖,比如加鎖寫(xiě)入請(qǐng)求頭。

        cc.wmu.Lock()
        endStream := !hasBody && !hasTrailers
        werr := cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs)
        cc.wmu.Unlock()

        6、如果有請(qǐng)求body,則開(kāi)始寫(xiě)入請(qǐng)求body,沒(méi)有請(qǐng)求body則設(shè)置響應(yīng)header的超時(shí)時(shí)間(有請(qǐng)求body時(shí),響應(yīng)header的超時(shí)時(shí)間需要在請(qǐng)求body寫(xiě)完之后設(shè)置)。

        if hasBody {
        bodyWriter.scheduleBodyWrite()
        } else {
        http2traceWroteRequest(cs.trace, nil)
        if d := cc.responseHeaderTimeout(); d != 0 {
        timer := time.NewTimer(d)
        defer timer.Stop()
        respHeaderTimer = timer.C
        }
        }

        scheduleBodyWrite的內(nèi)容如下:

        func (s http2bodyWriterState) scheduleBodyWrite() {
        if s.timer == nil {
        // We're not doing a delayed write (see
        // getBodyWriterState), so just start the writing
        // goroutine immediately.
        go s.fn()
        return
        }
        http2traceWait100Continue(s.cs.trace)
        if s.timer.Stop() {
        s.timer.Reset(s.delay)
        }
        }

        因?yàn)楣P者的請(qǐng)求header中沒(méi)有攜帶100-continue標(biāo)頭,所以在前面的getBodyWriterState函數(shù)中初始化的s.timer為nil即調(diào)用scheduleBodyWrite會(huì)立即開(kāi)始發(fā)送請(qǐng)求body。

        7、輪詢(xún)管道獲取響應(yīng)結(jié)果。

        在看輪詢(xún)?cè)创a之前,先看一個(gè)簡(jiǎn)單的函數(shù):

        handleReadLoopResponse := func(re http2resAndError) (*Response, bool, error) {
        res := re.res
        if re.err != nil || res.StatusCode > 299 {
        bodyWriter.cancel()
        cs.abortRequestBodyWrite(http2errStopReqBodyWrite)
        }
        if re.err != nil {
        cc.forgetStreamID(cs.ID)
        return nil, cs.getStartedWrite(), re.err
        }
        res.Request = req
        res.TLS = cc.tlsState
        return res, false, nil
        }

        該函數(shù)主要就是判斷讀到的響應(yīng)是否正常,并根據(jù)響應(yīng)的結(jié)果構(gòu)造(*http2ClientConn).roundTrip的返回值。

        了解了handleReadLoopResponse之后,下面就看看輪詢(xún)的邏輯:

        for {
        select {
        case re := <-readLoopResCh:
        return handleReadLoopResponse(re)
        // 此處省略代碼(包含請(qǐng)求取消,請(qǐng)求超時(shí)等管道的輪詢(xún))
        case err := <-bodyWriter.resc:
        // Prefer the read loop's response, if available. Issue 16102.
        select {
        case re := <-readLoopResCh:
        return handleReadLoopResponse(re)
        default:
        }
        if err != nil {
        cc.forgetStreamID(cs.ID)
        return nil, cs.getStartedWrite(), err
        }
        bodyWritten = true
        if d := cc.responseHeaderTimeout(); d != 0 {
        timer := time.NewTimer(d)
        defer timer.Stop()
        respHeaderTimer = timer.C
        }
        }
        }

        筆者僅對(duì)上面的第二種情況即請(qǐng)求body發(fā)送完成進(jìn)行描述:

        • 能否讀到響應(yīng),如果能夠讀取響應(yīng)則直接返回。

        • 判斷請(qǐng)求body是否發(fā)送成功,如果發(fā)送失敗,直接返回。

        • 如果請(qǐng)求body發(fā)送成功,則設(shè)置響應(yīng)header的超時(shí)時(shí)間。

        總結(jié)

        本文主要描述了兩個(gè)方面的內(nèi)容:

        1. 確認(rèn)client和server都支持http2協(xié)議,并構(gòu)建一個(gè)http2的連接,同時(shí)開(kāi)啟該連接的讀循環(huán)。

        2. 通過(guò)http2連接池獲取一個(gè)http2連接,并發(fā)送請(qǐng)求和讀取響應(yīng)。

        預(yù)告

        鑒于HTTTP2.0的內(nèi)容較多,且文章篇幅過(guò)長(zhǎng)時(shí)不易閱讀,筆者將后續(xù)要分析的內(nèi)容拆為兩個(gè)部分:

        1. 描述數(shù)據(jù)幀和流控制以及讀循環(huán)讀到響應(yīng)并發(fā)送給readLoopResCh管道。

        2. http2.0標(biāo)頭壓縮邏輯。

        最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。

        :

        1. 寫(xiě)本文時(shí), 筆者所用go版本為: go1.14.2。

        2. 本文對(duì)h2c的情況不予以考慮。

        3. 因?yàn)楣P者分析的是請(qǐng)求流程,所以沒(méi)有在本地搭建server,而是使用了一個(gè)支持http2連接的圖片一步步的debug。eg:?https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/topnav/[email protected]

        參考

        https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn

        -------------------?End?-------------------

        往期精彩文章推薦:

        歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持

        想加入Go學(xué)習(xí)群請(qǐng)?jiān)诤笈_(tái)回復(fù)【入群

        萬(wàn)水千山總是情,點(diǎn)個(gè)【在看】行不行

        瀏覽 33
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 开心 五月 综合 亚洲 成人 | 麻豆精品三级 | 精品一区久九综合 | 忘穿内裤坐公交被疯狂进去视频 | 91欧美日韩 |