上手go的grpc以及protobuf的使用 | 豆包MarsCode AI刷题

204 阅读6分钟

什么是序列化和反序列化?

  • 场景:用在两个客户端和服务端通信,或者两个服务器之间的通信。
  • 举例:比如服务器A需要调用服务器B上的一个函数。
    1. 服务器A的代码中可能是一个对象的形式存储的数据信息。首先它得把对象序列化成json格式,使用二进制数据的方式发送给服务器B。
    2. 服务器B接收到json格式的二进制数据。首先得进行反序列化存储到对象模型中,然后进行业务处理得到结果。结果同样也得先序列化成json格式发送给服务器A。
  • 只能序列化成json格式吗?
    • 不是。除了json,还有xm、protobuf、msgpack等数据编码协议
    • 不同的数据传入格式有什么不同?
      • 传输性能不同。protobuf和msgpack格式的传输性能更好。go微服务项目中就是使用的protobuf。

protobuf序列化编码协议和上手实操

  • protobuf是什么?
    • 是一种IDL(Interface Defintion Language 接口定义语言),还有Thrift也是。
  1. window上安装protobuf:
  2. go中安装protobuf编译插件、安装grpc框架::
    go get -u google.golang.org/grpc
    # 安装 `protoc-gen-go` 和 `protoc-gen-go-grpc`
    go install google.golang.org/protobuf/cmd/protoc-gen-go
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
    
  3. 创建项目目录:proto、server、client
      • proto:存放 .proto 文件。
    • server:存放服务端代码。
    • client:存放客户端代码。
  4. 编写proto文件。
    • protoc3.10之后要添加option go_package参数
  5. 执行命令根据proto文件生成go文件(两个go文件): protoc --go_out=. --go-grpc_out=. hello.proto 如果不加--go-grpc_out=.就不会生成service对应的go文件
  6. 编写server代码和client代码
  7. 运行代码
    • 运行服务端代码:go run server/server.go
    • 运行客户端代码:go run client/client.go
  8. 成功的话客户端会打印Greeting: Hello, World!

代码实践

代码编写思路

  1. 定义服务接口和服务消息
    1. 编写proto文件
    2. protoc命令根据proto文件生成go代码
    • 为什么使用proto的方式定义接口和消息服务?
      1. proto是跨语言的协议,不仅可以生成go语言的代码,还能生成其他语言的。
      2. protobuf的传输格式非常高效,能够被序列化成非常高效的二进制数据进行传输,速度快,空间小。
    • 为什么要通过定义接口的方式,而不是直接使用结构体?
      • 使用接口的方式,客户端在调用服务时,直接调用接口中指定的方法就行了,不用管是哪个具体的实现类实现的这个接口。
      • 这样比如具体的服务方法逻辑改变了,只用更换/修改实现类就行了。客户端调用方法的代码不用做任何改变,因为它只知道调用的是接口的方法。
      • 而如果使用结构体的方式。服务方法的逻辑改变后,更换/修改实现类后,客户端由于是通过具体的实现类调用的方法,所以它也得替换成新实现类实例。服务代码和客户端代码就会强耦合,修改一个,其他一堆客户端代码也得跟着改。
  2. 启动服务(server负责)
    1. 创建服务实例
    2. 创建监听器。
      1. 端口
    3. 启动服务。
  3. 连接服务(client负责)
    1. 建立连接
    2. 创建一个客户端实例
  4. 调用方法(client中调用server的方法)
    1. 发送请求数据
    2. 返回响应数据

编写proto文件

  • protoc3.10之后要添加option go_package参数,用于指定生成的go文件目录
// proto/hello.proto  
syntax = "proto3";  
  
option go_package = ".;proto";  
  
package hello;  
  
service Greeter {  
  rpc SayHello (HelloRequest) returns (HelloReply);  
}  
  
message HelloRequest {  
  string name = 1;  
}  
  
message HelloReply {  
  string message = 1;  
}

根据proto文件生成go文件

  • protoc --go_out=. --go-grpc_out=. hello.proto
  • 如果不加--go-grpc_out=.就不会生成service对应的go文件
  • 会生成两个文件:hello.pb.go,hello_grpc.pb.go
    • helloworld.pb.go:包含了消息类型的定义。
    • helloworld_grpc.pb.go:包含了 gRPC 服务的接口定义。

编写server.go和client.go

  • 定义一个 server 结构体,嵌入了生成的 GreeterServer 接口的默认未实现版本。
  • 实现 Greeter 服务中的 SayHello 方法
  • SayHello 方法会接收 HelloRequest 请求,并返回 HelloReply 响应
  • 创建问候消息,使用请求中的 Name 字段
  • 返回 HelloReply 响应,包含生成的问候消息
  • 创建一个监听器,监听端口 50051 lis, err := net.Listen("tcp", ":50051")
  • 创建一个 gRPC 服务器的实例
  • 将我们的 server 结构体注册到 gRPC 服务器中, 这样 gRPC服务器 就知道如何处理 Greeter 服务的请求了
  • 启动 gRPC 服务器,开始监听客户端的请求
package main  
  
import (  
    "context"  
    "fmt"    "google.golang.org/grpc"    pb "grpc-demo-proj/proto"  
    "log"    "net")  
  
// 定义一个 server 结构体,嵌入了生成的 GreeterServer 接口的默认未实现版本。  
// 这个结构体将会实现我们定义的 Greeter 服务中的方法。  
type server struct {  
    pb.UnimplementedGreeterServer  
}  
  
// 实现 Greeter 服务中的 SayHello 方法  
// SayHello 方法会接收 HelloRequest 请求,并返回 HelloReply 响应  
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {  
    // 创建问候消息,使用请求中的 Name 字段  
    replyMessage := fmt.Sprintf("Hello, %s!", req.Name)  
  
    // 返回 HelloReply 响应,包含生成的问候消息  
    return &pb.HelloReply{Message: replyMessage}, nil  
}  
  
func main() {  
    // 创建一个监听器,监听端口 50051    lis, err := net.Listen("tcp", ":50051")  
    if err != nil {  
       log.Fatalf("监听端口失败: %v", err)  
    }  
  
    // 创建一个 gRPC 服务器的实例  
    grpcServer := grpc.NewServer()  
  
    // 将我们的 server 结构体注册到 gRPC 服务器中,  
    // 这样 gRPC服务器 就知道如何处理 Greeter 服务的请求了  
    pb.RegisterGreeterServer(grpcServer, &server{})  
  
    // 打印服务器启动消息  
    log.Println("gRPC 服务器正在运行,监听端口 50051")  
  
    // 启动 gRPC 服务器,开始监听客户端的请求  
    // 如果 Serve 过程中发生错误,将会打印错误信息并退出  
    if err := grpcServer.Serve(lis); err != nil {  
       log.Fatalf("gRPC 服务器启动失败: %v", err)  
    }  
}
// client/client.go  
package main  
  
import (  
    "context"  
    "log"    "time"  
    "google.golang.org/grpc"    pb "grpc-demo-proj/proto" // 根据实际的文件路径替换  
)  
  
func main() {  
    // 使用 gRPC Dial 方法连接到服务端,连接地址为 `localhost:50051`(即服务端的地址)  
    // grpc.WithInsecure() 是为了简化示例,跳过 SSL 加密认证,仅在本地开发环境中使用。  
    // grpc.WithBlock() 表示在连接成功前阻塞,不让代码继续往下执行。  
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithBlock())  
    if err != nil {  
       log.Fatalf("无法连接到服务端: %v", err)  
    }  
    // defer 会在 main 函数结束时自动执行 conn.Close(),关闭连接,避免资源泄漏  
    defer conn.Close()  
  
    // 使用生成的代码创建一个 Greeter 服务的客户端实例  
    client := pb.NewGreeterClient(conn)  
  
    // 定义请求上下文(Context),设置超时时间为 1 秒,避免调用卡住  
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)  
    // cancel() 会在 main 结束时执行,以确保请求及时关闭。  
    defer cancel()  
  
    // 创建请求消息,包含要发送的 name 字段  
    req := &pb.HelloRequest{Name: "World"}  
  
    // 使用客户端实例的 SayHello 方法向服务端发送请求,并接收响应  
    res, err := client.SayHello(ctx, req)  
    if err != nil {  
       log.Fatalf("请求失败: %v", err)  
    }  
  
    // 输出服务端返回的消息  
    log.Printf("收到服务端响应: %s", res.Message)  
}