注:本文是在作者本人的同意下进行翻译的,如无本人同意,不支持任何商业的和非商业的转载【授权见文末】。

这是一篇介绍我们是如何在 Go 语言中使用 gRPC (和 Protobuf) 从而构建稳定的 CS 系统的技术文章。

在这里不会介绍为什么选择 gRPC 作为客户端与服务器端之间的主要通信协议,确实已经有不少很不错的的文章在讲这些东西了,例如这些:

从大方面来说,我们正在使用 Golang 构建我们的 C/S 系统,同时需要他足够快,足够可靠,足够具有伸缩性(这也是为什么我们选择gRPC)。我们希望客户端与服务端之间的通信内容越少越好,越安全越好,并且客户端和服务器使用的模式要一致(这也是为什么选择 Protobuf )。

此外,我们同时也希望能够在服务端暴露其他类型的接口,因为有些客户端无法使用 gRPC:例如暴露传统的 REST 接口,我们希望这个需求(几乎)不需额外的代价。

概述

我们将开始使用 Go 构建一个非常简单的C/S系统,系统将会在客户端、服务端之间交换虚拟的消息。在完成第一步,客户端、服务端之间理解了对方的消息后,我们将会加入其它的特性,例如 TLS 支持,认证,以及一个 REST API。

文章的接下去部分假设你具有基本的Go语言编程能力。同时,也假设你已经安装了 protobuf 包,protoc 命令是可用的(再次说明,已经有很多文章涵盖了介绍如何安装的主题,同时,这里也有一份 官方文档)。

你也需要安装Go依赖库,例如 protobuf的go实现,还有 gRPC网关

这篇文章中的所有代码在:https://gitlab.com/pantomath-io/demo-grpc。你可以随意使用这个仓库,并通过标签来导航、定位。这个仓库应当放置在你 $GOPATHsrc 目录:

$ cd $GOPATH/src mkdir pantomath-io git clone https://gitlab.com/pantomath-io/demo-grpc go get -v -d gitlab.com/pantomath-io/demo-grpc   $ cd $GOPATH/src/gitlab.com/pantomath-io/demo-grpc

协议文件

首先,我们需要定义协议。也就是定义在客户端和服务端我们能交互些什么,以及如何交互。这就是 Protobuf 发挥作用的地方。它让你定义两类东西:服务( Service )和消息( Message )。一个service是服务端的一组动作集合,服务端针对客户端的请求执行并产生不同的响应。一个 message 就是一个客户端请求的内容。简单来说,你可以认为 service 定义了动作,而 message 定义了对象

api/api.proto 中写入如下内容:

syntax = "proto3";package api;message PingMessage {  string greeting = 1;}service Ping {  rpc SayHello(PingMessage) returns (PingMessage) {}}

可以看到,这里定义了两个结构:一个是名为 Pingservice,暴露了一个叫做 SayHellomethod,这个方法接收了一个叫做 PingMessage 的输入参数,并返回一个结果 PingMessage ;另外还有一个叫做 PingMessagemessage,这个 message 只有一个单一的字段 greeting,这个字段的类型是 string

同时,从文档中也说明了,这里使用的是 proto3 的规范,它和 proto2 有所区别(详见文档

事实上,这个文件其实现在是不能用的 —— 它需要被编译。所谓的编译 proto 文件,其实就是生成你想要的目标语言的代码,也就是你项目中所使用的编程语言。

在 shell 中,切换到你的项目目录,并执行以下命令:

protoc -I api/ \
    -I${PROTO_PATH} \
    --go_out=plugins=grpc:api \
    api/api.proto

 这条命令会自动生成文件 api/api.pb.go,它内部已经实现了应用所需要的关于 gRPC 的 Go 代码,你可以阅读并且使用它,但切记不要去人工修改(每次你执行protoc命令,他都将被覆盖掉)。

同时您还需要定义由 service Ping 所调用的函数,因此请创建一个名为 api/handler.go 的文件:

// Server represents the gRPC servertype Server struct {}// SayHello generates response to a Ping requestfunc (s *Server) SayHello(ctx context.Context, in *PingMessage) (*PingMessage, error)  log.Printf("Receive message %s", in.Greeting)  return &PingMessage{Greeting: "bar"}, nil}

简单的服务器

现在你已经有了一个协议,是时候创建一个简单的服务器来实现 service 和理解 message 了,就拿起你最喜欢的编辑器来创建文件 server/main.go

package mainimport (  "fmt"  "log"  "net"// main start a gRPC server and waits for connection  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 7777))  if err != nil     log.Fatalf("failed to listen: %v", err)  }  // create a server instances := api.Server{}  // create a gRPC server object  grpcServer := grpc.NewServer()  // attach the Ping service to the server  api.RegisterPingServer(grpcServer, &s)  // start the server  if err := grpcServer.Serve(lis); err != nil {    log.Fatalf("failed to serve: %s", err)  }}

让我来分解一下代码,让你能够理解得更清晰一些:

然后你就可以通过编译您的代码来获取服务器二进制文件了:

$ go build -i -v -o bin/server gitlab.com/pantomath-io/demo-grpc/server

简单的客户端

客户端也需要导入 api 包,以便 messageservice 可用,创建文件client/main.go:

这次代码的分解就简单多了:

你现在可以通过编译您的代码以获取客户端二进制文件:

$ go build -i -v -o bin/client gitlab.com/pantomath-io/demo-grpc/client

客户端-服务器交互

您刚刚构建了一个客户端和一个服务器,是时候在两个终端中对它们进行了测试了:

$ bin/server
2006/01/02 15:04:05 Receive message foo

$ bin/client
2006/01/02 15:04:05 Response from server: bar

减轻你工作的工具

现在 API、客户端和服务器都可以工作了,您可能更喜欢使用 Makefile 来编译代码、清理文件夹和管理依赖关系等。

在项目文件夹的根目录下创建一个 Makefile。解释这个文件超出了这篇文章的范围,它主要使用你之前已经产生的编译命令。

要使用Makefile ,请尝试调用以下内容:

$ make help
api                            Auto-generate grpc go sources
build_client                   Build the binary file for client
build_server                   Build the binary file for server
clean                          Remove previous builds
dep                            Get the dependencies
help                           Display this help screen

加密通信

客户端和服务器是通过 HTTP/2gRPC上的传输层)相互通信。这些消息是二进制数据(感谢 Protobuf),但通信是纯文本的。幸运的是,gRPC 具有 SSL/TLS 集成功能,可用于从客户端角度对服务器进行身份验证,并对消息交换进行加密。

你不需要改变任何协议:它仍然是一样的。这些更改发生在客户端和服务器端的 gRPC 对象创建中。请注意,如果您仅更改一侧,则连接将不会起作用。

在更改代码中的任何内容之前,您需要创建一个自签名SSL证书。这个帖子的目的不是为了解释如何做到这一点,但OpenSSL官方文档(genrsa,req,x509)可以回答你的问题(DigitalOcean 也有一个很好的和完整的教程)。同时,您可以使用该文件 cert 夹中提供的文件。以下命令已用于生成文件:

$ openssl genrsa -out cert / server.key 2048 
$ openssl req -new -x509 -sha256 -key cert / server.key -out cert / server.crt -days 3650 
$ openssl req -new -sha256 -key cert / server。 key -out cert / server.csr 
$ openssl x509 -req -sha256 -in cert / server.csr -signkey cert / server.key -out cert / server.crt -days 3650

您可以继续并更新服务器定义以使用证书和密钥:

那么改变了什么?

请注意,这 grpc.NewServer() 是一个可变参数函数,所以您可以传递任意数量的结尾参数,这里您创建了一系列选项,以便稍后添加其他选项。

如果你现在已经编译好了你的服务端程序,并使用之前的客户端程序,那他们两者之间的连接将无法工作,他们两边都会抛出error。

2006/01/02 15:04:05 grpc: Server.Serve failed to complete security handshake from "localhost:64018": tls: first record does not look like a TLS handshake
2006/01/02 15:04:05 transport: http2Client.notifyError got notified that the client transport was broken read tcp localhost:64018->127.0.0.1:7777: read: connection reset by peer.
2006/01/02 15:04:05 Error when calling SayHello: rpc error: code = Internal desc = transport is closing

您需要在客户端使用完全相同的证书文件。所以编辑 client/main.go 文件:

客户端的更改与服务器上的更改几乎相同:

两边都使用了 credentials,所以他们应该能够像以前一样说话,但是要以加密方式。现在再重新编译代码::

$ make

并在两个独立的终端中运行双方:

$ bin/server
2006/01/02 15:04:05 Receive message foo

$ bin/client
2006/01/02 15:04:05 Response from server: bar

客户端标识

gRPC 服务器的另一个有趣功能是拦截来自客户端的请求。客户端可以在传输层上注入信息。您可以使用该功能来识别您的客户端,因为 SSL 实现通过证书验证服务器,但不验证客户端(所有客户端都使用相同证书)。

因此,您需要更新客户端,以便在每个调用上注入元数据(如登录名和密码),并在服务器端为每个调用检查这些凭据。

在客户端,你只需在你的 grpc.Dial() 调用中指定一个 DialOption,但是这个 DialOption 有一些限制,编辑你的 client/main.go(链接:file) 文件:

客户端在调用服务器时需要额外的数据,但服务器现在不知道,所以你需要告诉他检查这些元数据,打开 server/main.go 并更新它:

译者注:不希望堆代码,可以新 Tab 打开看 Code

再次,让我为你分解这件事:

现在你可以编译代码了:

$ make

并在两个独立的终端中运行双方:

显然,你的认证逻辑可以是更加聪明的,可以使用数据库,而不是使用凭证。方便的做法是,你的认真该函数获取到你的Server对象,而在你的这个Server结构中,能够保存了你数据库的句柄。

提供 REST

最后一件事:你有一个漂亮的服务器,客户端和协议; 序列化,加密和认证。但是有一个重要的限制:您的客户端需要符合gRPC标准,即在 受支持的平台列表 中。为了避免这种限制,我们可以将服务器打开到REST网关,以允许REST客户端执行请求。幸运的是,有一个 gRPC protoc 插件 用于生成将 RESTful JSON API 转换为 gRPC 的反向代理服务器。我们可以使用几行纯 Go 代码作为反向代理服务:

让我们在你的 api/api.proto 文件中加入一些额外信息

引入的 annotations.proto 能够让 protoc 理解在文件后面设置的 optionoption 则定义了这个方法指定调用路径:

更新你的 Makefile文件从而加入新的编译目标:

+++++++ API_REST_OUT:=“api / api.pb.gw.go” 
+++++++ api: api/api.pb.go api/api.pb.gw.go ## Auto-generate grpc go sources

+++++++ api / api.pb.go:api / api.proto 
+++++++   @protoc -I api / \ 
+++++++     -I $ {GOPATH} / src \ 
+++++++     -I $ {GOPATH} /src/github.com/grpc-ecosystem/grpc-gateway/

生成为gateway准备的Go代码(和api/api.pb.go类似,将会生成api/api.pb.gw.go文件 – 不要编辑它,在编译的时候他会自动更新)

$ make api

服务端的改变更重要。grpc.Server()是一个阻塞型调用,他只会在发生错误时返回(或者是被信号kill掉的时候)。因为我们需要启动另外一个服务端(提供REST服务),因此我们需要是的这个调用是非阻塞的。幸运的是,我们可以使用goroutines来达到这个目的。并且在认证的时候,也有一些小技巧。因为,REST gateway仅仅是一个反向代理,在gRPC的角度来看,他实际上是一个gRPC的客户端,因此,当他与服务端建立链接的时候,也需要使用WithPerRPCCredentials选项。

这里是你的 server/main.go(点击可查看)

那这里究竟发生了什么呢?

现在构建整个项目,以便测试 REST 接口:

$ make

并在两个独立的终端中运行双方:

还剩一个 swag…

REST 形式的网关是很 cool 的,但是,如果能直接从他生成文档,岂不是更 cool,对吗?

通过使用 protoc插件 来生成 swagger json文件,你可以很容易的做到:

protoc -I api/ \
  -I${GOPATH}/src \
  -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --swagger_out=logtostderr=true:api \
  api/api.proto

这将会生成api/api.swagger.json文件。像其他有Protobuf编译生成的文件一样,你不应该手动编辑它,但你可以使用它,同时,你也可以通过修改你的定义文件来编译更新他。

你可以把上述编译命令放入到 Makefile 中。

总结

你已经拥有了一个完整功能的gRPC客户端和服务端,他具有SSL加密、身份认证、客户端标识,以及REST网关(并包含swagger文件)等功能。那接下来,应该干什么呢?

你可以在REST网关上再添加一些新的功能,让他支持HTTPS,而不是HTTP。显然,你还可以在你的Protobuf上添加更加复杂的数据结构,增加更多的service。你也可以从HTTP/2的特性中获益,例如从客户端到服务端,或者从服务端到客户端,甚至是双向的流式特性。(当然,这个特性是仅仅针对gRPC的,REST是基于HTTP/1.1,无此特性)

非常感谢 Charles Francoise,他和我一同完成这篇文章,并编写了示例代码:https://gitlab.com/pantomath-io/demo-grpc.

译者声明