深入学习 GRPC - 1. 非加密协议

· · 697 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文基于以下版本:

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


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:

查看原文:深入学习 GRPC - 1. 非加密协议

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

697 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传