Go 实现 gRPC 服务
介绍
最近学习了一下 gRPC 在 Go 中的实现,做一些记录。
gRPC 是什么
讲 gRPC 之前,可以先讲一下 RPC(Remote Procedure Call) 远程过程调用,它能让客户端直接类似于调用本地方法一样调用服务端的方法。gRPC 是 Google 开源的一款高性能 RPC 框架。
gRPC 中主要有四种请求/响应模式:
- 普通 RPC
- 服务端流式 RPC
- 客户端流式 RPC
- 服务端/客户端双向流式 RPC
gRPC 好处
RPC 一般是建立在 TCP 或者 HTTP 网络连接之上的,是一个框架,所以对于开发者而言,如果使用 RPC 去实现服务端与客户端的通信,基本可以不考虑网络协议、连接等内容,专注与处理逻辑。
使用 gRPC 的话,可以将我们的服务定义在 .proto 文件中,然后在任何一种支持 gRPC 的语言中实现客户端和服务端,这可以让服务端和客户端运行在不同的环境中,另外使用 gRPC 还有其他的好处:高效序列化与反序列化、简单的 IDL 语言、方便接口更新
基础使用
已官方的 helloworld 作为例子进行讲解,示例
准备工作
- Mac 安装 protoc 编译器
$ brew install protobuf
- 安装 protoc 编译器插件
$ go get -u github.com/golang/protobuf/protoc-gen-go
定义服务
gRPC 是通过 Protocol Buffers 去定义 gRPC 相关的 service 服务以及请求和响应相关的类型,如果对 Protocol Buffer 不熟悉的,其实也没什么关系,直接看 protoc 文件也是比较容易看懂的。如果对 Protocol Buffer 想深入了解的 ,可以参考这个链接: Protocol Buffers
创建 helloworld.proto 文件,并填写以下内容
syntax = "proto3";
package protos;
// 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;
}
来解释一下上面的代码:
- 第一行
syntax = "proto3"表示目前使用的是 proto3 的语法 - 通过 service 关键字定义了一个 Greeter 服务,且这个服务目前只有 SayHello 一个方法,SayHello 这个方法会接收一个 HelloRequest 类型的消息,并返回一个 HelloReply 类型的消息
- 通过 message 关键字定义了 HelloRequest 类型消息的结构是什么样的,上述代码中,HelloRequest 消息结构只有一个字段,是 string 类型
- 通过 message 关键字定义了 HelloReply 类型消息的结构是什么样的,上述代码中,HelloReply 消息结构只有一个字段,是 string 类型
- 定义消息结构字段的时候,需要指定字段的字段编号,比如
string name = 1中,1 就是字段编号,这个字段编号是一个唯一的数字,作用是在二进制消息体中表示字段用的 - 定义消息结构字段时,需要制定更多数据类型的话,可以参考链接:Language Guide (proto3)
在 proto 文件中完成定义 gRPC 的服务方法和消息之后,我们就可以来生成 gRPC 通信中服务端与客户端的接口了,这个时候就需要我们之前安装的 protoc 以及 protoc-gen-go 工具了
$ protoc --go_out=plugins=grpc:. helloworld.proto
--go_out 表示指定最终生成文件的输出路径,. 表示生成在当前路径下
## 注意这边如果使用 protoc --go_out=grpc:. hellororld.proto 命令生成的结果和指定了 plugins 生成的结果有不同,需要加上 plugins 去生成
命令执行完成之后,会生成 helloworld.pd.go,具体的内容因为篇幅原因,不完全展示了,可以参考:helloworld.pd.go
会根据 proto 文件中定义的服务方法和消息生成对应的代码,比如:
- 是消息的话会对应生成
- 对应的 struct 结构体
- Reset()/String()/ProtoMessage()/Descriptor()/XXX_Unmarshal()/XXX_Marshal()/XXX_Merge()/XXX_Merge()/XXX_DiscardUnknown() 方法
- 消息中定义字段的 get 方法
- 是服务的话会对应生成
- 生成服务对应的 server 以及 client 接口
- 接口对应的方法
编写服务端和客户端代码
gRPC 服务方法和消息定义完成,对应的服务端和客户端接口也定义完成之后,就需要来写服务端和客户端的具体实现代码了
服务端代码
这部分代码也是 grpc-go 官方 helloworld example 例子中的代码,借这个简单的代码理解一下服务端代码的编写
package main
import (
"context"
"log"
"net"
pb "github.com/Win-Man/sg-server/protos"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
// 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() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建一个 gRPC 服务端实例
s := grpc.NewServer()
// 注册
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
解释一下上面的代码
- 首先定义了一个 server 的结构体,这个没有指定字段,直接继承了生成的 helloworld.pb.go 中定义的 pb.UnimplementedGreeterServer 结构体
- 实现了 server 结构体的 SayHello 方法,SayHello 方法接受两个参数(ctx context.Context,in *pb.HelloRequest)返回两个参数(*pb.HelloReply,error),HelloRequest 和 HelloReply 其实就是 proto 中定义的消息对象,SayHello 方法的具体实现就看具体需求了,在例子中的话,就是获取了发送过来的请求中的 name 字段,将获取到的 name 值拼接上 Hello 以 Message 返回给客户端
- 在 main 函数中定义了服务端的启动和监听过程
客户端代码
这部分代码也是 grpc-go 官方 helloworld example 例子中的代码,借这个简单的代码理解一下客户端代码的编写
package main
import (
"context"
"log"
"os"
"time"
pb "github.com/Win-Man/sg-server/protos"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "sg"
)
func main() {
// 建立连接
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
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.
// 获取命令行中传入的第一个参数作为 name 值,否则name就使用默认值
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 设置超时时间
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())
}
解释一下上面的代码:
- 所有的逻辑都是在 main 函数中
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())这个是在建立客户端与服务端之间的 grpc 连接c := pb.NewGreeterClient(conn)通过连接初始化客户端r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})客户端通过调用 gRPC 方法与服务端通信,就收服务端返回的信息
运行演示
- 运行服务端代码
$ go run server.go
2020/03/29 00:19:48 Received: sg
2020/03/29 00:22:11 Received: hhhh
- 运行客户端代码
$ go run client.go
2020/03/29 00:19:48 Greeting: Hello sg
$ go run client.go hhhh
2020/03/29 00:22:11 Greeting: Hello hhhh
客户端成功通过 gRPC 调用了服务端。
进阶使用
简单使用中已经介绍了简单 RPC 的使用方式,在这一节中,会补充讲解 gRPC 其他几种请求/响应模式,标准步骤都是:
- 在 .proto 文件中定义 RPC 服务方法和消息
- 根据 .proto 文件生成 pb.go 文件
- 实现服务端代码
- 实现客户端代码
服务端流式 RPC
服务端流式 RPC 顾名思义就是服务端会按照多次返回响应,直到服务端认为响应发送完毕,会告诉客户端响应应发送完毕
- 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
service Greeter {
......
rpc TellMeSomething(HelloRequest) returns (stream Something){}
......
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
......
message Something{
int64 lineCode = 1;
string line = 2;
}
在原先定义的 Greeter 服务的基础上添加了一个 TellMeSomething 的 RPC 方法,这个 RPC 方法接收的是请求结构是 HelloRequest 类型,返回的结构是 Something 类型,且用了 stream 定义,表示返回结构是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段。
- 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
- 编写服务端代码,在服务端实现 TellMeSomething 方法
func (s *server)TellMeSomething(in *pb.HelloRequest, stream pb.Greeter_TellMeSomethingServer) error{
log.Printf("Received ServerStream request from: %v", in.GetName())
// 获取客户端发送的请求内容
hellostr := fmt.Sprintf("Hello,%s",in.GetName())
something := []string{hellostr,"ServerLine1","ServerLine2","ServerLine3"}
for i,v := range(something){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
return err
}
}
return nil
}
TellMeSomething 方法接受两个参数,一个是 *pb.HelloRequest 类型的参数,表示客户端发送的请求,另一个是 pb.Greeter_TellMeSomethingServer 类型的参数,这个参数其实就是服务端返回给客户端的流对象接口,生成的 .pb.go 文件中已经帮我们定义好了这个接口,包含一个 Send() 方法,用于发送响应给客户端,还有就是继承成 grpc.ServerStream 的流对象
type Greeter_TellMeSomethingServer interface {
Send(*Something) error
grpc.ServerStream
}
在服务端通过 pb.Greeter_TellMeSomethingServer 对象的 Send() 方法给客户端发送响应,如果发送所有响应结束,则通过 return nil 的方式通知客户端响应发送结束,客户端会接收到 io.EOF 信号知道服务端已经发送完毕。如果发送中间过程有错误,则通过 return err 的方式将对应的错误通知给客户端
- 编写客户端代码,在客户端调用 TellMeSomethinng 方法
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
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.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "ServerStream":
stream,err := c.TellMeSomething(ctx,&pb.HelloRequest{Name:name})
if err != nil{
log.Fatalf("TellMeSomething error: %v ",err)
}
for {
something,err := stream.Recv()
// 服务端信息发送完成,退出
if err == io.EOF{
break
}
if err != nil{
log.Fatalf("TellMeSomething stream error:%v",err)
}
log.Printf("Recevie from server:{LineCode:%v Line:%s}\n",something.GetLineCode(),something.GetLine())
}
......
}
客户端调用 TellMeSomething 方法,发送了一个 HelloRequest 类型的请求,获得一个 stream 返回对象,通过调用 Recv() 方法客户端接收服务端发送的一次次响应内容
客户端流式 RPC
客户端流式 RPC 就是客户端不断发送请求,直到发送完毕之后通知服务端请求发送完毕
- 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
service Greeter {
......
rpc TellYouSomething(stream Something) returns(HelloReply){}
......
}
......
// The response message containing the greetings
message HelloReply {
string message = 1;
}
message Something{
int64 lineCode = 1;
string line = 2;
}
在原先定义的 Greeter 服务的基础上添加了一个TellYouSomething 的 RPC 方法,这个 RPC 方法接收的是请求结构是 Something 类型,且用了 stream 定义,表示接受的请求是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段,服务端返回给客户端一个 HelloReply 类型的响应
- 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
- 编写服务端代码,在服务端实现 TellYouSomething 方法
func (s *server)TellYouSomething(stream pb.Greeter_TellYouSomethingServer) error{
log.Printf("Recevied ClientStream request")
messgeCount := 0
length := 0
for {
something,err := stream.Recv()
// 接收到客户端所有请求,返回响应结果
if err == io.EOF{
return stream.SendAndClose(&pb.HelloReply{Message:fmt.Sprintf("It's over.\nReceived %v times. Length:%v",messgeCount,length)})
}
if err != nil{
return err
}
messgeCount ++
length += len(something.GetLine())
}
return nil
}
这个与服务端流式 RPC 中客户端的代码比较类似,服务端通过调用 stream 的 Recv() 方法获取客户端发送的请求,直到接受到 io.EOF 信号表示已经接收到全部客户端的请求,可以返回响应了,如果中间接收请求出错,则将错误直接返回给客户端
- 编写客户端代码,在客户端调用 TellYouSomething 方法
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
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.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "ClientStream":
stream,err := c.TellYouSomething(ctx)
if err != nil{
log.Fatalf("TellYouSomething err :%v",err)
}
clientStr := []string{"ClientLine1","ClientLine2"}
for i,v := range(clientStr){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
log.Fatalf("TellYouSomething stream error:%v",err)
}
}
rep,err := stream.CloseAndRecv()
if err != nil{
log.Fatalf("CloseAndRecd error:%v",err)
}
log.Printf("Recive from server:%s",rep.GetMessage())
......
}
与服务端流式 RPC 中服务端的代码类似,通过调用 stream 的 Send() 方法给客户端发送请求,等所有请求发送完毕通过调用 CloseAndRevc() 方法通知服务端请求发送完毕,并接收服务端的响应。
服务端/客户端双向流式 RPC
服务端/客户端双向流式 RPC 即客户端和服务端都是流式方式发送请求的
- 首先在 .proto 文件中定义服务端流式 RPC 的服务方法
service Greeter {
......
rpc TalkWithMe(stream Something) returns(stream Something){}
}
......
message Something{
int64 lineCode = 1;
string line = 2;
}
在原先定义的 Greeter 服务的基础上添加了一个TalkWithMe的 RPC 方法,这个 RPC 方法接收的是请求结构是 Something 类型,且用了 stream 定义,表示接受的请求是一个流,Something 类型包含一个 int64 类型的 lineCode 字段和以一个 string 类型的 line 字段,服务端返回给客户端的也是 Something 类型的 stream 流。
- 根据 .proto 文件生成 pb.go 文件,因为是自动生成的,这边就不展示完全的 pb.go 文件内容
- 编写服务端代码,在服务端实现 TalkWithMe 方法
func (s *server)TalkWithMe(stream pb.Greeter_TalkWithMeServer) error{
log.Printf("Recevied Stream request")
messageCount := 0
for {
something ,err := stream.Recv()
if err == io.EOF{
return nil
}
messageCount ++
length := len(something.GetLine())
line := fmt.Sprintf("Got %s,Length:%v",something.GetLine(),length)
if err := stream.Send(&pb.Something{LineCode: int64(messageCount),Line:line});err != nil{
return err
}
}
return nil
}
服务端代码与客户端流式 RPC 中服务端代码几乎是一样的,所以就不作解释
- 编写客户端代码,调用 TalkWithMe 方法
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
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.
name := defaultName
clientFunc := "Default"
if len(os.Args) == 3 {
name = os.Args[1]
clientFunc = os.Args[2]
}else{
log.Fatal("Please input 2 arguments")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
switch clientFunc {
......
case "Stream":
stream,err := c.TalkWithMe(ctx)
if err != nil{
log.Fatalf("TalkWithMe err:%v",err)
}
waitc := make(chan struct{})
go func(){
for {
something,err := stream.Recv()
// 服务端信息发送完成,退出
if err == io.EOF{
break
}
if err != nil{
log.Fatalf("TalkWithMe stream error:%v",err)
}
log.Printf("Got %v:%s\n",something.GetLineCode(),something.GetLine())
}
}()
clientStr := []string{"one","two","three"}
for i,v := range(clientStr){
if err := stream.Send(&pb.Something{LineCode:int64(i),Line:v});err != nil{
log.Fatalf("TalkWithMe Send error:%v",err)
}
}
stream.CloseSend()
<- waitc
default:
log.Fatal("Please input second args in Default/ServerStream/ClientStream/Stream")
}
}
客户端发送请求还是通过调用 stream 的 Send() 方法发送请求,但是通知服务端请求发送完毕是通过调用 CloseSend() 方法,不过因为服务端发送的请求不是一次性发送的,所以这边用 goroutine 新开了一个线程用于接收服务端返回的响应。
演示
- 服务端流式 RPC
## 终端 1
$ go run server.go
2020/03/29 19:50:47 Received ServerStream request from: Aob
## 终端 2
$ go run client.go Aob ServerStream
2020/03/29 19:50:47 Recevie from server:{LineCode:0 Line:Hello,Aob}
2020/03/29 19:50:47 Recevie from server:{LineCode:1 Line:ServerLine1}
2020/03/29 19:50:47 Recevie from server:{LineCode:2 Line:ServerLine2}
2020/03/29 19:50:47 Recevie from server:{LineCode:3 Line:ServerLine3}
- 客户端流式 RPC
## 终端 1
$ go run server.go
2020/03/29 19:51:47 Recevied ClientStream request
## 终端 2
$ go run client.go Aob ClientStream
2020/03/29 19:51:47 Recive from server:It's over.
Received 2 times. Length:22
- 服务端/客户端流式 RPC
## 终端 1
$ go run server.go
2020/03/29 19:52:46 Recevied Stream request
## 终端 2
$ go run client.go Aob Stream
2020/03/29 19:52:46 Got 1:Got one,Length:3
2020/03/29 19:52:46 Got 2:Got two,Length:3
2020/03/29 19:52:46 Got 3:Got three,Length:5
总结
以上就是通过 Go 学习 gRPC 的一些记录,只是简单跑通了服务端与客户端之间的请求。完整代码地址
一开始刚开始看 gRPC 的时候其实有点晕,因为又是 gRPC 又是 protocol 又是流式 RPC 的,但是实际学习下来发现定义简单的 gRPC 服务还是比较简单的,按照步骤走就可以:
- 在 .proto 文件中定义 RPC 服务方法和消息
- 根据 .proto 文件生成 pb.go 文件
- 实现服务端代码
- 实现客户端代码
下一步准备学习一下 gRPC 与 TLS 安全加密相关的内容