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

        共 5287字,需瀏覽 11分鐘

         ·

        2020-09-17 02:17

        前言

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

        閱讀建議

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

        (*Transport).roundTrip

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

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

        h2transport: 如果本次請求是http2,那么h2transport會接管連接,請求和響應(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
        }

        筆者將上述的源碼簡單拆解為以下幾個步驟:

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

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

        3. 定義一個h2upgradeFn存儲到t1.TLSNextProto里。

        鑒于前一篇文章對新建連接前的步驟有了較為詳細的介紹,所以這里直接看和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
        }
        }

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

        筆者對上述的源碼描述如下:

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

        2. 如果是https的請求, 則對請求建立安全的tls傳輸通道。

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

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

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

        upgradeFn會調(diào)用connPool.addConnIfNeeded向http2的連接池添加一個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連接,如果有且該鏈接能夠處理新的請求則直接返回
        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)
        }

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

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

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

        最后我們回到(*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的請求會走pconn.alt.RoundTrip(req)分支,也就是說http2的請求流程就被http2Transport接管啦。

        (*http2Transport).NewClientConn

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

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

        1、初始化一個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{}),
        }

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

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

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

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

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

        2、創(chuàng)建一個條件鎖并且新建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沒什么好說的,需要注意的是cc.flow.add(int32(http2initialWindowSize))。

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

        3、新建一個讀寫數(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ā)送開場白,并發(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ā)送的開場白內(nèi)容如下:

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

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

        http2SettingEnablePush: 告知server客戶端是否開啟push功能。

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

        發(fā)送完SETTINGS數(shù)據(jù)幀后,發(fā)送WINDOW_UPDATE數(shù)據(jù)幀, 因為第一個參數(shù)為0即streamID為0,則是告知server此連接可接受的最大數(shù)據(jù)窗口為http2transportDefaultConnFlow(1G)。

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

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

        go cc.readLoop()

        (*http2Transport).RoundTrip

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

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

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

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

        (http2noDialClientConnPool).GetClientConn

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

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

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

        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)前連接池中的連接能否處理新的請求:

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

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

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

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

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

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

        • 當(dāng)前連接沒有長時間處于空閑狀態(tài)(主要通過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)從鏈接池成功獲取到一個可以處理請求的連接,就可以和server進行數(shù)據(jù)交互,即(*http2ClientConn).roundTrip流程。

        (*http2ClientConn).roundTrip

        1、在真正開始處理請求前,還要進行header檢查,http2對http1.1的某些header是不支持的,筆者就不對這個邏輯進行分析了,直接上源碼:

        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ù)返回錯誤,則本次請求失敗。

        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ù)流確實已經(jīng)達到上限,則開始進入等待流程。

        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--

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

        3、調(diào)用cc.newStream()在連接上創(chuàng)建一個數(shù)據(jù)流(創(chuàng)建數(shù)據(jù)流是線程安全的,因為源碼中在調(diào)用awaitOpenSlotForRequest之前先加鎖,直到寫入請求的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
        }

        筆者對上述代碼簡單描述如下:

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

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

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

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

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

        4、調(diào)用cc.t.getBodyWriterState(cs, body)會返回一個http2bodyWriterState結(jié)構(gòu)體。通過該結(jié)構(gòu)體可以知道請求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
        }
        // 此處省略代碼,因為絕大部分請求都不會設(shè)置100-continue的標(biāo)頭
        return
        }

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

        5、因為是多個請求共享一個連接,那么向連接寫入數(shù)據(jù)幀時需要加鎖,比如加鎖寫入請求頭。

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

        6、如果有請求body,則開始寫入請求body,沒有請求body則設(shè)置響應(yīng)header的超時時間(有請求body時,響應(yīng)header的超時時間需要在請求body寫完之后設(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)
        }
        }

        因為筆者的請求header中沒有攜帶100-continue標(biāo)頭,所以在前面的getBodyWriterState函數(shù)中初始化的s.timer為nil即調(diào)用scheduleBodyWrite會立即開始發(fā)送請求body。

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

        在看輪詢源碼之前,先看一個簡單的函數(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之后,下面就看看輪詢的邏輯:

        for {
        select {
        case re := <-readLoopResCh:
        return handleReadLoopResponse(re)
        // 此處省略代碼(包含請求取消,請求超時等管道的輪詢)
        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
        }
        }
        }

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

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

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

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

        總結(jié)

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

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

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

        預(yù)告

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

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

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

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

        :

        1. 寫本文時, 筆者所用go版本為: go1.14.2。

        2. 本文對h2c的情況不予以考慮。

        3. 因為筆者分析的是請求流程,所以沒有在本地搭建server,而是使用了一個支持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



        推薦閱讀



        學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進群」即可


        站長 polarisxu

        自己的原創(chuàng)文章

        不限于 Go 技術(shù)

        職場和創(chuàng)業(yè)經(jīng)驗


        Go語言中文網(wǎng)

        每天為你

        分享 Go 知識

        Go愛好者值得關(guān)注



        瀏覽 34
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 色欧美一区二区三区 | 四虎永久www成人影院 | 黄大色黄女片18第一次 | 被主人野外调教暴露尿便器电影 | 欧美老妇女视频 |