protobuf+grpc | ⻘训营笔记

102 阅读4分钟

这是我参与「第五届⻘训营 」笔记创作活动的第8天。

在上一次的笔记中我记录了使用protobuf定义接口消息,并使用protoc工具生成golang代码,实现对于定义结构的序列化和反序列化。这篇文章会继续上一次的内容,在protobuf文件中定义rpc调用消息,并生成相应的代码,之后会编写rpc服务端和客户端进行rpc调用测试。

rpc消息定义

在proto文件中,以service关键词定义一组服务,在每个调用函数之前加rpc关键词定义rpc调用。具体代码如下。

syntax = "proto3";
package douyin.core;
option go_package = "./core;core";

message douyin_feed_request {
  int64 latest_time = 1; // 可选参数,限制返回视频的最新投稿时间戳,精确到秒,不填表示当前时间
  string token = 2; // 可选参数,登录用户设置
}

message douyin_feed_response {
  int32 status_code = 1; // 状态码,0-成功,其他值-失败
  string status_msg = 2; // 返回状态描述
  repeated Video video_list = 3; // 视频列表
  int64 next_time = 4; // 本次返回的视频中,发布最早的时间,作为下次请求时的latest_time
}

service BasicApiService {
    rpc Feed(douyin_feed_request) returns (douyin_feed_response) {}
}

有一个小小注意的点,之前的proto文件我是用proto2标准,因为官方给的就是proto2,但是我发现proto2中数据类型和go语言类型对应,比如int64对应的是*int64,就导致在结构体中都是引用类型,有一些不好用,所以改成了proto3,当然改的话需要删除required关键词,并且也不要使用optional关键词,前者是被废除了,后者是proto3标准中每一个message字段都默认是proto2标准中的optional,也就是说如果没有写值也不会报错,而加了optional的字段会自动变为oneof修饰,具体的优化我现在也没想清楚,但是也会变成引用类型就是了。

在proto3中,所有字段都是“可选”的(如果发件人未能设置它们,这不是错误)。但是,字段不再是“可为空”的,因为无法分辨出明确设置为默认值的字段与根本没有设置默认值之间的区别。

如果您需要“空”状态(并且没有可用于此的超出范围的值),则需要将其编码为单独的字段。例如,您可以执行以下操作:

message Foo {
  bool has_baz = 1;  // always set this to "true" when using baz
  int32 baz = 2;
}

Alternatively, you could use oneof:

message Foo {
  oneof baz {
    bool baz_null = 1;  // always set this to "true" when null
    int32 baz_value = 2;
  }
}

The oneof version is more explicit and more efficient on the wire but requires understanding how oneof values work. 最后,另一个完全合理的选择是坚持使用proto2。 Proto2并没有被弃用,实际上许多项目(包括Google内部)非常依赖proto2的功能,这些功能已在proto3中删除,因此它们可能永远不会切换。因此,在可预见的将来继续使用它是安全的。

代码生成操作

使用工具protoc-gen-go-grpc,好像是protoc有两个仓库,google和GitHub,两个生成代码略有不同,其中GitHub仓库已经废弃,不过网上还是可以搜索到教程,区别简单来说是GitHub会同时生成pb和grpc代码,Google仓库只生成pb相关代码,grpc部分由protoc-gen-go-grpc完成。

生成代码也有差别,我按照网上教程使用了GitHub版本代码,就会报以下错误 image.png 所以按照提示使用以下代码成功生成grpc调用代码

protoc --go-grpc_out=. *.proto

服务端代码

此时我们有了rpc的代码,此时需要编写服务端代码。

protoc-gen-go-grpc给我们提供了一系列接口,这些接口是rpc客户端调用的函数,服务端需要实现这些接口,之后将这些服务完成注册,客户端就会顺利完成调用。

package main

import (
	"context"
	"dousheng/pb/core"
	"log"
	"net"

	"google.golang.org/grpc"
)

const (
	Address = "127.0.0.1:9988"
)

type BasicApiServer struct {
}

func (server *BasicApiServer) Feed(ctx context.Context, in *core.DouyinFeedRequest, opts ...grpc.CallOption) (*core.DouyinFeedResponse, error) {
	return &core.DouyinFeedResponse{
		StatusCode: 0,
		StatusMsg:  "success",
	}, nil
}

func main() {
	// start server
	lis, err := net.Listen("tcp", Address)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	core.RegisterBasicApiServiceServer(s, &BasicApiServer{})
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

可以看到我们一方面完成了接口的编写,其中Model是是定义在我们前一次使用protoc-gen-go生成的代码中,之后完成了服务注册,之后进行客户端调用。

客户端代码

package main

import (
	"context"
	"dousheng/pb/core"
	"fmt"

	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
)

const (
	Address = "127.0.0.1:9988"
)

func main() {
	conn, err := grpc.Dial(Address, grpc.WithInsecure())
	if err != nil {
		grpclog.Fatalln(err)
	}
	defer conn.Close()

	c := core.NewBasicApiServiceClient(conn)

	req := &core.DouyinFeedRequest{
		LatestTime: 0,
		Token:      "",
	}

	res, err := c.Feed(context.Background(), req)
	if err != nil {
		grpclog.Fatalln(err)
	}

	fmt.Println(res.StatusMsg)
}

代码中,我们先监听了rpc服务端地址,使用grpc库中带的方法建立连接,之后实例化一个rpc客户端,使用客户端进行rpc调用,这样一次rpc调用就完成了。

之后计划

有了rpc调用之后,可以将服务分成,分为靠前的api层,使用http协议和用户进行路由交互,以及靠后的服务层,使用rpc协议操作数据库和redis,这样让系统架构更加灵活,也更利于团队协作。