日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 人文社科 > 生活经验 >内容正文

生活经验

后端如何发出请求_gRPC系列(三) 如何借助HTTP2实现传输

發(fā)布時(shí)間:2023/11/27 生活经验 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 后端如何发出请求_gRPC系列(三) 如何借助HTTP2实现传输 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

本系列分為四大部分:

  • gRPC系列(一) 什么是RPC?
  • gRPC系列(二) 如何用Protobuf組織內(nèi)容
  • gRPC系列(三) 如何借助HTTP2實(shí)現(xiàn)傳輸
  • gRPC系列(四) 框架如何賦能分布式系統(tǒng) (盡情期待)

回顧

在系列二中,我們一起學(xué)習(xí)了gRPC如何使用Protobuf來組織數(shù)據(jù),達(dá)到高效編解碼、高壓縮率的目標(biāo)。本文我們將更進(jìn)一步,看看這些數(shù)據(jù)是如何在網(wǎng)絡(luò)中被傳輸?shù)?#xff0c;達(dá)到以更低的資源實(shí)現(xiàn)更高效傳輸?shù)哪繕?biāo)。內(nèi)容將圍繞以下幾點(diǎn)展開:

  • HTTP2 要解決的問題,HTTP1.1的缺點(diǎn)
  • HTTP2 的原理,它是如何降低傳輸成本,借此我們更深入理解何為二進(jìn)制編碼;同時(shí)它是如何提高網(wǎng)絡(luò)資源利用效率,重溫多路復(fù)用的思想
  • 拉通Protobuf和HTTP2,通過抓包,從數(shù)據(jù)和協(xié)議角度洞悉gRPC調(diào)用

網(wǎng)絡(luò)傳輸?shù)哪繕?biāo)

數(shù)據(jù)的傳輸,都是被切割成一個(gè)個(gè)小塊,包在層層網(wǎng)絡(luò)協(xié)議頭里,通過一個(gè)個(gè)路由器依次轉(zhuǎn)發(fā),最終到達(dá)目的地,被重新組裝起來。這是網(wǎng)絡(luò)傳輸?shù)幕驹?#xff0c;在這個(gè)過程中,有兩個(gè)亙古不變的目標(biāo):

  • 更快的傳輸。快的背后就是少,傳輸?shù)臄?shù)據(jù)越少、越小,整體的速度也就越快。
  • 更低的資源消耗。這背后是資源的高效利用,就像cpu那樣,壓榨的越厲害,就越節(jié)約資源。

隨著行業(yè)的發(fā)展,對(duì)上述兩個(gè)目標(biāo)的追求也更加極致,要想傳輸速度更快,傳輸?shù)臄?shù)據(jù)體積要小。數(shù)據(jù)體積拆開來看,有兩個(gè)部分:

  • 請(qǐng)求本身的數(shù)據(jù)。這是系列(二) 中討論的核心,用Protobuf實(shí)現(xiàn)極致的壓縮,感興趣可以回看
  • 協(xié)議本身的消耗。協(xié)議需要自我表達(dá),這會(huì)消耗一部分空間。這是本文的重點(diǎn),會(huì)討論HTTP2如何降低HTTP1.1在協(xié)議上的消耗

HTTP1.1 被視為差生

HTTP1.1以其簡(jiǎn)單、可讀性高、超高普及率、歷史悠久,作為經(jīng)典的存在,為互聯(lián)網(wǎng)的普及做出了重要貢獻(xiàn)。但在當(dāng)今超高的流量、超高的使用頻率背景下,打開一個(gè)頁(yè)面動(dòng)輒幾十個(gè)請(qǐng)求,使得速度已經(jīng)難以滿足貪婪人類的需求。這主要表現(xiàn)在以下幾個(gè)方面:

一、 冗余文本過多,導(dǎo)致傳輸體積很大
作為一款經(jīng)典的無(wú)狀態(tài)協(xié)議,它使得Web后端可以靈活地轉(zhuǎn)發(fā)、橫向擴(kuò)展,但其代價(jià)是每個(gè)請(qǐng)求都會(huì)帶上冗余重復(fù)的Header,這些文本內(nèi)容會(huì)消耗很多空間,和更快傳輸的目標(biāo)相左。

二、 并發(fā)能力差,網(wǎng)絡(luò)資源利用率低
HTTP1.1 是基于文本的協(xié)議,請(qǐng)求的內(nèi)容打包在header/body中,內(nèi)容通過rn來分割,同一個(gè)TCP連接中,無(wú)法區(qū)分request/response是屬于哪個(gè)請(qǐng)求,所以無(wú)法通過一個(gè)TCP連接并發(fā)地發(fā)送多個(gè)請(qǐng)求,只能等上一個(gè)請(qǐng)求的response回來了,才能發(fā)送下一個(gè)請(qǐng)求,否則無(wú)法區(qū)分誰(shuí)是誰(shuí)。

于是H1.1提出了一個(gè)pipeline的特性,允許請(qǐng)求方一口氣并發(fā)多個(gè)request,但對(duì)服務(wù)方有一個(gè)變態(tài)的要求,需要對(duì)應(yīng)的response按照request的順序嚴(yán)格排列,因?yàn)椴话错樞蚺帕芯头植磺宄esponse是屬于哪個(gè)request的。這給Proxy(Nginx等)帶來了復(fù)雜性,同時(shí)如果第一個(gè)請(qǐng)求遲遲不返回,那后面的請(qǐng)求都會(huì)受影響,所以普及率不高。

但當(dāng)今的Web頁(yè)面有玲瑯滿目的圖片、js、css,如果讓請(qǐng)求一個(gè)個(gè)串行執(zhí)行,那頁(yè)面的渲染會(huì)變得極慢。于是只能同時(shí)創(chuàng)建多個(gè)TCP連接,實(shí)現(xiàn)并發(fā)下載數(shù)據(jù),快速渲染出頁(yè)面。這會(huì)給瀏覽器造成較大的資源消耗,電腦會(huì)變卡。很多瀏覽器為了兼顧下載速度和資源消耗,會(huì)對(duì)同一個(gè)域名限制并發(fā)的TCP連接數(shù)量,如Chrome是6個(gè)左右,剩下的請(qǐng)求則需要排隊(duì),Network下的Waterfall就可以觀察排隊(duì)情況 (見下圖右邊的顏色條)。

H1.1時(shí),有6個(gè)并發(fā)連接,可以看到最下面三個(gè)請(qǐng)求在排隊(duì):

圖片淶源[5]

HTTP2中,可以看出請(qǐng)求時(shí)同時(shí)發(fā)出的,沒有排隊(duì),且只占用一個(gè)連接:

圖片淶源[5]

狡猾的人類為了避開這個(gè)數(shù)量限制,將圖片、css、js等資源放在不同域名下(或二級(jí)域名),避開排隊(duì)導(dǎo)致的渲染延遲。快速下載的目標(biāo)實(shí)現(xiàn)了,但這和更低的資源消耗目標(biāo)相違背,背后都是高昂的帶寬、CDN成本。

HTTP2 當(dāng)救世主

H1.1 在速度和成本上的權(quán)衡讓人糾結(jié)不已,HTTP2的出現(xiàn)就是為了優(yōu)化這些問題,在更快的傳輸更低的成本兩個(gè)目標(biāo)上更進(jìn)了一步。有以下幾個(gè)基本點(diǎn):

  • HTTP2 未改變HTTP的語(yǔ)義(如GET/POST等),只是在傳輸上做了優(yōu)化
  • 引入幀、流的概念,在TCP連接中,可以區(qū)分出多個(gè)request/response
  • 一個(gè)域名只會(huì)有一個(gè)TCP連接,借助幀、流可以實(shí)現(xiàn)多路復(fù)用,降低資源消耗
  • 引入二進(jìn)制編碼,降低header帶來的空間占用

核心可分為 頭部壓縮多路復(fù)用。這兩個(gè)點(diǎn)都服務(wù)于更快的傳輸更低的資源消耗這兩個(gè)目標(biāo),與上文呼應(yīng)。

頭部壓縮

現(xiàn)在的web頁(yè)面,大多比較復(fù)雜,新打開一個(gè)地址,動(dòng)輒產(chǎn)生幾十個(gè)請(qǐng)求,這會(huì)發(fā)送大量的header,大部分內(nèi)容都是一樣的內(nèi)容,以baidu為例:

request:
GET  HTTP/1.1
Host: www.baidu.com
Cache-Control: no-cache
Postman-Token: a9702bac-94c4-c7da-2041-7c7ac5f85b6eresponse:
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=UTF-8
Server: Apache
Transfer-Encoding: chunked
Vary: Accept-Encoding

這些文本內(nèi)容一次次重復(fù)地發(fā)送,占用了大量的帶寬,如何將這些成本降下去,而又保留HTTP無(wú)狀態(tài)的優(yōu)點(diǎn)呢?

基于這個(gè)想法,誕生了HPACK[2],全稱為HTTP2頭部壓縮,它以極富創(chuàng)造力的方式,提供了兩種方式極大地降低了header的傳輸占用。

一、將高頻使用的Header編成一個(gè)靜態(tài)表,每個(gè)header對(duì)應(yīng)一個(gè)數(shù)組索引,每次只用傳這個(gè)索引,而不是冗長(zhǎng)的文本。表總共有61項(xiàng),下圖是前30項(xiàng):

圖片來源[3]
  • 傳 3 代表 "POST",這用一個(gè)字節(jié)表示了原來4個(gè)字節(jié)
  • 傳28代表content-length,這用一個(gè)字節(jié)表示了原來14個(gè)字節(jié)(value下文會(huì)討論)

可以預(yù)見這種方式,在大量的請(qǐng)求環(huán)境下,可以明顯降低傳輸內(nèi)容。服務(wù)端根據(jù)內(nèi)容查表,就可以還原出header。

二、支持動(dòng)態(tài)地在表中增加header

Host: www.baidu.com

在上面的實(shí)例中,打開baidu時(shí),對(duì)應(yīng)域名的所有請(qǐng)求都會(huì)帶上Host,這又是重復(fù)冗余的數(shù)據(jù)。但由于各家網(wǎng)站的host不相同,無(wú)法像上面那樣做成一個(gè)靜態(tài)的表。HPACK支持動(dòng)態(tài)地在表中增加header,例如:

62   Host: www.baidu.com

在請(qǐng)求發(fā)起前,通過協(xié)議將上面Header添加到表中,則后面的請(qǐng)求都只用發(fā)送62即可,不用再發(fā)送文本,這又節(jié)約了大量空間。(請(qǐng)求方/服務(wù)方的表成員會(huì)保持同步一致)

上面兩個(gè)分別被成為靜態(tài)表和動(dòng)態(tài)表。靜態(tài)表是協(xié)議級(jí)別的約定,是不變的內(nèi)容。動(dòng)態(tài)表則是基于當(dāng)前TCP連接進(jìn)行協(xié)商的結(jié)果,發(fā)送請(qǐng)求時(shí)會(huì)相互設(shè)置好header,讓請(qǐng)求方和服務(wù)方維護(hù)同一份動(dòng)態(tài)表,后續(xù)的請(qǐng)求可復(fù)用。連接銷毀時(shí),動(dòng)態(tài)表也會(huì)注銷。

多路復(fù)用

H1.1核心的尷尬點(diǎn)在于,在同一個(gè)TCP連接中,沒辦法區(qū)分response是屬于哪個(gè)請(qǐng)求,一旦多個(gè)請(qǐng)求返回的文本內(nèi)容混在一起,就天下大亂,所以請(qǐng)求只能一個(gè)個(gè)串行排隊(duì)發(fā)送。這直接導(dǎo)致了TCP資源的閑置。

HTTP2為了解決這個(gè)問題,提出了的概念,每一次請(qǐng)求對(duì)應(yīng)一個(gè)流,有一個(gè)唯一ID,用來區(qū)分不同的請(qǐng)求。基于流的概念,進(jìn)一步提出了,一個(gè)請(qǐng)求的數(shù)據(jù)會(huì)被分成多個(gè)幀,方便進(jìn)行數(shù)據(jù)分割傳輸,每個(gè)幀都唯一屬于某一個(gè)流ID,將幀按照流ID進(jìn)行分組,即可分離出不同的請(qǐng)求。這樣同一個(gè)TCP連接中就可以同時(shí)并發(fā)多個(gè)請(qǐng)求,不同請(qǐng)求的幀數(shù)據(jù)可穿插在一起,根據(jù)流ID分組即可。這樣直接解決了H1.1的核心痛點(diǎn),通過這種復(fù)用TCP連接的方式,不用再同時(shí)建多個(gè)連接,提升了TCP的利用效率。 這也是多路復(fù)用思想的一種落地方式,在很多消息隊(duì)列協(xié)議中也廣泛存在,如AMQP[4],其channel的概念和如出一轍,大道相通。

在HTTP2中,流是一個(gè)邏輯上的概念,實(shí)際上就是一個(gè)int類型的ID,可順序自增,只要不沖突即可,每條數(shù)據(jù)都會(huì)攜帶一個(gè)流ID,當(dāng)一串串幀在TCP通道中傳輸時(shí),通過其流ID,即可區(qū)分出不同的請(qǐng)求。

幀則有更多較為復(fù)雜的作用,HTTP2幾乎所有數(shù)據(jù)交互,都是以幀為單位進(jìn)行的,包括header、body、約定配置(除了Magic串),這天然地就需要給幀進(jìn)行分類,于是協(xié)議約定了以下幀類型:

  • HEADERS:幀僅包含 HTTP header信息。
  • DATA:幀包含消息的所有或部分請(qǐng)求數(shù)據(jù)。
  • PRIORITY:指定分配給流的優(yōu)先級(jí)。服務(wù)方可先處理高優(yōu)先請(qǐng)求
  • RST_STREAM:錯(cuò)誤通知:一個(gè)推送承諾遭到拒絕。終止某個(gè)流。
  • SETTINGS:指定連接配置。(用于配置,流ID為0) [會(huì)ACK確認(rèn)收到]
  • PUSH_PROMISE:通知一個(gè)將資源推送到客戶端的意圖。
  • PING:檢測(cè)信號(hào)和往返時(shí)間。(流ID為0)[會(huì)ACK]
  • GOAWAY:停止為當(dāng)前連接生成流的停止通知。
  • WINDOW_UPDATE:用于流控制,約定發(fā)送窗口大小。
  • CONTINUATION:用于繼續(xù)傳送header片段序列。

一次HTTP2的請(qǐng)求有以下過程:

  • 通過一個(gè)或多個(gè)SETTINGS幀約定一些數(shù)據(jù)(會(huì)有ACK機(jī)制,確認(rèn)約定內(nèi)容)
  • 請(qǐng)求方通過HEADERS幀將請(qǐng)求Aheader打包發(fā)出
  • 請(qǐng)求B可穿插···
  • 請(qǐng)求方通過DATA幀將請(qǐng)求Arequest數(shù)據(jù)打包發(fā)出
  • 服務(wù)方通過HEADERS幀將請(qǐng)求Aresponse header打包發(fā)出
  • 請(qǐng)求C可穿插···
  • 服務(wù)方通過DATA幀將請(qǐng)求Aresponse數(shù)據(jù)打包發(fā)出

深入HTTP2

前文簡(jiǎn)單介紹了,頭部壓縮和多路復(fù)用的具體思路和解決問題的方法,接下來我們深入HTTP2,看看這兩個(gè)特性是如何落地的,在數(shù)據(jù)上形成直觀地把握,也借此了解何為二進(jìn)制編碼

任何一個(gè)應(yīng)用層的傳輸協(xié)議,都需要解決一個(gè)問題,那就是如何表示數(shù)據(jù)結(jié)尾,如何分割數(shù)據(jù)。在H1.1中,我們知道,它粗暴地先發(fā)Header,再發(fā)body,每個(gè)header通過rn文本內(nèi)容來分割,header和body通過rnrn來分割,通過content-length的值讀取body,一個(gè)請(qǐng)求的內(nèi)容就成功結(jié)束。

// 一次請(qǐng)求的返回
200 OKrnHeader1:Value1rnHeader2:Value2rnHeader3:Value3rnrnI am body
// 網(wǎng)絡(luò)中實(shí)際傳輸?shù)氖巧厦嫖谋镜腶scii編碼

HTTP2 為了降低協(xié)議占用,不會(huì)使用文本分割,也不會(huì)使用文本來表示header。它是如何表示一幀開始、一幀結(jié)束、header傳完了、body傳完了呢?

下面是幀格式,所有幀都是一個(gè)固定的 9 字節(jié)頭部 (payload 之前) 跟一個(gè)指定長(zhǎng)度的數(shù)據(jù)(payload):

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length 代表整個(gè)幀的長(zhǎng)度,用一個(gè) 24 位無(wú)符號(hào)整數(shù)表示。頭部的 9 字節(jié)不算在這個(gè)長(zhǎng)度里。從payload開始讀Length這么多字節(jié),一幀數(shù)據(jù)也就讀完結(jié)束。
  • Type 定義 幀 的類型,用 8 bits 表示。幀類型決定了幀的格式和語(yǔ)義,不同類型有差異
  • Flags 是為幀類型相關(guān)而預(yù)留的布爾標(biāo)識(shí)。標(biāo)識(shí)對(duì)于不同的幀類型賦予了不同的語(yǔ)義,例如下面會(huì)提到的Padding
  • R 是一個(gè)保留的比特位。這個(gè)比特的語(yǔ)義沒有定義,發(fā)送時(shí)它必須被設(shè)置為 (0x0), 接收時(shí)需要忽略。
  • Stream Identifier 唯一標(biāo)示一個(gè)流,用 31 位無(wú)符號(hào)整數(shù)表示。客戶端建立的 sid 必須為奇數(shù),服務(wù)端建立的 sid 必須為偶數(shù),值 (0x0) 保留給與整個(gè)連接相關(guān)聯(lián)的幀 (連接控制消息),而不是單個(gè)流
  • Frame Payload 是主體內(nèi)容,由幀類型決定(上面的9個(gè)字節(jié)都是協(xié)議本身的消耗,payload才是請(qǐng)求本身的主要內(nèi)容)

不同的幀類型,有不同的Payload格式,我們分別介紹DATA幀和HEADDERS幀:

DATA幀的Payload:

+---------------+|Pad Length? (8)|+---------------+-----------------------------------------------+|                            Data (*)                         ...+---------------------------------------------------------------+|                           Padding (*)                       ...+---------------------------------------------------------------+
  • Pad Length: ? 表示此字段的出現(xiàn)時(shí)有條件的,當(dāng)幀的Flags(8)的第三位為1時(shí),才有效,否則會(huì)被忽略
  • Data: 傳遞的數(shù)據(jù),其長(zhǎng)度上限等于幀的 payload 長(zhǎng)度減去其他出現(xiàn)的字段長(zhǎng)度(如果有pad的話)。在gRPC中,Data這部分內(nèi)容就是用Protobuf將數(shù)據(jù)編碼的結(jié)果
  • Padding: 填充字節(jié),沒有具體語(yǔ)義,發(fā)送時(shí)必須設(shè)為 0,作用是混淆報(bào)文長(zhǎng)度,為安全目的服務(wù)

Data幀的Flags(8)目前有兩個(gè)位有意義:

  • END_STREAM: bit 0 設(shè)為 1 代表當(dāng)前流的最后一幀,告訴接收方請(qǐng)求數(shù)據(jù)發(fā)送完畢,否則還要繼續(xù)等下一幀(接收方)
  • PADDED: bit 3 設(shè)為 1 代表存在 Padding

HEADER幀Payload:

+---------------+|Pad Length? (8)|+-+-------------+-----------------------------------------------+|E|                 Stream Dependency? (31)                     |+-+-------------+-----------------------------------------------+|  Weight? (8)  |+-+-------------+-----------------------------------------------+|                   Header Block Fragment (*)                 ...+---------------------------------------------------------------+|                           Padding (*)                       ...+---------------------------------------------------------------+
  • Pad Length: 同DATA幀
  • E: 一個(gè)比特位聲明流的依賴性是否是排他的,存在則代表 PRIORITY flag 被設(shè)置
  • Stream Dependency: 指定一個(gè) stream identifier,代表當(dāng)前流所依賴的流的 id,存在則代表 PRIORITY flag 被設(shè)置
  • Weight: 一個(gè)無(wú)符號(hào) 8 為整數(shù),代表當(dāng)前流的優(yōu)先級(jí)權(quán)重值 (1~256),存在則代表 PRIORITY flag 被設(shè)置
  • Header Block Fragment: header 塊片段,header依次打包排列在里面
  • Padding: 同DATA幀

HEADERS 幀有以下標(biāo)識(shí) (flags):

  • END_STREAM: bit 0 設(shè)為 1 代表當(dāng)前請(qǐng)求 header 發(fā)送完了(可能有CONTINUATION幀,可以認(rèn)為是HEADERS的一部分)
  • END_HEADERS: bit 2 設(shè)為 1 代表 header 塊結(jié)束
  • PADDED: bit 3 設(shè)為 1 代表 Pad 被設(shè)置,存在 Pad Length 和 Padding
  • PRIORITY: bit 5 設(shè)為 1 表示存在 Exclusive Flag (E), Stream Dependency, 和 Weight

請(qǐng)求的header打包在Header Block Fragment ,我們重點(diǎn)關(guān)注一下,以便理解header是如何被傳輸?shù)摹?/p>

由于上面頭部壓縮的內(nèi)容,我們知道header可以存在于靜態(tài)表、動(dòng)態(tài)表中。此時(shí)只需要傳一個(gè)index即可表達(dá)對(duì)應(yīng)的header,減少傳輸內(nèi)容。 請(qǐng)求傳遞的header情況有以下幾種:

  • header 的key、value 在靜態(tài)表/動(dòng)態(tài)表中,此時(shí)只需要傳遞一個(gè)index即可
  • header 的key 在靜態(tài)、動(dòng)態(tài)表中,而value由于多種多樣,不在表中(如Host),此時(shí)key可以由index表示,但value需要傳遞原內(nèi)容
  • header 的key、value完全不在靜態(tài)、動(dòng)態(tài)表中,key、value都需要傳遞原內(nèi)容(字符串)
  • 希望將本次傳遞的header寫入動(dòng)態(tài)表中,下次只需要傳index 即可
  • 不希望本次傳遞的header寫入動(dòng)態(tài)表中

Header Block Fragment 中打包header的方式也就是按照上面幾種情況展開,具體篇幅較多,本文找一個(gè)復(fù)雜點(diǎn)的例子: key、value都不在表中,且需要添加進(jìn)表中的情況進(jìn)行舉例:(更詳細(xì)HPACK細(xì)節(jié)可見[6]、[7])

+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |           // 通過頭8個(gè)bit表示是哪種case
+---+---+-----------------------+
| H |     Key Length (7+)      |
+---+---------------------------+
|  Key String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
  • 頭8個(gè)bit中01 000000 表達(dá)了兩點(diǎn)
  1. header的key不在表中(000000)、value也不在(需要傳文本內(nèi)容)
  2. 希望將此header追加到動(dòng)態(tài)表中,供下次使用(01開頭表示需要追加到表中)
  • Value Length 代表對(duì)應(yīng)value的長(zhǎng)度,借此可讀取完整的Value String
  • 其余的情況都可以用頭8個(gè)bit表示[7]
  • 多個(gè)上面的結(jié)構(gòu)前后拼接在一起,就可以在一個(gè)HEADERS幀中表示多個(gè)header了
  • 第二行H為1表示value用了霍夫曼編碼[9],可以理解為一種文本壓縮策略

上面的場(chǎng)景下,header的內(nèi)容包含key的Index,value的長(zhǎng)度、value的文本內(nèi)容,其實(shí)可分為兩種:

  • 數(shù)字的表達(dá)。 key的Index、key/value文本內(nèi)容的長(zhǎng)度
  • 字符串的表達(dá)。key的內(nèi)容(如custom-key)、value的內(nèi)容(custom-value)

對(duì)于 custom-key: custom-header表達(dá)示例:(來源[10])

編碼數(shù)據(jù)的十六進(jìn)制表示:
400a 6375 7374 6f6d 2d6b 6579 0d63 7573 | @.custom-key.cus
746f 6d2d 6865 6164 6572                | tom-header解碼過程:40                               | == Literal indexed ==   (01000000表示要追加到表中)
0a                               |   Literal name (len = 10) (得到key長(zhǎng)度)
6375 7374 6f6d 2d6b 6579         | custom-key         
0d                               |   Literal value (len = 13) (得到value長(zhǎng)度)
6375 7374 6f6d 2d68 6561 6465 72 | custom-header           (一個(gè)key:value 讀取完畢)解碼結(jié)果可得header:     custom-key:custom-header  
并將其加入動(dòng)態(tài)表,下次直接只傳index     

上圖中有Key Length (7+)Value Length (7+),這是上面提到的數(shù)字的表達(dá),可以看到有7+這個(gè)表示。這里面有一個(gè)擴(kuò)展問題,如果Value的長(zhǎng)度比較大,7個(gè)bit表示不了咋辦。

0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| ? | ? | ? | 1   1   1   1   1 |       第一個(gè)字節(jié)  N = 5
+---+---+---+-------------------+
| 1 |    Value-(2^N-1) LSB      |
+---+---------------------------+...
+---+---------------------------+
| 0 |    Value-(2^N-1) MSB      |
+---+---------------------------+

當(dāng)長(zhǎng)度len比較小,len < 2^N - 1, 則直接用第一個(gè)字節(jié)即可表達(dá)。(N <= 7)。 如果len >= 2^N - 1,則需要用后續(xù)的字節(jié)繼續(xù)表達(dá)。規(guī)則是:

  • 選擇一個(gè)N,如上面N=5,將第一個(gè)字節(jié)的后N位全部設(shè)為1,則第一個(gè)字節(jié)表達(dá)了 2^N - 1, 剩下的len - (2^N - 1) 用后面的字節(jié)表示。
  • 將len - (2^N - 1) 用二進(jìn)制表示出來,將二進(jìn)制位分別分給下面的字節(jié)
  • 只占用后面字節(jié)的后7位
  • 如果第一位為0,則表示表達(dá)完畢,為1 則表示下一個(gè)字節(jié)還在繼續(xù)表示len

示例: (來源[10])

  • 表達(dá)長(zhǎng)度為: 1337,設(shè)N = 5
  • 1337 大于 31(2^5-1),并使用 5 位前綴表示。5 位前綴使用其最大值(31)填充
  • 除第一個(gè)字節(jié)外,后面字節(jié)表達(dá) 1337 - 31 = 1306
  • 1036 二進(jìn)制串為: 010100011010,用多個(gè)字節(jié)表達(dá)
0   1   2   3   4   5   6   7+---+---+---+---+---+---+---+---+| X | X | X | 1 | 1 | 1 | 1 | 1 |  第一個(gè)字節(jié)表達(dá)了 2^N - 1 = 31, 下面的字節(jié)表達(dá) 1337 - 31 = 1306| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |   后面一截: 0011010   (低位)| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |   前面一截: 01010      (高位)+---+---+---+---+---+---+---+---+

gRPC 請(qǐng)求抓包

上文已經(jīng)搞清楚了HTTP2的傳輸原理,接下來通過wireshark透視一下gRPC調(diào)用的過程。

請(qǐng)求內(nèi)容:

請(qǐng)求抓包


請(qǐng)求返回:

返回抓包

幀示例:

header幀抓包


先約定配置,SETTINGS幀有ACK表達(dá)確認(rèn)

  • 請(qǐng)求的Method在header中傳遞
  • 參數(shù)用DATA幀
  • 返回狀態(tài)用HEADER幀
  • 返回?cái)?shù)據(jù)用DATA幀

可見調(diào)用語(yǔ)義和HTTP并無(wú)差別,但通過協(xié)議優(yōu)化,在很大程度上降低了傳輸?shù)捏w積,節(jié)省資源的同時(shí),也較好地提升了性能。

看了單個(gè)請(qǐng)求的抓包樣例,我們得再看看gRPC的stream是什么鬼,代碼約定如下:

// proto
service XXX {rpc StreamTest(stream StreamTestReq) returns (stream StreamTestResp);
}
message StreamTestReq {int64 i = 1;
}
message StreamTestResp {int64 j = 1;
}
// server端代碼
func (s *XXXService) StreamTest(re v1pb.XXX_StreamTestServer ) (err error) {for {data, err := re.Recv()if err != nil {break}// 將客戶端發(fā)送來的值乘以10再返回給它err = re.Send(&v1pb.StreamTestResp{J: data.I * 10 }) }return
}
// client 端代碼
func TestStream(t *testing.T) {c, _ := service2.daClient.StreamTest(context.TODO())go func(){for {rec, err := c.Recv()if err != nil {break}fmt.Printf("resp: %vn", rec.J)}}()for _, x := range []int64{1,2,3,4,5,6,7,8,9}{_ = c.Send(&dav1.StreamTestReq{I: x})time.Sleep(100*time.Millisecond)}_ = c.CloseSend()
}
// client端輸出結(jié)果
resp: 10
resp: 20
resp: 30
resp: 40
resp: 50
resp: 60
resp: 70
resp: 80
resp: 90
  • 上面是一個(gè)雙向stream流
  • client和server端同時(shí)在收發(fā)數(shù)據(jù)
  • client連續(xù)發(fā)送9次后,中斷過程。常規(guī)的流式服務(wù),如視頻編解碼,可以一直持續(xù)直到結(jié)束
  • 服務(wù)端將client的參數(shù)*10后返回

我們不禁要問,這種流式請(qǐng)求和常規(guī)的gRPC有沒有區(qū)別? 這從抓包便可知分曉:

stream

上面只提取了http2 和grpc的協(xié)議內(nèi)容,否則會(huì)被tcp的ack打亂視野,可以從圖上看到:

  • 請(qǐng)求的method只發(fā)送了一次
  • 服務(wù)端的回復(fù)header也只返回了一次(200 OK 那行)
  • 剩下的就是: client的data幀和server 端的data幀交替
  • 其實(shí)全場(chǎng)就只有一次請(qǐng)求(stream ID 未變化)

stream模式,其實(shí)就是gRPC從協(xié)議層支持了,在一次長(zhǎng)請(qǐng)求中,分批地處理小量數(shù)據(jù),達(dá)到多次請(qǐng)求的效果,像流水一樣可以延綿不絕,直到某一方終止。

試想下,如果gRPC內(nèi)部不支持這種模式,其實(shí)也能自己實(shí)現(xiàn)流式的服務(wù),只不過在形式上要多調(diào)用幾次接口而已。 從上面抓包來看,這種封裝在無(wú)論在性能和語(yǔ)義上都更好。

進(jìn)一步提升

參見HTTP3,拋棄TCP協(xié)議,擁抱QUIC。

參考資料

  • [1] https://juejin.im/post/5b88a4f56fb9a01a0b31a67e
  • [2] https://blog.csdn.net/u010129119/article/details/79392545
  • [3] https://tools.ietf.org/html/rfc7541#section-2.3.1
  • [4] https://www.rabbitmq.com/tutorials/amqp-concepts.html
  • [5]https://zhuanlan.zhihu.com/p/34662800
  • [6] https://http2.github.io/http2-spec/compression.html#index.address.space
  • [7] https://github.com/halfrost/Halfrost-Field/blob/master/contents/Protocol/HTTP:2_Header-Compression.md
  • [8] https://zhuanlan.zhihu.com/p/149821222
  • [9]https://zh.wikipedia.org/zh-hans/%E9%9C%8D%E5%A4%AB%E6%9B%BC%E7%BC%96%E7%A0%81
  • [10] https://github.com/halfrost/Halfrost-Field/blob/master/contents/Protocol/HTTP:2_HPACK-Example.md#1-%E6%95%B4%E6%95%B0%E8%A1%A8%E7%A4%BA%E7%9A%84%E7%A4%BA%E4%BE%8B

總結(jié)

以上是生活随笔為你收集整理的后端如何发出请求_gRPC系列(三) 如何借助HTTP2实现传输的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。