Go發(fā)起HTTP2.0請(qǐng)求流程分析(前篇)
前言
繼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è)步驟:
新建一個(gè)
http2clientConnPool并復(fù)制給t2,以后http2的請(qǐng)求會(huì)優(yōu)先從該連接池中獲取連接。初始化
TLSClientConfig,并將支持的h2和http1.1協(xié)議添加到TLSClientConfig.NextProtos中。定義一個(gè)
h2的upgradeFn存儲(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ì)上述的源碼描述如下:
調(diào)用
t.dial(ctx, "tcp", cm.addr())創(chuàng)建TCP連接。如果是https的請(qǐng)求, 則對(duì)請(qǐng)求建立安全的tls傳輸通道。
檢查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的值為h2且s.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)建好的t2即http2Transport。
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é)論:
執(zhí)行完
upgradeFn之后,(*Transport).dialConn返回的持久化連接中alt字段已經(jīng)不是nil了。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)容:
確認(rèn)client和server都支持http2協(xié)議,并構(gòu)建一個(gè)http2的連接,同時(shí)開(kāi)啟該連接的讀循環(huán)。
通過(guò)http2連接池獲取一個(gè)http2連接,并發(fā)送請(qǐng)求和讀取響應(yīng)。
預(yù)告
鑒于HTTTP2.0的內(nèi)容較多,且文章篇幅過(guò)長(zhǎng)時(shí)不易閱讀,筆者將后續(xù)要分析的內(nèi)容拆為兩個(gè)部分:
描述數(shù)據(jù)幀和流控制以及讀循環(huán)讀到響應(yīng)并發(fā)送給
readLoopResCh管道。http2.0標(biāo)頭壓縮邏輯。
最后,衷心希望本文能夠?qū)Ω魑蛔x者有一定的幫助。
注:
寫(xiě)本文時(shí), 筆者所用go版本為: go1.14.2。
本文對(duì)h2c的情況不予以考慮。
因?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
推薦閱讀
站長(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)注
