Go發起HTTP2.0請求流程分析(中篇)——數據幀&流控制

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來自公衆號:新世界雜貨鋪"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"閱讀建議"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是HTTP2.0系列的第二篇,所以筆者推薦閱讀順序如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/cb50c604d02c1ad0863e6a6d9","title":""},"content":[{"type":"text","text":"Go中的HTTP請求之——HTTP1.1請求流程分析"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/6107cc8ccba566d1bcb4b2159","title":""},"content":[{"type":"text","text":"Go發起HTTP2.0請求流程分析(前篇)"}]}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本篇主要分爲三個部分:數據幀,流控制器以及通過分析源碼逐步瞭解流控制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本有意將這三個部分拆成三篇文章,但它們之間又有聯繫,所以最後依舊決定放在一篇文章裏面。由於內容較多,筆者認爲分三次分別閱讀三個部分較佳。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"數據幀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP2通信的最小單位是數據幀,每一個幀都包含兩部分:"},{"type":"text","marks":[{"type":"strong"}],"text":"幀頭"},{"type":"text","text":"和*"},{"type":"text","marks":[{"type":"italic"}],"text":"Payload"},{"type":"text","text":"*。不同數據流的幀可以交錯發送(同一個數據流的幀必須順序發送),然後再根據每個幀頭的數據流標識符重新組裝。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於Payload中爲有效數據,故僅對幀頭進行分析描述。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"幀頭"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"幀頭總長度爲9個字節"},{"type":"text","text":",幷包含四個部分,分別是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Payload的長度,佔用三個字節。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"數據幀類型,佔用一個字節。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"數據幀標識符,佔用一個字節。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"數據流ID,佔用四個字節。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用圖表示如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/48/48e02c5543e295eb5f5ce5e41cd89108.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據幀的格式和各部分的含義已經清楚了, 那麼我們看看代碼中怎麼讀取一個幀頭:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func http2readFrameHeader(buf []byte, r io.Reader) (http2FrameHeader, error) {\n\t_, err := io.ReadFull(r, buf[:http2frameHeaderLen])\n\tif err != nil {\n\t\treturn http2FrameHeader{}, err\n\t}\n\treturn http2FrameHeader{\n\t\tLength: (uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])),\n\t\tType: http2FrameType(buf[3]),\n\t\tFlags: http2Flags(buf[4]),\n\t\tStreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1),\n\t\tvalid: true,\n\t}, nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的代碼中"},{"type":"codeinline","content":[{"type":"text","text":"http2frameHeaderLen"}]},{"type":"text","text":"是一個常量,其值爲9。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從io.Reader中讀取9個字節後,將前三個字節和後四個字節均轉爲"},{"type":"codeinline","content":[{"type":"text","text":"uint32"}]},{"type":"text","text":"的類型,從而得到Payload長度和數據流ID。另外需要理解的是幀頭的前三個字節和後四個字節存儲格式爲大端(大小端筆者就不在這裏解釋了,請尚不瞭解的讀者自行百度)。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"數據幀類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據http://http2.github.io/http2-spec/#rfc.section.11.2描述,數據幀類型總共有10個。在go源碼中均有體現:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"const (\n\thttp2FrameData http2FrameType = 0x0\n\thttp2FrameHeaders http2FrameType = 0x1\n\thttp2FramePriority http2FrameType = 0x2\n\thttp2FrameRSTStream http2FrameType = 0x3\n\thttp2FrameSettings http2FrameType = 0x4\n\thttp2FramePushPromise http2FrameType = 0x5\n\thttp2FramePing http2FrameType = 0x6\n\thttp2FrameGoAway http2FrameType = 0x7\n\thttp2FrameWindowUpdate http2FrameType = 0x8\n\thttp2FrameContinuation http2FrameType = 0x9\n)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FrameData"}]},{"type":"text","text":":主要用於發送請求body和接收響應的數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FrameHeaders"}]},{"type":"text","text":":主要用於發送請求header和接收響應header的數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":":主要用於client和server交流設置相關的數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FrameWindowUpdate"}]},{"type":"text","text":":主要用於流控制的數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其他數據幀類型因爲本文不涉及,故不做描述。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"數據幀標識符"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由於數據幀標識符種類較多,筆者在這裏僅介紹其中部分標識符,先看源碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"const (\n\t// Data Frame\n\thttp2FlagDataEndStream http2Flags = 0x1\n \n // Headers Frame\n\thttp2FlagHeadersEndStream http2Flags = 0x1\n \n // Settings Frame\n\thttp2FlagSettingsAck http2Flags = 0x1\n\t// 此處省略定義其他數據幀標識符的代碼\n)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FlagDataEndStream"}]},{"type":"text","text":":在前篇中提到,調用"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).newStream"}]},{"type":"text","text":"方法會創建一個數據流,那這個數據流什麼時候結束呢,這就是"},{"type":"codeinline","content":[{"type":"text","text":"http2FlagDataEndStream"}]},{"type":"text","text":"的作用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當client收到有響應body的響應時(HEAD請求無響應body,301,302等響應也無響應body),一直讀到"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameData"}]},{"type":"text","text":"數據幀的標識符爲"},{"type":"codeinline","content":[{"type":"text","text":"http2FlagDataEndStream"}]},{"type":"text","text":"則意味着本次請求結束可以關閉當前數據流。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FlagHeadersEndStream"}]},{"type":"text","text":":如果讀到的"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameHeaders"}]},{"type":"text","text":"數據幀有此標識符也意味着本次請求結束。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"http2FlagSettingsAck"}]},{"type":"text","text":":該標示符意味着對方確認收到"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"流控制器"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"流控制是一種阻止發送方向接收方發送大量數據的機制,以免超出後者的需求或處理能力。Go中HTTP2通過"},{"type":"codeinline","content":[{"type":"text","text":"http2flow"}]},{"type":"text","text":"結構體進行流控制:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"type http2flow struct {\n\t// n is the number of DATA bytes we're allowed to send.\n\t// A flow is kept both on a conn and a per-stream.\n\tn int32\n\n\t// conn points to the shared connection-level flow that is\n\t// shared by all streams on that conn. It is nil for the flow\n\t// that's on the conn directly.\n\tconn *http2flow\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"字段含義英文註釋已經描述的很清楚了,所以筆者不再翻譯。下面看一下和流控制有關的方法。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(*http2flow).available"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此方法返回當前流控制可發送的最大字節數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (f *http2flow) available() int32 {\n\tn := f.n\n\tif f.conn != nil && f.conn.n < n {\n\t\tn = f.conn.n\n\t}\n\treturn n\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果"},{"type":"codeinline","content":[{"type":"text","text":"f.conn"}]},{"type":"text","text":"爲nil則意味着此控制器的控制級別爲連接,那麼可發送的最大字節數就是"},{"type":"codeinline","content":[{"type":"text","text":"f.n"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果"},{"type":"codeinline","content":[{"type":"text","text":"f.conn"}]},{"type":"text","text":"不爲nil則意味着此控制器的控制級別爲數據流,且當前數據流可發送的最大字節數不能超過當前連接可發送的最大字節數。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(*http2flow).take"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此方法用於消耗當前流控制器的可發送字節數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (f *http2flow) take(n int32) {\n\tif n > f.available() {\n\t\tpanic(\"internal error: took too much\")\n\t}\n\tf.n -= n\n\tif f.conn != nil {\n\t\tf.conn.n -= n\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過實際需要傳遞一個參數,告知當前流控制器想要發送的數據大小。如果發送的大小超過流控制器允許的大小,則"},{"type":"codeinline","content":[{"type":"text","text":"panic"}]},{"type":"text","text":",如果未超過流控制器允許的大小,則將當前數據流和當前連接的可發送字節數"},{"type":"codeinline","content":[{"type":"text","text":"-n"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"(*http2flow).add"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有消耗就有新增,此方法用於增加流控制器可發送的最大字節數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (f *http2flow) add(n int32) bool {\n\tsum := f.n + n\n\tif (sum > n) == (f.n > 0) {\n\t\tf.n = sum\n\t\treturn true\n\t}\n\treturn false\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的代碼唯一需要注意的地方是,當sum超過int32正數最大值(2^31-1)時會返回false。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"回顧"},{"type":"text","text":":在前篇中提到的"},{"type":"codeinline","content":[{"type":"text","text":"(*http2Transport).NewClientConn"}]},{"type":"text","text":"方法和"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).newStream"}]},{"type":"text","text":"方法均通過"},{"type":"codeinline","content":[{"type":"text","text":"(*http2flow).add"}]},{"type":"text","text":"初始化可發送數據窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了幀和流控制器的基本概念,下面我們結合源碼來分析總結流控制的具體實現。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"(*http2ClientConn).readLoop"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前篇分析"},{"type":"codeinline","content":[{"type":"text","text":"(*http2Transport).newClientConn"}]},{"type":"text","text":"時止步於讀循環,那麼今天我們就從"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).readLoop"}]},{"type":"text","text":"開始。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (cc *http2ClientConn) readLoop() {\n\trl := &http2clientConnReadLoop{cc: cc}\n\tdefer rl.cleanup()\n\tcc.readerErr = rl.run()\n\tif ce, ok := cc.readerErr.(http2ConnectionError); ok {\n\t\tcc.wmu.Lock()\n\t\tcc.fr.WriteGoAway(0, http2ErrCode(ce), nil)\n\t\tcc.wmu.Unlock()\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,readLoop的邏輯比較簡單,其核心邏輯在"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).run"}]},{"type":"text","text":"方法裏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (rl *http2clientConnReadLoop) run() error {\n\tcc := rl.cc\n\trl.closeWhenIdle = cc.t.disableKeepAlives() || cc.singleUse\n\tgotReply := false // ever saw a HEADERS reply\n\tgotSettings := false\n\tfor {\n\t\tf, err := cc.fr.ReadFrame()\n // 此處省略代碼\n\t\tmaybeIdle := false // whether frame might transition us to idle\n\n\t\tswitch f := f.(type) {\n\t\tcase *http2MetaHeadersFrame:\n\t\t\terr = rl.processHeaders(f)\n\t\t\tmaybeIdle = true\n\t\t\tgotReply = true\n\t\tcase *http2DataFrame:\n\t\t\terr = rl.processData(f)\n\t\t\tmaybeIdle = true\n\t\tcase *http2GoAwayFrame:\n\t\t\terr = rl.processGoAway(f)\n\t\t\tmaybeIdle = true\n\t\tcase *http2RSTStreamFrame:\n\t\t\terr = rl.processResetStream(f)\n\t\t\tmaybeIdle = true\n\t\tcase *http2SettingsFrame:\n\t\t\terr = rl.processSettings(f)\n\t\tcase *http2PushPromiseFrame:\n\t\t\terr = rl.processPushPromise(f)\n\t\tcase *http2WindowUpdateFrame:\n\t\t\terr = rl.processWindowUpdate(f)\n\t\tcase *http2PingFrame:\n\t\t\terr = rl.processPing(f)\n\t\tdefault:\n\t\t\tcc.logf(\"Transport: unhandled response frame type %T\", f)\n\t\t}\n\t\tif err != nil {\n\t\t\tif http2VerboseLogs {\n\t\t\t\tcc.vlogf(\"http2: Transport conn %p received error from processing frame %v: %v\", cc, http2summarizeFrame(f), err)\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tif rl.closeWhenIdle && gotReply && maybeIdle {\n\t\t\tcc.closeIfIdle()\n\t\t}\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).run"}]},{"type":"text","text":"的核心邏輯是讀取數據幀然後對不同的數據幀進行不同的處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"cc.fr.ReadFrame()"}]},{"type":"text","text":"會根據前面介紹的數據幀格式讀出數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前篇中提到使用了一個支持h2協議的圖片進行分析,本篇繼續複用該圖片對"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).run"}]},{"type":"text","text":"方法進行debug。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":" 收到http2FrameSettings數據幀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀循環會最先讀到"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"數據幀。讀到該數據幀後會調用"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).processSettings"}]},{"type":"text","text":"方法。"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).processSettings"}]},{"type":"text","text":"主要包含3個邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、判斷是否是"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"的ack信息,如果是直接返回,否則繼續後面的步驟。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"if f.IsAck() {\n if cc.wantSettingsAck {\n cc.wantSettingsAck = false\n return nil\n }\n return http2ConnectionError(http2ErrCodeProtocol)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、處理不同"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"的數據幀,並根據server傳遞的信息,修改"},{"type":"codeinline","content":[{"type":"text","text":"maxConcurrentStreams"}]},{"type":"text","text":"等的值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"err := f.ForeachSetting(func(s http2Setting) error {\n switch s.ID {\n case http2SettingMaxFrameSize:\n cc.maxFrameSize = s.Val\n case http2SettingMaxConcurrentStreams:\n cc.maxConcurrentStreams = s.Val\n case http2SettingMaxHeaderListSize:\n cc.peerMaxHeaderListSize = uint64(s.Val)\n case http2SettingInitialWindowSize:\n if s.Val > math.MaxInt32 {\n return http2ConnectionError(http2ErrCodeFlowControl)\n }\n delta := int32(s.Val) - int32(cc.initialWindowSize)\n for _, cs := range cc.streams {\n cs.flow.add(delta)\n }\n cc.cond.Broadcast()\n cc.initialWindowSize = s.Val\n default:\n // TODO(bradfitz): handle more settings? SETTINGS_HEADER_TABLE_SIZE probably.\n cc.vlogf(\"Unhandled Setting: %v\", s)\n }\n return nil\n})"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當收到ID爲"},{"type":"codeinline","content":[{"type":"text","text":"http2SettingInitialWindowSize"}]},{"type":"text","text":"的幀時,會調整當前連接中所有數據流的可發送數據窗口大小,並修改當前連接的"},{"type":"codeinline","content":[{"type":"text","text":"initialWindowSize"}]},{"type":"text","text":"(每個新創建的數據流均會使用該值初始化可發送數據窗口大小)爲"},{"type":"codeinline","content":[{"type":"text","text":"s.Val"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、發送"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"的ack信息給server。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"\tcc.wmu.Lock()\n\tdefer cc.wmu.Unlock()\n\n\tcc.fr.WriteSettingsAck()\n\tcc.bw.Flush()\n\treturn cc.werr"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"收到http2WindowUpdateFrame數據幀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在筆者debug的過程中,處理完"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"數據幀後,緊接着就收到了"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀。收到該數據幀後會調用"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).processWindowUpdate"}]},{"type":"text","text":"方法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (rl *http2clientConnReadLoop) processWindowUpdate(f *http2WindowUpdateFrame) error {\n\tcc := rl.cc\n\tcs := cc.streamByID(f.StreamID, false)\n\tif f.StreamID != 0 && cs == nil {\n\t\treturn nil\n\t}\n\n\tcc.mu.Lock()\n\tdefer cc.mu.Unlock()\n\n\tfl := &cc.flow\n\tif cs != nil {\n\t\tfl = &cs.flow\n\t}\n\tif !fl.add(int32(f.Increment)) {\n\t\treturn http2ConnectionError(http2ErrCodeFlowControl)\n\t}\n\tcc.cond.Broadcast()\n\treturn nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 上面的邏輯主要用於更新當前連接和數據流的可發送數據窗口大小。如果http2WindowUpdateFrame幀中的StreamID爲0,則更新當前連接的可發送數據窗口大小,否則更新對應數據流可發送數據窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注意"},{"type":"text","text":":在debug的過程,收到"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀後,又收到一次"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":",且該數據幀標識符爲"},{"type":"codeinline","content":[{"type":"text","text":"http2FlagSettingsAck"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"筆者在這裏特意提醒,這是因爲前篇中提到的(*http2Transport).NewClientConn方法,也向server發送了http2FrameSettings數據幀和http2WindowUpdateFrame數據幀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,在處理"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"過程中,均出現了"},{"type":"codeinline","content":[{"type":"text","text":"cc.cond.Broadcast()"}]},{"type":"text","text":"調用,該調用主要用於喚醒因爲以下兩種情況而"},{"type":"codeinline","content":[{"type":"text","text":"Wait"}]},{"type":"text","text":"的請求:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"因當前連接處理的數據流已經達到"},{"type":"codeinline","content":[{"type":"text","text":"maxConcurrentStreams"}]},{"type":"text","text":"的上限(詳見前篇中"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).awaitOpenSlotForRequest"}]},{"type":"text","text":"方法分析)。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"因發送數據流已達可發送數據窗口上限而等待可發送數據窗口更新的請求(後續會介紹)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"收到http2MetaHeadersFrame數據幀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收到此數據幀意味着某一個請求已經開始接收響應數據。此數據幀對應的處理函數爲"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).processHeaders"}]},{"type":"text","text":":"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (rl *http2clientConnReadLoop) processHeaders(f *http2MetaHeadersFrame) error {\n\tcc := rl.cc\n\tcs := cc.streamByID(f.StreamID, false)\n\t// 此處省略代碼\n\tres, err := rl.handleResponse(cs, f)\n\tif err != nil {\n\t\t// 此處省略代碼\n\t\tcs.resc 1 {\n // TODO: care? unlike http/1, it won't mess up our framing, so it's\n // more safe smuggling-wise to ignore.\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知,當前數據流沒有結束或者是HEAD請求才讀取ContentLength。如果header中的ContentLength不合法則res.ContentLength的值爲 "},{"type":"text","marks":[{"type":"strong"}],"text":"-1"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、構建"},{"type":"codeinline","content":[{"type":"text","text":"res.Body"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"cs.bufPipe = http2pipe{b: &http2dataBuffer{expected: res.ContentLength}}\ncs.bytesRemain = res.ContentLength\nres.Body = http2transportResponseBody{cs}\ngo cs.awaitRequestCancel(cs.req)\n\nif cs.requestedGzip && res.Header.Get(\"Content-Encoding\") == \"gzip\" {\n res.Header.Del(\"Content-Encoding\")\n res.Header.Del(\"Content-Length\")\n res.ContentLength = -1\n res.Body = &http2gzipReader{body: res.Body}\n res.Uncompressed = true\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據"},{"type":"codeinline","content":[{"type":"text","text":"Content-Encoding"}]},{"type":"text","text":"的編碼方式,會構建兩種不同的Body:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"非gzip編碼時,構造的res.Body類型爲"},{"type":"codeinline","content":[{"type":"text","text":"http2transportResponseBody"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"gzip編碼時,構造的res.Body類型爲"},{"type":"codeinline","content":[{"type":"text","text":"http2gzipReader"}]},{"type":"text","text":"。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"收到http2DataFrame數據幀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收到此數據幀意味着我們開始接收真實的響應,即平常開發中需要處理的業務數據。此數據幀對應的處理函數爲"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).processData"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲server無法及時知道數據流在client端的狀態,所以server可能會向client中一個已經不存在的數據流發送數據:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"cc := rl.cc\ncs := cc.streamByID(f.StreamID, f.StreamEnded())\ndata := f.Data()\nif cs == nil {\n cc.mu.Lock()\n neverSent := cc.nextStreamID\n cc.mu.Unlock()\n // 此處省略代碼\n if f.Length > 0 {\n cc.mu.Lock()\n cc.inflow.add(int32(f.Length))\n cc.mu.Unlock()\n\n cc.wmu.Lock()\n cc.fr.WriteWindowUpdate(0, uint32(f.Length))\n cc.bw.Flush()\n cc.wmu.Unlock()\n }\n return nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接收到的數據幀在client沒有對應的數據流處理時,通過流控制器爲當前連接可讀窗口大小增加"},{"type":"codeinline","content":[{"type":"text","text":"f.Length"}]},{"type":"text","text":",並且通過"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameWindowUpdate"}]},{"type":"text","text":"數據幀告知server將當前連接的可寫窗口大小增加"},{"type":"codeinline","content":[{"type":"text","text":"f.Length"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果client有對應的數據流且"},{"type":"codeinline","content":[{"type":"text","text":"f.Length"}]},{"type":"text","text":"大於0:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、如果是head請求結束當前數據流並返回。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"if cs.req.Method == \"HEAD\" && len(data) > 0 {\n cc.logf(\"protocol error: received DATA on a HEAD request\")\n rl.endStreamError(cs, http2StreamError{\n StreamID: f.StreamID,\n Code: http2ErrCodeProtocol,\n })\n return nil\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、檢查當前數據流能否處理"},{"type":"codeinline","content":[{"type":"text","text":"f.Length"}]},{"type":"text","text":"長度的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"cc.mu.Lock()\nif cs.inflow.available() >= int32(f.Length) {\n cs.inflow.take(int32(f.Length))\n} else {\n cc.mu.Unlock()\n return http2ConnectionError(http2ErrCodeFlowControl)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可知當前數據流如果能夠處理該數據,通過流控制器調用"},{"type":"codeinline","content":[{"type":"text","text":"cs.inflow.take"}]},{"type":"text","text":"減小當前數據流可接受窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、當前數據流被重置或者被關閉即"},{"type":"codeinline","content":[{"type":"text","text":"cs.didReset"}]},{"type":"text","text":"爲true時又或者數據幀有填充數據時需要調整流控制窗口。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"var refund int\nif pad := int(f.Length) - len(data); pad > 0 {\n refund += pad\n}\n// Return len(data) now if the stream is already closed,\n// since data will never be read.\ndidReset := cs.didReset\nif didReset {\n refund += len(data)\n}\nif refund > 0 {\n cc.inflow.add(int32(refund))\n cc.wmu.Lock()\n cc.fr.WriteWindowUpdate(0, uint32(refund))\n if !didReset {\n cs.inflow.add(int32(refund))\n cc.fr.WriteWindowUpdate(cs.ID, uint32(refund))\n }\n cc.bw.Flush()\n cc.wmu.Unlock()\n}\ncc.mu.Unlock()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果數據幀有填充數據則計算需要返還的填充數據長度。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果數據流無效該數據幀的長度需要全部返還。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,根據計算的refund增加當前連接或者當前數據流的可接受窗口大小,並且同時告知server增加當前連接或者當前數據流的可寫窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4、數據長度大於0且數據流正常則將數據寫入數據流緩衝區。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"if len(data) > 0 && !didReset {\n if _, err := cs.bufPipe.Write(data); err != nil {\n rl.endStreamError(cs, err)\n return err\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"回顧"},{"type":"text","text":":前面的"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientConnReadLoop).handleResponse"}]},{"type":"text","text":"方法中有這樣一行代碼"},{"type":"codeinline","content":[{"type":"text","text":"res.Body = http2transportResponseBody{cs}"}]},{"type":"text","text":",所以在業務開發時能夠通過Response讀取到數據流中的緩衝數據。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"(http2transportResponseBody).Read"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在前面的內容裏,如果數據流狀態正常且數據幀沒有填充數據則數據流和連接的可接收窗口會一直變小,而這部分內容就是增加數據流的可接受窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲篇幅和主旨的問題筆者僅分析描述該方法內和流控制有關的部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、讀取響應數據後計算當前連接需要增加的可接受窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"cc.mu.Lock()\ndefer cc.mu.Unlock()\nvar connAdd, streamAdd int32\n// Check the conn-level first, before the stream-level.\nif v := cc.inflow.available(); v < http2transportDefaultConnFlow/2 {\n connAdd = http2transportDefaultConnFlow - v\n cc.inflow.add(connAdd)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果當前連接可接受窗口的大小已經小於"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultConnFlow"}]},{"type":"text","text":"(1G)的一半,則當前連接可接收窗口大小需要增加"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultConnFlow - cc.inflow.available() "}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"回顧"},{"type":"text","text":":"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultConnFlow"}]},{"type":"text","text":"在前篇"},{"type":"codeinline","content":[{"type":"text","text":"(*http2Transport).NewClientConn"}]},{"type":"text","text":"方法部分有提到,且連接剛建立時會通過"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀告知server當前連接可發送窗口大小增加"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultConnFlow"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、讀取響應數據後計算當前數據流需要增加的可接受窗口大小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"if err == nil { // No need to refresh if the stream is over or failed.\n // Consider any buffered body data (read from the conn but not\n // consumed by the client) when computing flow control for this\n // stream.\n v := int(cs.inflow.available()) + cs.bufPipe.Len()\n if v < http2transportDefaultStreamFlow-http2transportDefaultStreamMinRefresh {\n streamAdd = int32(http2transportDefaultStreamFlow - v)\n cs.inflow.add(streamAdd)\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果當前數據流可接受窗口大小加上當前數據流緩衝區剩餘未讀數據的長度小於"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultStreamFlow-http2transportDefaultStreamMinRefresh"}]},{"type":"text","text":"(4M-4KB),則當前數據流可接受窗口大小需要增加"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultStreamFlow - v"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"回顧"},{"type":"text","text":":"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultStreamFlow"}]},{"type":"text","text":"在前篇"},{"type":"codeinline","content":[{"type":"text","text":"(*http2Transport).NewClientConn"}]},{"type":"text","text":"方法和"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).newStream"}]},{"type":"text","text":"方法中均有提到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"連接剛建立時,發送"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"數據幀,告知server每個數據流的可發送窗口大小爲"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultStreamFlow"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"newStream"}]},{"type":"text","text":"時,數據流默認的可接收窗口大小爲"},{"type":"codeinline","content":[{"type":"text","text":"http2transportDefaultStreamFlow"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、將連接和數據流分別需要增加的窗口大小通過"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀告知server。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"if connAdd != 0 || streamAdd != 0 {\n cc.wmu.Lock()\n defer cc.wmu.Unlock()\n if connAdd != 0 {\n cc.fr.WriteWindowUpdate(0, http2mustUint31(connAdd))\n }\n if streamAdd != 0 {\n cc.fr.WriteWindowUpdate(cs.ID, http2mustUint31(streamAdd))\n }\n cc.bw.Flush()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上就是server向client發送數據的流控制邏輯。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":" (*http2clientStream).writeRequestBody"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前篇中"},{"type":"codeinline","content":[{"type":"text","text":"(*http2ClientConn).roundTrip"}]},{"type":"text","text":"未對"},{"type":"codeinline","content":[{"type":"text","text":"(*http2clientStream).writeRequestBody"}]},{"type":"text","text":"進行分析,下面我們看看該方法的源碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (cs *http2clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) (err error) {\n\tcc := cs.cc\n\tsentEnd := false // whether we sent the final DATA frame w/ END_STREAM\n // 此處省略代碼\n\treq := cs.req\n\thasTrailers := req.Trailer != nil\n\tremainLen := http2actualContentLength(req)\n\thasContentLen := remainLen != -1\n\n\tvar sawEOF bool\n\tfor !sawEOF {\n\t\tn, err := body.Read(buf[:len(buf)-1])\n // 此處省略代碼\n\t\tremain := buf[:n]\n\t\tfor len(remain) > 0 && err == nil {\n\t\t\tvar allowed int32\n\t\t\tallowed, err = cs.awaitFlowControl(len(remain))\n\t\t\tswitch {\n\t\t\tcase err == http2errStopReqBodyWrite:\n\t\t\t\treturn err\n\t\t\tcase err == http2errStopReqBodyWriteAndCancel:\n\t\t\t\tcc.writeStreamReset(cs.ID, http2ErrCodeCancel, nil)\n\t\t\t\treturn err\n\t\t\tcase err != nil:\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcc.wmu.Lock()\n\t\t\tdata := remain[:allowed]\n\t\t\tremain = remain[allowed:]\n\t\t\tsentEnd = sawEOF && len(remain) == 0 && !hasTrailers\n\t\t\terr = cc.fr.WriteData(cs.ID, sentEnd, data)\n\t\t\tif err == nil {\n\t\t\t\terr = cc.bw.Flush()\n\t\t\t}\n\t\t\tcc.wmu.Unlock()\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n // 此處省略代碼\n\treturn err\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的邏輯可簡單總結爲:不停的讀取請求body然後將讀取的內容通過"},{"type":"codeinline","content":[{"type":"text","text":" cc.fr.WriteData"}]},{"type":"text","text":"轉爲"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameData"}]},{"type":"text","text":"數據幀發送給server,直到請求body讀完爲止。其中和流控制有關的方法是"},{"type":"codeinline","content":[{"type":"text","text":"awaitFlowControl"}]},{"type":"text","text":",下面我們對該方法進行分析。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":" (*http2clientStream).awaitFlowControl"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此方法的主要作用是等待當前數據流可寫窗口有容量能夠寫入數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (cs *http2clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) {\n\tcc := cs.cc\n\tcc.mu.Lock()\n\tdefer cc.mu.Unlock()\n\tfor {\n\t\tif cc.closed {\n\t\t\treturn 0, http2errClientConnClosed\n\t\t}\n\t\tif cs.stopReqBody != nil {\n\t\t\treturn 0, cs.stopReqBody\n\t\t}\n\t\tif err := cs.checkResetOrDone(); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif a := cs.flow.available(); a > 0 {\n\t\t\ttake := a\n\t\t\tif int(take) > maxBytes {\n\n\t\t\t\ttake = int32(maxBytes) // can't truncate int; take is int32\n\t\t\t}\n\t\t\tif take > int32(cc.maxFrameSize) {\n\t\t\t\ttake = int32(cc.maxFrameSize)\n\t\t\t}\n\t\t\tcs.flow.take(take)\n\t\t\treturn take, nil\n\t\t}\n\t\tcc.cond.Wait()\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據源碼可以知道,數據流被關閉或者停止發送請求body,則當前數據流無法寫入數據。當數據流狀態正常時,又分爲兩種情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"當前數據流可寫窗口剩餘可寫數據大於0,則計算可寫字節數,並將當前數據流可寫窗口大小消耗"},{"type":"codeinline","content":[{"type":"text","text":"take"}]},{"type":"text","text":"。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"當前數據流可寫窗口剩餘可寫數據小於等於0,則會一直等待直到被喚醒並進入下一次檢查。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的第二種情況在"},{"type":"text","marks":[{"type":"strong"}],"text":"收到http2WindowUpdateFrame數據幀"},{"type":"text","text":"這一節中提到過。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"server讀取當前數據流的數據後會向client對應數據流發送"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀,client收到該數據幀後會增大對應數據流可寫窗口,並執行"},{"type":"codeinline","content":[{"type":"text","text":"cc.cond.Broadcast()"}]},{"type":"text","text":"喚醒因發送數據已達流控制上限而等待的數據流繼續發送數據。 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上就是client向server發送數據的流控制邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"幀頭長度爲9個字節,幷包含四個部分:Payload的長度、幀類型、幀標識符和數據流ID。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"流控制可分爲兩個步驟:"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始時,通過"},{"type":"codeinline","content":[{"type":"text","text":"http2FrameSettings"}]},{"type":"text","text":"數據幀和"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀告知對方當前連接讀寫窗口大小以及連接中數據流讀寫窗口大小。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在讀寫數據過程中,通過發送"},{"type":"codeinline","content":[{"type":"text","text":"http2WindowUpdateFrame"}]},{"type":"text","text":"數據幀控制另一端的寫窗口大小。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"預告"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前篇和中篇已經完成,下一期將對"},{"type":"text","marks":[{"type":"strong"}],"text":"http2.0標頭壓縮"},{"type":"text","text":"進行分析。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,衷心希望本文能夠對各位讀者有一定的幫助。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"注"},{"type":"text","text":":寫本文時, 筆者所用go版本爲: go1.14.2"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章