gRPC 常被说成“基于 HTTP/2 的 RPC”,但一次调用在网线上到底是什么样?这篇在客户端和服务端之间放一个抓包代理,把一个最简单的 Unary 调用的每一帧、每个头、每个字节都拆开看。
调用返回 "echo:hello",n=7,用时(含建立连接)2.3 ms
1.62ms C→S PREFACE 流 0 长 24 "PRI * HTTP/2.0\r\n"
1.62ms C→S SETTINGS 流 0 长 0
1.62ms S→C SETTINGS 流 0 长 6 MAX_FRAME_SIZE=16384
1.77ms S→C SETTINGS 流 0 长 0 [ACK]
2.11ms C→S SETTINGS 流 0 长 0 [ACK]
2.18ms C→S HEADERS 流 1 长 82 [END_HEADERS] :method: POST; :scheme: http; :path: /demo.Demo/Unary; :authority: 127.0.0.1:9801; content-type: application/grpc; user-agent: grpc-go/1.84.0; te: trailers; grpc-timeout: 4998325u
2.19ms C→S DATA 流 1 长 14 [END_STREAM] 00000000090a0568656c6c6f1007
2.28ms S→C WINDOW_UPDATE 流 0 长 4 增量 14
2.28ms S→C PING 流 0 长 8 02041010090e0707
2.33ms S→C HEADERS 流 1 长 14 [END_HEADERS] :status: 200; content-type: application/grpc
2.33ms S→C DATA 流 1 长 19 000000000e0a0a6563686f3a68656c6c6f1007
2.33ms S→C HEADERS 流 1 长 24 [END_STREAM|END_HEADERS] grpc-status: 0; grpc-message:
2.37ms C→S PING 流 0 长 8 [ACK] 02041010090e0707
2.45ms C→S WINDOW_UPDATE 流 0 长 4 增量 19
2.45ms C→S PING 流 0 长 8 02041010090e0707
2.52ms S→C PING 流 0 长 8 [ACK] 02041010090e0707连接上先交换 24 字节的前言和 SETTINGS。每个调用占一条 HTTP/2 流(流 1、3、5……),请求是 HEADERS 加带 END_STREAM 的 DATA,响应是 HEADERS、DATA 和带 END_STREAM 的 HEADERS(trailers)。
:method: POST :scheme: http :path: /demo.Demo/Unary :authority: 127.0.0.1:9801 content-type: application/grpc user-agent: grpc-go/1.84.0 te: trailers grpc-timeout: 4998325u (这个 HEADERS 帧的负载 82 字节)
方法永远是 POST,路径是 /包名.服务名/方法名,content-type 是 application/grpc,te: trailers 表示客户端能接收 trailers,grpc-timeout 带着截止时间(单位微秒)。这就是 gRPC 对 HTTP/2 的全部约定。
DATA 帧负载共 14 字节:00000000090a0568656c6c6f1007
第 1 字节 压缩标志 = 0(0 表示没压缩);第 2–5 字节 消息长度 = 9(大端)
载荷 0a0568656c6c6f1007;我自己 proto.Marshal(Req{msg:hello,n:7}) = 0a0568656c6c6f1007;两者字节相同一条消息 = 1 字节压缩标志 + 4 字节大端长度 + protobuf 载荷。载荷与直接 proto.Marshal 的结果相同,所以任何能看懂 HTTP/2 和 protobuf 的工具都能解它(见 protobuf 那一篇的字节拆解)。
流 1 HEADERS 标志 END_HEADERS :status: 200; content-type: application/grpc 流 1 DATA 负载 19 字节 前缀+载荷:000000000e0a0a6563686f3a68656c6c6f1007 流 1 HEADERS 标志 END_STREAM|END_HEADERS grpc-status: 0; grpc-message:
响应的 HTTP 状态永远是 200,调用成败看最后 trailers 里的 grpc-status 和 grpc-message。也因此只能到流结束才知道结果(下一篇再展开)。
客户端→服务端 203 字节;服务端→客户端 155 字节;请求消息本身 protobuf 9 字节,响应消息 14 字节
首次调用客户端发 203 字节、服务端发 155 字节,而业务数据只有 9 和 14 字节;大头是连接建立和 82 字节的请求头。
第二次调用用时 0.4 ms;这一次调用的 TCP 字节数:客户端→服务端 94,服务端→客户端 97
0.19ms C→S HEADERS 流 3 长 15 [END_HEADERS] :method: POST; :scheme: http; :path: /demo.Demo/Unary; :authority: 127.0.0.1:9801; content-type: application/grpc; user-agent: grpc-go/1.84.0; te: trailers; grpc-timeout: 4999971u
0.19ms C→S DATA 流 3 长 14 [END_STREAM] 00000000090a0568656c6c6f1007
0.30ms S→C WINDOW_UPDATE 流 0 长 4 增量 14
0.31ms S→C PING 流 0 长 8 02041010090e0707
0.32ms S→C HEADERS 流 3 长 2 [END_HEADERS] :status: 200; content-type: application/grpc
0.33ms S→C DATA 流 3 长 19 000000000e0a0a6563686f3a68656c6c6f1007
0.33ms S→C HEADERS 流 3 长 2 [END_STREAM|END_HEADERS] grpc-status: 0; grpc-message:
0.39ms C→S PING 流 0 长 8 [ACK] 02041010090e0707
0.43ms C→S WINDOW_UPDATE 流 0 长 4 增量 19
0.44ms C→S PING 流 0 长 8 02041010090e0707
0.51ms S→C PING 流 0 长 8 [ACK] 02041010090e0707
请求 HEADERS 帧负载:第一次 82 字节,第二次 15 字节第二次调用不需要重新握手,请求头靠 HPACK 动态表从 82 字节降到 15 字节,响应头从 14 降到 2,trailers 从 24 降到 2。这是 gRPC 小消息仍然高效的原因之一。