<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 5253字,需瀏覽 11分鐘

           ·

          2022-04-14 12:20

          點擊上方“Go語言進階學習”,進行關(guān)注

          回復(fù)“Go語言”即可獲贈從入門到進階共10本電子書

          遠芳侵古道,晴翠接荒城。

          前言

          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驗證,當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{}),
          }

          上面的源碼新建了一個默認的http2ClientConn。

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

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

          streams:當前連接上的數(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將當前連接的可寫流控制窗口大小設(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()判斷當前連接池中的連接能否處理新的請求:

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

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

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

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

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

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

          • 當前連接沒有長時間處于空閑狀態(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()

          當從鏈接池成功獲取到一個可以處理請求的連接,就可以和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,一直等到當前連接處理的數(shù)據(jù)流小于maxConcurrentStreams, 如果此函數(shù)返回錯誤,則本次請求失敗。

          2.1、double check當前連接可用。

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

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

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

          2.3、如果當前連接處理的數(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--

          通過上面的邏輯知道,當前連接處理的數(shù)據(jù)流達到上限后有兩種情況,一是等待請求被取消,二是等待其他請求結(jié)束。如果有其他數(shù)據(jù)流結(jié)束并喚醒當前等待的請求,則重復(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)。

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

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

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

          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的標頭
          return
          }

          s.fn: 標記當前數(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標頭,所以在前面的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. 確認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標頭壓縮邏輯。

          最后,衷心希望本文能夠?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

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

          往期精彩文章推薦:

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

          想加入Go學習群請在后臺回復(fù)【入群

          萬水千山總是情,點個【在看】行不行

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大鸡巴视频网站 | 亚洲高清无码免费在线观看 | 香焦久久网 | 天天三级片 | 国产精品无码AV无码 |