<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請(qǐng)求流程分析(前篇)

          共 5287字,需瀏覽 11分鐘

           ·

          2020-09-17 02:17

          前言

          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客戶端是否開(kāi)啟push功能。

          http2SettingInitialWindowSize:告知server客戶端可接受的最大數(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、輪詢管道獲取響應(yīng)結(jié)果。

          在看輪詢?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之后,下面就看看輪詢的邏輯:

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



          推薦閱讀



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


          站長(zhǎng) polarisxu

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

          不限于 Go 技術(shù)

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


          Go語(yǔ)言中文網(wǎng)

          每天為你

          分享 Go 知識(shí)

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



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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  成人做爰A片免费播放乱码 | 91成人视频18 | 日韩中文字幕成人 | 色婷婷色99国产综合精品 | 久久久欧美精品sm网站 |