本文基于以下版本:
github.com/golang/protobuf v1.3.2
google.golang.org/grpc v1.25.1
openresty v1.15.8.2
本篇主要进行非加密 GRPC 的通信在字节层面的讨论,假设读者对 GRPC、HTTP/2 等已有基本的了解。
本篇使用一个简单的 proto:
syntax = "proto3";
package pb;
service Hot {
rpc Inc (IntReq) returns (IntResp);
}
message IntReq {
int32 i = 1;
}
message IntResp {
int32 i = 1;
}
以及如下的 golang 代码:
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 runServer(port string) {
srv := grpc.NewServer()
pb.RegisterHotServer(srv, &HotService{})
l, err := net.Listen("tcp", ":"+port)
if nil != err {
println(err.Error())
return
}
srv.Serve(l)
}
func runClient(port string) {
conn, err := grpc.Dial(":"+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())
}
func main() {
port := "30080"
if len(os.Args) >= 2 {
port = os.Args[1]
}
if len(os.Args) >= 3 && os.Args[2] == "cli" {
runClient(port)
} else {
runServer(port)
}
}
1.1. HTTP/2
启动上述 golang 代码的服务端,调用一次客户端,均使用默认端口。使用 wireshark 抓包,总共抓到 19 帧。除去那些不包含 TCP 荷载的帧,我们首先逐帧来看看在 HTTP 这一层长什么亚子。
frame | source | 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 帧外,TCP 荷载的结构均如下:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
-
Length
:帧的荷载的字节数,注意是 HTTP/2 帧的荷载,不是 TCP 的荷载 -
Type
:帧的类型
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
:不同类型的帧具有不同的 flag 定义
1.1.1. 连接
第 4 帧用许多语言都表示为这样:
"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
客户端通过这样一帧去试探服务端是否支持 HTTP/2。
接下来第 6、7、9、11 帧,两端相互请求 SETTINGS
。
SETTINGS
帧的荷载为零到多组键值对,每组键值对的结构为 2 字节的 id 和 4 字节的值。如第 7 帧包含一组键值对,id 为 00 05
,值为 00 00 40 00
。
id 和值的定义见 RFC-7540, section 6.5.2.
1.1.2. 首部
第 12 帧,客户端向服务端发送 HTTP 请求的首部。
HEADERS
帧的荷载结构如下:
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
本文的情况中,HEADERS
的帧荷载只有 Header Block Fragment
字段存在。其他字段的定义见 RFC-7540, section 6.2.
Fragment 的编解码使用 HPACK 算法(RFC-7541),包括霍夫曼编码。我们可以使用 Golang 的副标准库当中的封装来解码第 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)
}
}
其中传入的字节序列长度为帧的 Length
字段指示的 0x38
,但可以看到帧荷载的实际长度不止 0x38
,后面剩余的 16 个字节应该是一段 trailer,具体是什么暂时不得而知。这里打印出的 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,由于使用了静态索引表和霍夫曼编码,实际传输的首部只有 56 字节,通信精简的效果很明显。
同样,第 16 帧服务端发送的 HEADERS
帧,从长度上看也包含 trailer,首部解码出来如下:
:status 200
content-type application/grpc
神奇的是整个过程中没有一个 DATA
帧,那么 GRPC 使用的 HTTP body 在那里呢,我猜你也猜到了。
1.2. GRPC
References
RFC-7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
RFC-7541: HPACK: Header Compression for HTTP/2
Licensed under CC BY-SA 4.0
有疑问加站长微信联系(非本文作者)