Go + gRPC 使用初探

gRPC 是一个高性能、开源和通用的 RPC 框架,基于 HTTP/2 传输协议,使用 Protocol Buffers 作为接口描述语言,支持众多的编程语言。 在这篇文章里,我记录了如何从零开始,在 Windows 下,用 Docker 和 Go 搭建一个简单的 gRPC 示例。

关于 gRPC

gRPC 最初在2015年由Google开发。它使用 HTTP / 2 进行传输,并提供诸如身份验证,双向流和流控制,阻塞或非阻塞绑定以及取消和超时等功能。它为多种语言生成跨平台的客户端和服务器绑定。最常见的使用场景包括在微服务中连接服务风格的架构,并将移动设备,浏览器客户端连接到后端服务。

在 gRPC 里,客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得创建分布式应用和服务变得更加容易。同时,只需要用 Protocol Buffers 定义接口与数据格式,gRPC 就可以生成各个语言下的请求与回复的函数定义,极大的简化了不同平台之间服务与客户端的开发流程。

Protocol Buffers

Protocol Buffers 是与语言无关,与平台无关的可扩展机制,用于对结构化数据进行序列化(例如 XML),但更小,更快,更简单。 您定义要一次构造数据的方式,然后可以使用生成的特殊源代码轻松地使用各种语言在各种数据流中写入和读取结构化数据。

1
2
3
4
5
message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

上面定义了一个简单的 Person 消息数据,按顺序分别有三个字段,因此编号了 1, 2, 3proto3 官方文档 提供了更详细的介绍与使用示例。

gRPC vs REST

主要的不同是 gRPC 使用了 Protobuf 作为数据传输格式,因为是二进制,所以体积更小,消耗资源更低。而 REST 一般用的是 JSON 格式,便于直观理解,但数据本身体积很大,而且解析需要更多计算资源。此外,gRPC 默认使用 HTTP/2,而很多 REST 采用的还是 HTTP/1。更多详细的不同可以参考 Compare gRPC services with HTTP APIs

环境设置

为了避免配置环境的麻烦,推荐使用 DockerVisual Studio Code Remote - Containers,这样一来可以更便捷地在不同的容器环境中进行开发。 在 Windows 下,安装 Docker Desktop 来启动 Docker 服务,我使用了 WSL 2 作为其引擎,更多关于 WSL 2 的可以参考我之前的文章 升级到 WSL 2 开发环境。由于使用了 WSL 2,为了更快的文件访问速度,我选择把整个项目文件夹放在 WSL 2 下的文件夹内: $ mkdir ~/grpc-go。 用到了 VS Code Remote Container 的插件来使 VS Code 能够支持在容器环境内开发。 为了使得开发用到的容器环境具有一致性,需要使用 devcontainer.json 文件,可以参考 vscode-dev-containers/containers/go/ 下面的示例配置。 先在 ~/grpc-go 下创建 .devcontainer 目录,并新建 Dockerfiledevcontainer.json 配置,直接复制 vscode-dev-containers/containers/go/ 中的配置即可,后面需要对 devcontainer.json 做一些修改。创建完成后的 grpc-go 目录如下:

1
2
3
4
5
6
7
➜  grpc-go tree -a
.
└── .devcontainer
    ├── Dockerfile
    └── devcontainer.json

1 directory, 2 files

为了更方便的使用容器中的 GOPATH,在 devcontainer.json 中添加如下两行,将 WSL 中的 grpc-go 挂载到容器中的 /go/src/github.com/fing/ 目录下,方便后面模块的导入。

1
2
"workspaceMount":  "source=${localWorkspaceFolder},target=/go/src/github.com/fing/${localWorkspaceFolderBasename},type=bind,consistency=cached",
"workspaceFolder":  "/go/src/github.com/fing/${localWorkspaceFolderBasename}",

在配置好 grpc-go/.devcontainer/ 之后,按 F1 打开 VS Code 的命令界面,选择 Remote-Containers: Open Folder in Containers...,之后在打开的 Windows 文件选择框地址栏中输入 \\wsl$ 即可选择 WSL 中的文件夹打开。

你好 gRPC

在这个例子中将用 gRPC 创建一个简单的 greet 接口以及用 Go 实现服务和客户端。

安装 Protocol Buffer 编译器

因为这个容器的镜像是基于 Debian 的,因此可以使用熟悉的 apt install 来安装 protobuf-compiler

1
2
3
# Install protobuf compiler
apt update && apt install -y protobuf-compiler
protoc --version  # Ensure compiler version is 3+

gRPC – Protocol Buffer Compiler Installation

安装 Go 模块

需要 gRPC 以及 protobuf 的 Go 插件:

1
2
3
# Install go plugins
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/{proto, protoc-gen-go}

测试 protoc 编译

首先创建 protobuf 文件 grpc-go/greet/greetpb/greet.proto

1
2
3
4
5
6
syntax = "proto3";

package greet;
option go_package="greet/greetpb";

service GreetService {}

这里定义了了一个简单 service:GreetService,暂时将具体定义留白。 使用 protoc 测试编译:

1
protoc greet/greetpb/greet.proto --go_out=plugins=grpc:.

编译完成后能在 greetpb 目录下看到生成好的 greet.pb.go 文件。

添加基本的 Server

首先在 greet 目录下新建 greet_server 文件夹并添加 greet/greet_server/server.go

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
	fmt.Println("Hello world!")
}

测试运行 go run greet/greet_server/server.go,能看到 Hello World! 的输出。 接下来添加一下基本的 Server 框架。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"fmt"
	"log"
	"net"

	"github.com/fing/grpc-go/greet/greetpb"

	"google.golang.org/grpc"
)

type server struct{}

func main() {
	fmt.Println("Hello world Server")
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	greetpb.RegisterGreetServiceServer(s, &server{})

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

要构建服务器,我们:

  1. 指定要用于侦听客户端请求的端口 lis, err := net.Listen("tcp", ":50051")
  2. 创建 gRPC 服务器的实例 grpc.NewServer()
  3. 在 gRPC 服务器上注册我们的服务实现 greetpb.RegisterGreetServiceServer(s, &server{})
  4. 调用服务器实例 Server() 来启动侦听

添加基本的 Client

同样的,创建 client.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"log"

	"github.com/fing/grpc-go/greet/greetpb"

	"google.golang.org/grpc"
)

func main() {
	fmt.Println("Hello I'm client")
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	c := greetpb.NewGreetServiceClient(conn)
	fmt.Println("Created client: %f", c)
}

在这里,我们:

  1. 使用 grpc.Dial() 创建 gRPC channel
  2. 创建 GreetService 的客户端 greetpb.NewGreetServiceClient()

测试 Server 和 Client:我们可以用 go run 测试运行刚刚创建的 server.goclient.go,尽管我们并没有实现 GreetService 的具体细节,但这样可以测试是否能成功连接 gRPC 的服务器和客户端。

实现 Unary Call

Unary Call 顾名思义,就是简单的发送请求与回应。

定义 Request/Response 消息

在protobuf 文件 grpc-go/greet/greetpb/greet.proto 中添加不同的 message 以及声明 GreetService 中 RPC 函数 Greet(),使得它发送 GreetingRequest,并等待接收 GreetingResponse 的消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

package greet;
option go_package="greet/greetpb";

message Greeting {
    string first_name = 1;
    string last_name = 2;
}

message GreetingRequest {
    Greeting greeting = 1;
}

message GreetingResponse {
    string result = 1;
}

service GreetService {
    // Unary
    rpc Greet(GreetingRequest) returns (GreetingResponse) {};
}

别忘了再次使用 protoc 编译我们修改过的 .proto 文件。

实现 RPC 函数

之后要在 server.go 中实现 Greet() 函数,我们可以从生成好的 greet.pb.go 中找到这个接口的定义

1
2
3
func (*UnimplementedGreetServiceServer) Greet(context.Context, *GreetingRequest) (*GreetingResponse, error) {
	...
}

将其复制到 server.go 中,将具体的变量名放进去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (*server) Greet(ctx context.Context, req *greetpb.GreetingRequest) (*greetpb.GreetingResponse, error) {
	fmt.Printf("Greet function was invoked with %v", req)

	firstName := req.GetGreeting().GetFirstName()
	result := "Hello " + firstName
	res := &greetpb.GreetingResponse{
		Result: result,
	}
	return res, nil
}

之后可以在 client.go 里调用 Greet() 了。可以在 greet.pb.go 文件里找到函数的定义

1
Greet(ctx context.Context, in *GreetingRequest, opts ...grpc.CallOption)

client.gomain() 函数中创建 Request

1
2
3
4
5
6
req := &greetpb.GreetingRequest{
	Greeting : &greetpb.Greeting {
		FirstName: "James",
		LastName: "Bond",
	},
}

即可像下面一样调用 client 的 Greet() 并且传递 Request 信息

1
c.Greet(context.Background(), req)

之后再分别运行 server.goclient.go 即可看到 RPC 调用结果

1
2
3
4
5
6
7
root@d90ef922fdfd:/go/src/github.com/fing/grpc-go# go run greet/greet_server/server.go 
Hello world Server
Greet function was invoked with greeting:{first_name:"James" last_name:"Bond"}

root@d90ef922fdfd:/go/src/github.com/fing/grpc-go# go run greet/greet_client/client.go 
Hello I'm client
2020/06/04 04:25:24 Response from Greet: Hello James

附录

下面附上完整的代码,主要参考了官方的 Helloword 示例

client.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/fing/grpc-go/greet/greetpb"

	"google.golang.org/grpc"
)

func main() {
	fmt.Println("Hello I'm client")
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	c := greetpb.NewGreetServiceClient(conn)

	req := &greetpb.GreetingRequest{
		Greeting: &greetpb.Greeting{
			FirstName: "James",
			LastName:  "Bond",
		},
	}
	res, err := c.Greet(context.Background(), req)
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Response from Greet: %v", res.Result)
}

server.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"github.com/fing/grpc-go/greet/greetpb"

	"google.golang.org/grpc"
)

type server struct{}

func (*server) Greet(ctx context.Context, req *greetpb.GreetingRequest) (*greetpb.GreetingResponse, error) {
	fmt.Printf("Greet function was invoked with %v", req)

	firstName := req.GetGreeting().GetFirstName()
	result := "Hello " + firstName
	res := &greetpb.GreetingResponse{
		Result: result,
	}
	return res, nil
}

func main() {
	fmt.Println("Hello world Server")
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	s := grpc.NewServer()
	greetpb.RegisterGreetServiceServer(s, &server{})

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

参考链接:

加载评论