深入理解grpc-go(一):从hello world说起

之前已分两篇文章讲解了grpc及其原理,本篇继续grpc的话题,讲讲grpc的golang实现grpc-go。

我们以grpc-go提供的hello world为例,分多篇讲讲grpc-go中client端和server端的整个通信过程的实现方式。

准备工作

  • Go,安装Go最新的三个版本中的任意一个都可。有关安装的方法,可参阅Go的Getting Started指南。

  • Protocol buffer编译器,protoc 3。有关安装的方法,可参阅Protocol Buffer Compiler Installation

  • protoc的Go插件:

    • 使用以下命令安装protoc的Go插件:
      $ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
      $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
    • 更新你的环境变量PATH,以便protoc能找到这个插件:
      $ export PATH="$PATH:$(go env GOPATH)/bin"

运行示例

获取示例代码

示例代码是grpc-go仓库的一部分。

  1. clone下仓库代码:

    $ git clone -b v1.50.0 --depth 1 https://github.com/grpc/grpc-go
  2. 切换到示例代码的文件夹:

    $ cd grpc-go/examples/helloworld

生成gRPC代码

运行以下命令:

$ protoc --go_out=. --go_opt=paths=source_relative
 --go-grpc_out=. --go-grpc_opt=paths=source_relative
 helloworld/helloworld.proto

这将根据helloworld.proto中定义的数据结构生成helloworld/helloworld.pb.gohelloworld/helloworld_grpc.pb.go文件。

github仓库中提供的示例代码里面已包含这两个文件,无需我们重新生成。

运行示例

  1. 编译并执行server端代码:
    $ go run greeter_server/main.go
  2. 打开另外一个终端,编译并执行client端代码:
    $ go run greeter_client/main.go
    Greeting: Hello world
    如果输出内容和上述一致,那么恭喜你,你已经使用 gRPC 运行了一个客户端-服务器应用程序。

分析示例

我们回头再深入看看刚才那个示例中涉及到的代码,详细了解下整个过程。

示例中涉及到的所有代码及目录结构如下:

├── greeter_client
│   └── main.go
├── greeter_server
│   └── main.go
└── helloworld
 ├── helloworld.pb.go
 ├── helloworld.proto
 └── helloworld_grpc.pb.go

helloworld.proto

Helloworld.proto是使用protocol buffer接口描述语言所定义的数据结构,具体内容如下:

syntax = "proto3";

option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
  1. syntax:表明此文件所使用的protocol语法版本为proto3。目前有两个主要版本:proto2、proto3。

  2. option:文件中的各个声明可以使用option进行注释。option不会改变声明的整体含义,但可能会影响它在特定上下文中的处理方式。你可以使用官方提供的option,也可以使用自定义的option。

  3. package:表示此文件所属的包名为helloworld。这里的package与Go中的package含义类似,可以避免message类型之间的名称冲突。

  4. message:消息定义,用于定义请求响应中涉及到的消息格式。此处定义了两种消息格式:HelloRequest、HelloReply。

  5. service:服务定义,用来定义对外提供的服务接口格式。此处定义了一种服务Greeter,它有一个rpc方法SayHello。

helloword.pb.go & helloword_grpc.pb.go

这两个文件是protoc工具根据helloworld.proto文件生成的对应的golang代码。

helloworld.pb.go整体结构如下图所示。其中File_examples_helloworld_helloworld_helloworld_proto存储的是pb中的一些元数据,包括pb中定义了多少个enum、message、service等;HelloRequest、HelloReply是根据pb中定义的message HelloRequest、message HelloReply转换得到的对应的go struct。


helloworld_grpc.pb.go整体结构如下图所示:

  1. GreeterClient:根据pb中定义的Greeter service生成的client,它提供SayHello方法,用于调用远程RPC方法SayHello。

  2. GreeterServer:根据pb中定义的Greeter service生成的server接口,开发者需要实现对应的SayHello方法,填充自己的业务逻辑,处理client端的请求。

  3. UnimplementedGreeterServer:GreeterServer的一个内置实现,主要是为了向前兼容。它的Sayhello方法实现如下,直接返回“method SayHello not implemented”的error。

    func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
        return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
    }
  4. UnsafeGreeterServer:接口,也是为了向前兼容,官方目前不推荐使用这个接口。

  5. NewGreeterClient:返回一个新的Greeter client。

  6. RegisterGreeterServer:用于向grpc注册Greeter server。


简单总结下:

  1. helloworld.pb.go:用于填充、序列化、获取message HelloRequestmessage HelloReply的代码。

  2. helloworld_grpc.pb.go:生成的client端和server端代码。

greeter_server/main.go

greeter_server/main.go的主要代码如下:

var (
    port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
    pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
  1. 创建了一个server的数据结构,实现了SayHello方法。SayHello方法中写的是我们具体的业务逻辑。

  2. 通过grpc.NewServer()生成了一个新的grpc server,并向其注册了greeter server。

  3. grpc server监听port端口,并处理从此端口中接收到的rpc请求。

greeter_client/main.go

greeter_client/main.go的主要代码如下:

const (
    defaultName = "world"
)

var (
    addr = flag.String("addr", "localhost:50051", "the address to connect to")
    name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    flag.Parse()
    // Set up a connection to the server.
    conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}
  1. 与grpc server建立连接,生成了一个conn对象。

  2. 根据conn对象新生成了一个greeter client对象。

  3. 调用greeter client的SayHello方法,获取rpc响应结果。

总结

本文以hello world为例,讲述了从pb文件生成golang代码,到grpc client端调用server端获取数据的整个过程。

后续我再详细讲解下其中的几个细节,包括:

  1. grpc.NewServer()做了什么;grpc server的数据结构;grpc server是如何路由的,即它是如何映射到具体的实现的。

  2. c.SayHello()做了什么,它是如何与server端进行通信的,包括如何建立连接、发送请求信息、获取响应结果。

推荐阅读

关于我

大厂搬砖人员,有多年的微服务开发经验,精通 Go、Python 语言。关注我了解更多⬇️
关于我

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!