GRPC 的字節(jié)結(jié)構(gòu)觀察

本文基于以下版本:

github.com/golang/protobuf: v1.3.2
google.golang.org/grpc: v1.25.1
nginx: openresty v1.15.8.2

1. 非加密非流式

本節(jié)主要進(jìn)行非加密非流式 GRPC 的通信在字節(jié)層面的討論,假設(shè)讀者對(duì) GRPC、HTTP/2 等已有基本的了解。
本節(jié)使用一個(gè)簡(jiǎn)單的 proto:

syntax = "proto3";

package pb;

service Hot {
  rpc Inc (IntReq) returns (IntResp);
}

message IntReq {
  int32 i = 1;
}

message IntResp {
  int32 i = 1;
}

以及如下的 golang 服務(wù)端代碼:

package main

import (
    "context"
    "net"
    "os"

    "grpc_hot/pb"

    "google.golang.org/grpc"
)

type HotService struct{}

func (svc *HotService) Inc(_ context.Context, req *pb.IntReq) (*pb.IntResp, error) {
    return &pb.IntResp{I: req.GetI() + 1}, nil
}

func main() {
    port := "30080"
    if len(os.Args) >= 2 {
        port = os.Args[1]
    }

    srv := grpc.NewServer()
    pb.RegisterHotServer(srv, &HotService{})
    l, err := net.Listen("tcp", ":"+port)
    if nil != err {
        println(err.Error())
        return
    }
    srv.Serve(l)
}

和客戶(hù)端代碼:

package main

import (
    "context"
    "os"

    "grpc_hot/pb"

    "google.golang.org/grpc"
)

func main() {
    port := "30080"
    if len(os.Args) >= 2 {
        port = os.Args[1]
    }

    conn, err := grpc.Dial("127.0.0.1:"+port, grpc.WithInsecure())
    if nil != err {
        println(err.Error())
        return
    }
    defer conn.Close()
    cli := pb.NewHotClient(conn)
    resp, err := cli.Inc(context.Background(), &pb.IntReq{I: 6})
    if nil != err {
        println(err.Error())
        return
    }
    println("resp:", resp.GetI())
}

1.1. HTTP/2

啟動(dòng)上述 golang 的服務(wù)端,調(diào)用一次客戶(hù)端,均使用默認(rèn)端口。使用 wireshark 抓包,總共抓到 19 幀。除去那些不包含 TCP 荷載的幀,我們首先逐幀來(lái)看看它們?cè)?HTTP/2 這一層長(zhǎng)什么亞子。

frame side TCP payload
04 client 50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a
0d 0a 53 4d 0d 0a 0d 0a
06 client 00 00 00 04 00 00 00 00 00
07 server 00 00 06 04 00 00 00 00 00 00 05 00 00 40 00
09 server 00 00 00 04 01 00 00 00 00
11 client 00 00 00 04 01 00 00 00 00
12 client 00 00 38 01 04 00 00 00 01 83 86 45 89 62 b8 d7
c6 74 b1 92 a2 7f 41 85 b8 c8 00 f0 7f 5f 8b 1d
75 d0 62 0d 26 3d 4c 4d 65 64 7a 8a 9a ca c8 b4
c7 60 2b 89 b5 c3 40 02 74 65 86 4d 83 35 05 b1
1f 00 00 07 00 01 00 00 00 01 00 00 00 00 02 08
06
14 server 00 00 04 08 00 00 00 00 00 00 00 00 07 00 00 08
06 00 00 00 00 00 02 04 10 10 09 0e 07 07
15 client 00 00 08 06 01 00 00 00 00 02 04 10 10 09 0e 07
07
16 server 00 00 0e 01 04 00 00 00 01 88 5f 8b 1d 75 d0 62
0d 26 3d 4c 4d 65 64 00 00 07 00 00 00 00 00 01
00 00 00 00 02 08 07 00 00 18 01 05 00 00 00 01
40 88 9a ca c8 b2 12 34 da 8f 01 30 40 89 9a ca
c8 b5 25 42 07 31 7f 00
17 client 00 00 04 08 00 00 00 00 00 00 00 00 07 00 00 08
06 00 00 00 00 00 02 04 10 10 09 0e 07 07
18 server 00 00 08 06 01 00 00 00 00 02 04 10 10 09 0e 07
07

除第 4 幀外,HTTP 層的結(jié)構(gòu)均如下:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length:荷載的字節(jié)數(shù),注意是 HTTP 的荷載,不是 TCP 的荷載
  • Type:HTTP 幀的類(lèi)型
frame type code
DATA 0x0
HEADERS 0x1
PRIORITY 0x2
RST_STREAM 0x3
SETTINGS 0x4
PUSH_PROMISE 0x5
PING 0x6
GOAWAY 0x7
WINDOW_UPDATE 0x8
CONTINUATION 0x9
  • Flags:不同類(lèi)型的幀具有不同的 flag 定義

1.1.1. 連接

第 4 幀用許多語(yǔ)言都表示為這樣:

"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

客戶(hù)端通過(guò)這樣一幀去試探服務(wù)端是否支持 HTTP/2。
接下來(lái)第 6、7、9、11 幀,兩端相互請(qǐng)求 SETTINGS
SETTINGS 幀的荷載為零到多組鍵值對(duì),每組鍵值對(duì)的結(jié)構(gòu)為 2 字節(jié)的 id 和 4 字節(jié)的值。如第 7 幀包含一組鍵值對(duì),id 為 00 05,值為 00 00 40 00。
id 和值的定義見(jiàn) RFC-7540, section 6.5.2.

1.1.2. 首部

第 12 幀,客戶(hù)端向服務(wù)端發(fā)送 HTTP 請(qǐng)求的首部。
HEADERS 幀的荷載結(jié)構(gòu)如下:

+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E|                 Stream Dependency? (31)                     |
+-+-------------+-----------------------------------------------+
|  Weight? (8)  |
+-+-------------+-----------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

本文的情況中,HEADERS 的幀荷載只有 Header Block Fragment 字段存在。其他字段的定義見(jiàn) RFC-7540, section 6.2.
Fragment 的編解碼使用 HPACK 算法(RFC-7541),包括霍夫曼編碼。我們可以使用 Golang 的副標(biāo)準(zhǔn)庫(kù)當(dāng)中的封裝來(lái)解碼第 12 幀的 fragment。

import "golang.org/x/net/http2/hpack"

func decodeHeaders(bs []byte) {
    d := hpack.NewDecoder(128, nil)
    hdrs, _ := d.DecodeFull(bs)
    for _, hdr := range hdrs {
        println(hdr.Name, hdr.Value)
    }
}

其中傳入的字節(jié)序列長(zhǎng)度為幀的 Length 字段指示的 0x38,但可以看到幀荷載的實(shí)際長(zhǎng)度不止 0x38,后面剩余的 16 個(gè)字節(jié)應(yīng)該是在 HTTP 層的一個(gè)后續(xù)幀在粘包,先不管。這里打印出的 header 如下:

:method POST
:scheme http
:path /pb.Hot/Inc
:authority :30081
content-type application/grpc
user-agent grpc-go/1.25.1
te trailers

可以看到這里的首部還包含 HTTP/1.x 中的 method 和 path,由于使用了靜態(tài)索引表和霍夫曼編碼,實(shí)際傳輸?shù)氖撞恐挥?56 字節(jié),通信精簡(jiǎn)的效果很明顯。
同樣,第 16 幀服務(wù)端發(fā)送的 HEADERS 幀,從長(zhǎng)度上看也包含后續(xù)幀,首部解碼出來(lái)如下:

:status 200
content-type application/grpc

神奇的是整個(gè)過(guò)程中沒(méi)有一個(gè) DATA 幀,那么 GRPC 使用的 HTTP body 在哪里呢,我猜你也猜到了。

1.2. GRPC

1.2.1. 請(qǐng)求

在第 12 幀的 HTTP 首部里可以看到,對(duì)于 GRPC 調(diào)用的請(qǐng)求,method 始終是 POST,路徑是 /{包名}.{服務(wù)名}/{方法名}。
而請(qǐng)求的數(shù)據(jù)放在這一幀的后續(xù)幀中:00 00 07 00 01 00 00 00 01 00 00 00 00 02 08 06,從第 4 個(gè)字節(jié)來(lái)看,它正是一個(gè) DATA 幀。
DATA 幀的荷載結(jié)構(gòu)對(duì) HTTP 是透明的,真正的定義在于 GRPC 這一層。GRPC 中 DATA 幀的荷載結(jié)構(gòu)如下:

+---------------+
| Compressed(8) |
+---------------+-----------------------------------------------+
|                          Length (32)                          |
+---------------------------------------------------------------+
|                           Data (*)                          ...
+---------------------------------------------------------------+
  • CompressedData 字段是否被壓縮,0 為未壓縮,1 為壓縮
    ,此時(shí)壓縮的算法會(huì)標(biāo)記在首部的 Message-Encoding 字段
  • LengthData 的字節(jié)數(shù)
  • Data:實(shí)際的數(shù)據(jù),默認(rèn)為 ProtoBuf 編碼,編碼算法見(jiàn) 這里

這里 DATA 幀的荷載是 00 00 00 00 02 08 06,表明 Data 未段未壓縮,長(zhǎng)度為 2,內(nèi)容為 08 06。

1.2.2. 響應(yīng)

和請(qǐng)求的幀相同的套路,我們可以看清第 16 幀中的響應(yīng)數(shù)據(jù)。不過(guò)在這個(gè)邏輯上的 DATA 幀后面還有一個(gè) HAEDERS 幀,解碼出來(lái)是這樣:

grpc-status 0
grpc-message

至此,我們已基本看清一個(gè)非加密非流式的最簡(jiǎn)單情況下的 GRPC 請(qǐng)求在字節(jié)層面的樣子。

1.3. Nginx 代理

下一節(jié)我們將會(huì)通過(guò)使用帶 TLS 的 Nginx 代理非加密 GRPC 節(jié)點(diǎn),來(lái)討論帶 TLS 的 GRPC 協(xié)議。所以這里先給出一個(gè)簡(jiǎn)單的非加密 Nginx 代理非加密 GRPC 節(jié)點(diǎn)的 Nginx 配置,包括負(fù)載均衡。
我們啟動(dòng)兩個(gè) golang 的服務(wù)端節(jié)點(diǎn),端口分別為 3008130082。在 Nginx 配置文件的 http 段中加入:

upstream grpc_hot {
    server 127.0.0.1:30081;
    server 127.0.0.1:30082;
}
server {
    listen 30080 http2;
    location / {
        grpc_pass grpc://grpc_hot;
    }
}

2. 加密非流式

本節(jié)主要進(jìn)行加密非流式 GRPC 的通信在字節(jié)層面的討論,使用帶 TLSv1.2 的 nginx 節(jié)點(diǎn)代理非加密的 golang 服務(wù)端節(jié)點(diǎn),密鑰交換使用橢圓曲線,在服務(wù)端使用自簽名證書(shū),不使用客戶(hù)端證書(shū),假設(shè)讀者對(duì) TLS 等已有基本的了解。
使用以下命令生成橢圓曲線密鑰和服務(wù)端自簽名證書(shū):

openssl ecparam -genkey -name secp256r1 | openssl ec -out hot.key -aes128
openssl req -new -x509 -days 365 -key hot.key -out hot.crt

上一節(jié)的 proto 和 golang 服務(wù)端代碼不變,golang 客戶(hù)端代碼變?yōu)椋?/p>

package main

import (
    "context"
    "crypto/tls"
    "os"

    "grpc_hot/pb"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func main() {
    port := "30080"
    if len(os.Args) >= 2 {
        port = os.Args[1]
    }

    creds := credentials.NewTLS(&tls.Config{
        InsecureSkipVerify: true,
    })
    conn, err := grpc.Dial("127.0.0.1:"+port, grpc.WithTransportCredentials(creds))
    if nil != err {
        println(err.Error())
        return
    }
    defer conn.Close()
    cli := pb.NewHotClient(conn)
    resp, err := cli.Inc(context.Background(), &pb.IntReq{I: 6})
    if nil != err {
        println(err.Error())
        return
    }
    println("resp:", resp.GetI())
}

nginx 配置文件變?yōu)椋?/p>

upstream grpc_hot {
    server 127.0.0.1:30081;
    server 127.0.0.1:30082;
}
server {
    listen 30080 ssl http2;
    ssl_protocols TLSv1.2;
    ssl_certificate hot.crt;
    ssl_certificate_key hot.key;
    ssl_password_file hot.pass;
    ssl_prefer_server_ciphers on;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256;
    ssl_session_cache shared:grpc_hot_sess:32m;
    ssl_session_timeout 10m;
    keepalive_timeout 60;
        
    location / {
        grpc_pass grpc://grpc_hot;
    }
}

2.1. TLS

啟動(dòng)上述 golang 的服務(wù)端和 nginx,調(diào)用一次客戶(hù)端,在客戶(hù)端連接 30080 端口。使用 wireshark 抓包,總共抓到 40 幀,基本比上節(jié)中的情況多了一倍。
在 OSI 七層結(jié)構(gòu)中,TCP、TLS、HTTP 分別位居第 4、6、7 層。本節(jié)中我們當(dāng)然只關(guān)心 TCP 的荷載為 TLS 層的幀。TLS 層的結(jié)構(gòu)如下:

+---------------+-------------------------------+------------------------------+
| Cont Type (8) |         Version (16)          |         Length (16)          |
+---------------+-------------------------------+------------------------------+
|                                   Data (*)                                 ...
+------------------------------------------------------------------------------+

在第 4、6、8、9 幀,兩端完成了 10 步的 TLS 握手:

  • Client Hello / Server Hello:兩端各生成一個(gè)隨機(jī)串告知對(duì)方,并由服務(wù)端決定使用套件 ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  • Certificate:服務(wù)端下發(fā)證書(shū),包括公鑰??蛻?hù)端驗(yàn)證證書(shū),這里選擇不驗(yàn)證
  • Server Key Exchange / Server Hello Done:服務(wù)端隨機(jī)生成一個(gè)服務(wù)端臨時(shí)私鑰,根據(jù)該私鑰在橢圓曲線上計(jì)算出一個(gè)服務(wù)端臨時(shí)公鑰,下發(fā)給客戶(hù)端
  • Client Key Exchange / Client Change Cipher Spec / Client Finished:同樣,客戶(hù)端隨機(jī)生成一個(gè)客戶(hù)端臨時(shí)私鑰,根據(jù)該私鑰在橢圓曲線上計(jì)算出一個(gè)客戶(hù)端臨時(shí)公鑰,上傳給服務(wù)端。同時(shí),客戶(hù)端根據(jù) hello 步的兩個(gè)隨機(jī)串、客戶(hù)端臨時(shí)私鑰和服務(wù)端臨時(shí)公鑰,計(jì)算出兩端分別使用的對(duì)稱(chēng)密鑰
  • Server Change Cipher Spec / Server Finished:同樣,服務(wù)端根據(jù) hello 步的兩個(gè)隨機(jī)串、服務(wù)端臨時(shí)私鑰和客戶(hù)端臨時(shí)公鑰,計(jì)算出兩端分別使用的對(duì)稱(chēng)密鑰。數(shù)學(xué)的魔力保證了兩端分別計(jì)算出的對(duì)稱(chēng)密鑰必然相同,感覺(jué)這很浪漫啊。

2.2 HTTP/2

接下來(lái)抓到 9 個(gè) TLS 層的幀,它們的 Content type 均為 Application Data (23),顯然,其中的 Data 字段均為已被對(duì)稱(chēng)密鑰加密的內(nèi)容,解密之后即是 HTTP 層的內(nèi)容。
這里我們打印出解密后的數(shù)據(jù):

frame source TLS payload(decrypted)
10 server 00 00 12 04 00 00 00 00 00 00 03 00 00 00 80 00
04 00 01 00 00 00 05 00 FF FF FF 00 00 04 08 00
00 00 00 00 7F FF 00 00
11 client 50 52 49 20 2A 20 48 54 54 50 2F 32 2E 30 0D 0A
0D 0A 53 4D 0D 0A 0D 0A
12 client 00 00 00 04 00 00 00 00 00
14 server 00 00 00 04 01 00 00 00 00
15 client 00 00 00 04 01 00 00 00 00
16 client 00 00 3E 01 04 00 00 00 01 83 87 45 89 62 B8 D7
C6 74 B1 92 A2 7F 41 8B 08 9D 5C 0B 81 70 DC 64
00 78 1F 5F 8B 1D 75 D0 62 0D 26 3D 4C 4D 65 64
7A 8A 9A CA C8 B4 C7 60 2B 89 B5 C3 40 02 74 65
86 4D 83 35 05 B1 1F 00 00 07 00 01 00 00 00 01
00 00 00 00 02 08 06
33 server 00 00 35 01 04 00 00 00 01 88 76 8D 3D 65 AA C2
A1 3E 98 0A E1 6D 77 97 17 61 96 DC 34 FD 28 07
54 BE 52 28 20 05 F5 00 ED C6 9B B8 07 54 C5 A3
7F 5F 8B 1D 75 D0 62 0D 26 3D 4C 4D 65 64 00 00
07 00 00 00 00 00 01 00 00 00 00 02 08 07
35 server 00 00 18 01 05 00 00 00 01 00 88 9A CA C8 B2 12
34 DA 8F 01 30 00 89 9A CA C8 B5 25 42 07 31 7F
00
39 client 00 00 04 08 00 00 00 00 00 00 00 00 07 00 00 08
06 00 00 00 00 00 02 04 10 10 09 0E 07 07

撥云見(jiàn)日,熟悉的亞子又回來(lái)了??梢钥吹?,服務(wù)端的 SETTINGS 幀早于客戶(hù)端的試探幀,其他差不都不大。
其中,第 16、33、35 幀的首部解碼出來(lái)分別如下:

:method POST
:scheme https
:path /pb.Hot/Inc
:authority 127.0.0.1:30080
content-type application/grpc
user-agent grpc-go/1.25.1
te trailers
:status 200
server openresty/1.15.8.2
date Sat, 07 Dec 2019 07:45:07 GMT
content-type application/grpc
grpc-status 0
grpc-message

請(qǐng)求首部的 :scheme 字段變?yōu)榱?https,其它都沒(méi)有什么變化。而兩個(gè) DATA 幀也還是我們熟悉的樣子。

References

RFC-7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
RFC-7541: HPACK: Header Compression for HTTP/2
Protocol Buffers: Encoding
Introducing gRPC Support with NGINX 1.13.10
Elliptic Curve Cryptography: a gentle introduction
RFC-5246: The Transport Layer Security (TLS) Protocol Version 1.2

Licensed under CC BY-SA 4.0

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容