wireshark配置
打开wireshark主面板,编辑-首选项,选择Protocols,在其中找到protobuf选项:
选择路径,这个路径下存放proto文件,注意把load all选中:
把用到的所有proto文件都放在这个路径下面,尤其是引用的第三方proto文件,wireshark会自动识别。
注:官方的第三方proto文件,主要是google/protobuf下面的,选择相应的版本,下载之后,解压,里面的bin目录下的proto.exe,是用来生成pb文件的,include目录下面,就是官方的文件,把google目录复制到前面指定的proto路径下。
解析
正常的捕获文件,wireshark原本的解析应该是类似这样的:
右键,选择“解码为”,进行如下设置:
选择http2协议,OK之后,数据就被解析出来。
大致原理
原来的wireshark解析,只把tcp协议解析出来,剩下payload部分,而grpc协议使用的是http2协议,因此下一步手动指定http2协议,之后wireshark就会自动解析grpc和protobuf。解析protobuf时,会自动的与2.2中读取到的proto文件的message部分匹配,进行字段的解析。如果没有proto文件,wireshark会把简单字段的值解码出来,但是不知道这些值代表的含义,对于复杂字段,直接就是二进制,无法解析。
实战
proto
syntax = "proto3";
package proto;
message PingRequest {
}
message PingResponse{
string res = 1;
}
service hello {
rpc Ping(PingRequest) returns (PingResponse) {}
}
client
package main
import (
pb "commons/grpc5/proto"
"context"
"fmt"
"log"
"time"
"google.golang.org/grpc"
)
func main() {
//建立链接
conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewHelloClient(conn)
res, err := client.Ping(context.TODO(), &pb.PingRequest{})
if err != nil {
return
}
fmt.Println(res.Res)
time.Sleep(time.Minute * 2)
}
service
package main
import (
pb "commons/grpc5/proto"
"context"
"log"
"net"
"os"
"time"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc"
)
type Server struct {
}
func (s Server) Ping(ctx context.Context, request *pb.PingRequest) (*pb.PingResponse, error) {
return &pb.PingResponse{Res: "pong"}, nil
}
func main() {
grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stdout))
lis, err := net.Listen("tcp", ":3000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var kasp = keepalive.ServerParameters{
MaxConnectionIdle: 15 * time.Second,
MaxConnectionAge: 30 * time.Second,
MaxConnectionAgeGrace: 5 * time.Second,
Time: 5 * time.Second,
Timeout: 1 * time.Second,
}
s := grpc.NewServer(grpc.KeepaliveParams(kasp))
pb.RegisterHelloServer(s, &Server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
抓包
TCP握手阶段
Magic
根据抓包分析,得出以下结论:
- 这个请求是client -> service
- 发送请求方seq = 1 , 数据 24字段,下次的seq = 对方的ack = 1+24 = 25
Magic是干嘛的?
详见 HTTP/2 Connection Preface
根据http2链接规范,在建立http2之前,客户端首先发送一个magic八字节流,这主要适用于客户端从HTTP/1.1升级而来:0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
解码成ASCII:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这看起来像一个h1消息,h1服务器收到它,将返回错误。
SETTINGS
根据http2链接规范,在建立连接开始时双方都要发送 SETTINGS 帧以表明自己期许对方应做的配置,对方接收后同意配置参数便返回带有 ACK 标识的空 SETTINGS 帧表示确认,所以这里看到了4次SETTINGS
- c < - > s
- s < - > c
最关键的数据交互
来到了最精彩的部分,一条条解析
1.95行,HEADERS[1]: POST /proto.hello/Ping, DATA[1] (GRPC) (PROTOBUF) proto.PingRequest
可见HEADERS帧与DATA帧合在一起了
根据截图得到以下信息:
- HEADERS帧与DATA帧属于 stream ID =1的流
- HEADERS帧完成后,end stream = false,DATA帧完成后 end stream = true , 表明这是一个一元rpc操作,此时的流处于half-closed (local)
- 看到了header 倍感亲切
- DATA帧,有5个字节的数据
- grpc 请求中没携带任何数据
2.96行 3000 → 57805 [ACK] Seq=25 Ack=131 Win=408128 Len=0 TSval=902000267 TSecr=902000267
95行的传输层
前面提到过ack的计算方式,根据95行的seq TCPpayload 可以计算出Ack=131
3.97行 WINDOW_UPDATE[0], PING[0]
这是服务端发送个客户端的
- WINDOW_UPDATE 可以看到window size increment : 5。这是因为客户端在发送DATA帧时,会减少窗口大小(避免服务端堵塞用),此时服务端已经接收到了数据,所以要通知客户端增大自己的窗口
- WINDOW_UPDATE 不需要双向确认
- PING 发送WINDOW_UPDATE后,会固定ping一下,确保WINDOW_UPDATE成功
99行 PING[0]
客户端回复服务端的ping
101行 HEADERS[1]: 200 OK, DATA[1] (GRPC) (PROTOBUF) proto.PingResponse, HEADERS[1]
根据抓包得到以下信息:
- 服务端回复的过程是:headers->data->headers
- 最后一个headers流,标志end stream :true,表明此时流处于half-closed (remote),客户端也收到服务端end stream :true,会将流标志为closed状态
- 服务端返回的消息为 “pong”
- DATA帧,有11个字节的数据
WINDOW_UPDATE[0], PING[0]
客户端发送给服务端
- WINDOW_UPDATE 可以看到window size increment : 11。表示客户端已经处理完了11个字节数据,服务端的窗口要自增11
- WINDOW_UPDATE 不需要双向确认
- PING 发送WINDOW_UPDATE后,会固定ping一下,确保WINDOW_UPDATE成功
PING[0]
服务端回复客户端的ping
101,109,113,115 ping
这里是服务端配置的keepalive而触发的ping操作
GOAWAY[0], PING[0]
- 这里是服务端配置的keepalive而触发的GOAWAY操作,通知链接需要关闭
- 同时GOAWAY也告知了,链接关闭的原因,Error: NO_ERROR (0)
- PING 发送GOAWAY后,确保证GOAWAY成功(ping操作过去,不返回就是正常关闭了,😄)
TCP 挥手阶段
TCP 再次握手,挥手….循环
挥手后,grpc客户端,又立马建立链接,过会后,又被发送GOAWAY关闭链接再挥手,然后又握手…
无线循环…. 直到client过两分钟进程结束, 这很明显是grpc-go框架内部的链接管理做得
func (ac *addrConn) createTransport : 定义了GOAWAY函数,如下:
onGoAway := func(r transport.GoAwayReason) {
ac.mu.Lock()
ac.adjustParams(r)
once.Do(func() {
if ac.state == connectivity.Ready {
// Prevent this SubConn from being used for new RPCs by setting its
// state to Connecting.
//
// TODO: this should be Idle when grpc-go properly supports it.
ac.updateConnectivityState(connectivity.Connecting, nil)
}
})
ac.mu.Unlock()
reconnect.Fire()
}
然后创建ClientTransport,NewClientTransport -> newHTTP2Client
最后返回的ClientTransport接口体有个onGoAway字段,就是上面定义的onGoAway方法
根据连接规则,处于Connecting状态的addrConn会一直进行连接
注:上述的seq值,都是相对值,所以看到的值都比较小
实际上是因为 Wireshark 工具帮我们做了优化,它默认显示的是序列号 seq 是相对值,而不是真实值。
如果你想看到实际的序列号的值,可以右键菜单, 然后找到「协议首选项」,接着找到「Relative Seq」后,把它给取消,操作如下:
取消后,Seq 显示的就是真实值了: