wireshark配置

打开wireshark主面板,编辑-首选项,选择Protocols,在其中找到protobuf选项:
image.png
选择路径,这个路径下存放proto文件,注意把load all选中:
image.png
把用到的所有proto文件都放在这个路径下面,尤其是引用的第三方proto文件,wireshark会自动识别。

注:官方的第三方proto文件,主要是google/protobuf下面的,选择相应的版本,下载之后,解压,里面的bin目录下的proto.exe,是用来生成pb文件的,include目录下面,就是官方的文件,把google目录复制到前面指定的proto路径下。

解析

正常的捕获文件,wireshark原本的解析应该是类似这样的:
image.png
右键,选择“解码为”,进行如下设置:
image.png
选择http2协议,OK之后,数据就被解析出来。

大致原理

原来的wireshark解析,只把tcp协议解析出来,剩下payload部分,而grpc协议使用的是http2协议,因此下一步手动指定http2协议,之后wireshark就会自动解析grpc和protobuf。解析protobuf时,会自动的与2.2中读取到的proto文件的message部分匹配,进行字段的解析。如果没有proto文件,wireshark会把简单字段的值解码出来,但是不知道这些值代表的含义,对于复杂字段,直接就是二进制,无法解析。

实战

proto

  1. syntax = "proto3";
  2. package proto;
  3. message PingRequest {
  4. }
  5. message PingResponse{
  6. string res = 1;
  7. }
  8. service hello {
  9. rpc Ping(PingRequest) returns (PingResponse) {}
  10. }

client

  1. package main
  2. import (
  3. pb "commons/grpc5/proto"
  4. "context"
  5. "fmt"
  6. "log"
  7. "time"
  8. "google.golang.org/grpc"
  9. )
  10. func main() {
  11. //建立链接
  12. conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
  13. if err != nil {
  14. log.Fatalf("did not connect: %v", err)
  15. }
  16. defer conn.Close()
  17. client := pb.NewHelloClient(conn)
  18. res, err := client.Ping(context.TODO(), &pb.PingRequest{})
  19. if err != nil {
  20. return
  21. }
  22. fmt.Println(res.Res)
  23. time.Sleep(time.Minute * 2)
  24. }

service

  1. package main
  2. import (
  3. pb "commons/grpc5/proto"
  4. "context"
  5. "log"
  6. "net"
  7. "os"
  8. "time"
  9. "google.golang.org/grpc/grpclog"
  10. "google.golang.org/grpc/keepalive"
  11. "google.golang.org/grpc"
  12. )
  13. type Server struct {
  14. }
  15. func (s Server) Ping(ctx context.Context, request *pb.PingRequest) (*pb.PingResponse, error) {
  16. return &pb.PingResponse{Res: "pong"}, nil
  17. }
  18. func main() {
  19. grpclog.SetLoggerV2(grpclog.NewLoggerV2(os.Stdout, os.Stdout, os.Stdout))
  20. lis, err := net.Listen("tcp", ":3000")
  21. if err != nil {
  22. log.Fatalf("failed to listen: %v", err)
  23. }
  24. var kasp = keepalive.ServerParameters{
  25. MaxConnectionIdle: 15 * time.Second,
  26. MaxConnectionAge: 30 * time.Second,
  27. MaxConnectionAgeGrace: 5 * time.Second,
  28. Time: 5 * time.Second,
  29. Timeout: 1 * time.Second,
  30. }
  31. s := grpc.NewServer(grpc.KeepaliveParams(kasp))
  32. pb.RegisterHelloServer(s, &Server{})
  33. if err := s.Serve(lis); err != nil {
  34. log.Fatalf("failed to serve: %v", err)
  35. }
  36. }

抓包

image.png

TCP握手阶段

image.png

Magic

image.png
image.png
根据抓包分析,得出以下结论:

  • 这个请求是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

image.png
根据http2链接规范,在建立连接开始时双方都要发送 SETTINGS 帧以表明自己期许对方应做的配置,对方接收后同意配置参数便返回带有 ACK 标识的空 SETTINGS 帧表示确认,所以这里看到了4次SETTINGS

  • c < - > s
  • s < - > c

最关键的数据交互

来到了最精彩的部分,一条条解析
image.png
1.95行,HEADERS[1]: POST /proto.hello/Ping, DATA[1] (GRPC) (PROTOBUF) proto.PingRequest
可见HEADERS帧与DATA帧合在一起了
image.png
根据截图得到以下信息:

  • 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行的传输层
image.png
前面提到过ack的计算方式,根据95行的seq TCPpayload 可以计算出Ack=131

3.97行 WINDOW_UPDATE[0], PING[0]
image.png
这是服务端发送个客户端的

  • WINDOW_UPDATE 可以看到window size increment : 5。这是因为客户端在发送DATA帧时,会减少窗口大小(避免服务端堵塞用),此时服务端已经接收到了数据,所以要通知客户端增大自己的窗口
  • WINDOW_UPDATE 不需要双向确认
  • PING 发送WINDOW_UPDATE后,会固定ping一下,确保WINDOW_UPDATE成功
  1. 99行 PING[0]

    客户端回复服务端的ping

  2. 101行 HEADERS[1]: 200 OK, DATA[1] (GRPC) (PROTOBUF) proto.PingResponse, HEADERS[1]

    image.png
    根据抓包得到以下信息:

  • 服务端回复的过程是:headers->data->headers
  • 最后一个headers流,标志end stream :true,表明此时流处于half-closed (remote),客户端也收到服务端end stream :true,会将流标志为closed状态
  • 服务端返回的消息为 “pong”
  • DATA帧,有11个字节的数据
  1. WINDOW_UPDATE[0], PING[0]

    客户端发送给服务端
    image.png

  • WINDOW_UPDATE 可以看到window size increment : 11。表示客户端已经处理完了11个字节数据,服务端的窗口要自增11
  • WINDOW_UPDATE 不需要双向确认
  • PING 发送WINDOW_UPDATE后,会固定ping一下,确保WINDOW_UPDATE成功


  1. PING[0]

    服务端回复客户端的ping

  2. 101,109,113,115 ping

    这里是服务端配置的keepalive而触发的ping操作

  3. GOAWAY[0], PING[0]

image.png

  • 这里是服务端配置的keepalive而触发的GOAWAY操作,通知链接需要关闭
  • 同时GOAWAY也告知了,链接关闭的原因,Error: NO_ERROR (0)
  • PING 发送GOAWAY后,确保证GOAWAY成功(ping操作过去,不返回就是正常关闭了,😄)


TCP 挥手阶段

image.png

TCP 再次握手,挥手….循环

挥手后,grpc客户端,又立马建立链接,过会后,又被发送GOAWAY关闭链接再挥手,然后又握手…
无线循环…. 直到client过两分钟进程结束, 这很明显是grpc-go框架内部的链接管理做得

  1. func (ac *addrConn) createTransport : 定义了GOAWAY函数,如下:
  2. onGoAway := func(r transport.GoAwayReason) {
  3. ac.mu.Lock()
  4. ac.adjustParams(r)
  5. once.Do(func() {
  6. if ac.state == connectivity.Ready {
  7. // Prevent this SubConn from being used for new RPCs by setting its
  8. // state to Connecting.
  9. //
  10. // TODO: this should be Idle when grpc-go properly supports it.
  11. ac.updateConnectivityState(connectivity.Connecting, nil)
  12. }
  13. })
  14. ac.mu.Unlock()
  15. reconnect.Fire()
  16. }
  17. 然后创建ClientTransportNewClientTransport -> newHTTP2Client
  18. 最后返回的ClientTransport接口体有个onGoAway字段,就是上面定义的onGoAway方法
  19. 根据连接规则,处于Connecting状态的addrConn会一直进行连接

image.png

注:上述的seq值,都是相对值,所以看到的值都比较小
实际上是因为 Wireshark 工具帮我们做了优化,它默认显示的是序列号 seq 是相对值,而不是真实值。
如果你想看到实际的序列号的值,可以右键菜单, 然后找到「协议首选项」,接着找到「Relative Seq」后,把它给取消,操作如下:
image.png
取消后,Seq 显示的就是真实值了:
image.png