0%

grpc的原理与实战

1. grpc原理

1.1 通信方式

gRPC 允许你定义四类服务方法:

  1. 简单RPC(Simple RPC)

    即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

    1
    2
    rpc SayHello(HelloRequest) returns (HelloResponse){
    }
  2. 服务端流式RPC(Server-side streaming RPC):

    一个请求对象,服务端可以传回多个结果对象。即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

    1
    2
    rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
    }
  3. 客户端流式RPC(Client-side streaming RPC)

    客户端传入多个请求对象,服务端返回一个响应结果。即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

    1
    2
    rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
    }
  4. 双向流式RPC(Bidirectional streaming RPC)

    结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象。即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写。

    1
    2
    rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
    }

1.2 超时

go里面一般会使用Context进行超时控制以及参数传递, 其中超时控制可以使用context.WithDeadline()或者context.WithTimeout()实现, 二者实现效果是一致的。

gRPC基本上所有的对外函数都是带context参数的, 所以说它默认就集成了context的功能, 我们只需要在调用方法的时候传入 ctx 参数便可。

  • WithTimeout 只能设置在某一段时间后超时,比如3秒后超时。

  • WithDeadline() 则可以设置到具体某个时间点, 比如在临晨0点10分20秒的时候返回。

1.3 重试

gRPC 中已经内置了 retry 功能,客户端需要通过grpc.WithDefaultServiceConfig()配置 retry 功能, 并且设置环境变量export GRPC_GO_RETRY=on

  • client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
// method 可以不指定 即当前service下的所以方法都使用该配置。
retryPolicy := `{
"methodConfig": [{
"name": [{"service": "pb.Greeter","method":"SayHello"}],
"retryPolicy": {
"MaxAttempts": 4,
"InitialBackoff": ".01s",
"MaxBackoff": ".01s",
"BackoffMultiplier": 1.0,
"RetryableStatusCodes": [ "UNAVAILABLE" ]
}
}]}`

conn, err := grpc.Dial(address,grpc.WithDefaultServiceConfig(retryPolicy))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
}

1.4 拦截器

拦截器(interceptor)是grpc提供给开发者便于在每个RPC请求前中后进行如认证、鉴权、统计等功能的机制,熟悉Gin框架的同学的可以把拦截器理解为gin.HandlerFunc中间件,只不过grpc由于是双向通信,所以客户端和服务端都可以使用interceptor。

拦截器主要划分为两种,一元拦截器(unary interceptor)和流拦截器(stream interceptor),区别就是在于有没有rpc使用stream。

2. 安装使用

2.1 下载protoc 生成器

通过proto文件,生成相关代码。下载地址:https://github.com/protocolbuffers/protobuf/releases

例如我下载的是 protoc-3.9.0-osx-x86_64.zip

1
2
3
4
cd protoc-3.9.0-osx-x86_64 
cp -r include/ /usr/local/include/
cp -r bin/ /usr/local/bin/
protoc --version

2.2 proto文件

helloworld.proto 文件如下

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

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;
}

2.3 golang使用

1
2
go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

我们去 $GOPATH/src/google.golang.org/grpc/examples 看helloworld例子

1
cd $GOPATH/src/google.golang.org/grpc/examples/helloworld

我们通过.proto文件生成golang文件, 生成后的文件不要编辑。

1
2
3
4
protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld

# 获取进入到helloworld目录里
protoc helloworld.proto --go_out=plugins=grpc:.

运行后生成了 helloworld.pb.go

client 和 server 调用

我们看下greeter_server 的代码并且启动: go run greeter_server/main.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
package main

import (
"context"
"log"
"net"

"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
port = ":50051"
)

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

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

func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

我们看下greeter_client的代码并且启动:go run greeter_client/main.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
package main

import (
"context"
"log"
"os"
"time"

"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
address = "localhost:50051"
defaultName = "world"
)

func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure())
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
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.Message)
}

If things go smoothly, you will see the Greeting: Hello world in the client side output.

Congratulations! You’ve just run a client-server application with gRPC.

增加新的方法实现

  1. 在proto文件里增加 rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}的定义

  2. 通过protoc重新生成golang文件, 生成的文件里增加了SayHelloAgain相关方法。

  3. 修改server文件增加方法来实现接口

1
2
3
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.Name}, nil
}
  1. 修改client方法增加调用
1
2
3
4
5
r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.Message)

2.4 cpp 版本

参考: https://github.com/grpc/grpc/blob/master/BUILDING.md

1
2
3
4
5
6
7
8
9
10
11
12
# 安装xcode command line tools
sudo xcode-select --install
# 先安装必备的软件
brew install autoconf automake libtool shtool gflags

# 下载官方代码并更新依赖
git clone https://github.com/grpc/grpc
git submodule update --init

# 编译并且安装
LIBTOOL=glibtool LIBTOOLIZE=glibtoolize make
sudo make install

我们先进入hellworld例子

1
2
3
4
5
cd examples/cpp/hellworld/
make

# make过程中可以看到会先生成helloworld.pb.cc
protoc -I ../../protos --cpp_out=. ../../protos/helloworld.proto

还可以这样生成:

1
2
protoc --cpp_out=. helloworld.proto
protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` helloworld.proto

2.5 python版本

1
2
3
sudo python3 -m pip install grpcio
sudo python3 -m pip install grpcio-tools
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. helloworld.proto

3. 参考资料

可以加首页作者微信,咨询相关问题!