golang gRPC 入门

本文详细介绍了gRPC入门的全过程,包括安装Go语言和gRPC,定义protobuf文件,编译并运行服务端和客户端代码。强调了在处理流式RPC时的数据大小限制,展示了如何设置接收消息的最大大小。通过实例解析了gRPC的工作原理,是新手学习gRPC的实用指南。
摘要由CSDN通过智能技术生成

golang gRPC 入门

网上有不少的页面都提供 golang gRPC 的简单例子,但是大都有些问题:

  • 给出的例子可以看,但是自己运行总是失败;
  • 不告诉大家怎么配置环境,执行什么命令,直接就讲 gRPC 语法,不疼不痒;
  • 关键步骤不告诉大家为什么这么做,就是贴代码;

新手最需要的是手把手教,否则挫折感会让他失去尝试的信心。网上的文章,要么是新手抄来抄去,要么老手不屑于写,导致文档质量奇差无比。

1. 安装 golang

go 语言是一个编译型的语言,一旦编译(linux)好后,就可以独立运行,没有任何附加依赖。这比 python 的部署方便太多,以前从事 openstack 开发,最怕解决依赖、部署环境的问题。基于 golang 的 k8s 的部署,比 openstack 简单无数倍,很少出现依赖的问题。

golang 语言编译器等本身也仅仅是一个可执行文件,因此安装十分方便:

# 创建下载目录
[root@localhost /]# mkdir /root/jack/ && mkdir /root/go && cd /root/jack

# 下载 golang
[root@localhost jack]# wget https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz

# 解压
[root@localhost jack]# tar -zxvf go1.13.4.linux-amd64.tar.gz

# 设置必要的环境变量
[root@localhost jack]# export GOPATH=/root/go
[root@localhost jack]# export PATH=$PATH:/root/jack/go/bin/:/root/go/bin

# 检查是否安装成功
[root@localhost /]# go version
go version go1.13.4 linux/amd64

2. 安装 gRPC 和 protoc 编译器

# go 使用 grpc 的 SDK
[root@localhost /]# go get google.golang.org/grpc

# 下载 protoc 编译器
[root@localhost jack]# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.10.1/protoc-3.10.1-linux-x86_64.zip
[root@localhost jack]# cp bin/protoc /usr/bin/
[root@localhost jack]# protoc --version
libprotoc 3.10.1

# 安装 protoc go 插件
[root@localhost jack]# go get -u github.com/golang/protobuf/protoc-gen-go

3. 定义 protobuf 文件

[root@localhost grpc-example]# cat /root/jack/grpc-example/service.proto
syntax = "proto3";
package test;

message StringMessage {
    repeated StringSingle ss = 1;
}

message StringSingle {
    string id = 1;
    string name = 2;
}

message Empty {

}

service MaxSize {
  rpc Echo(Empty) returns (stream StringMessage) {};
}

4. 编译 proto 文件

# 创建项目的文件夹
# 创建 src/test 的目的是我们在 proto 文件中,填写了 package test; 因此,编译出来的 go 文件属于 test project。
# 创建 src 是 go 语言的标准,go 语言通过 $GOPATH/src/ 下寻找依赖。
[root@localhost /]# mkdir -p /root/jack/grpc-example/src/test
[root@localhost /]# mkdir -p /root/jack/grpc-example/server
[root@localhost /]# mkdir -p /root/jack/grpc-example/client

# 将 protobuf 文件写入 /root/jack/grpc-example/proto/service.proto 。

# 执行
[root@localhost /]# cd /root/jack/grpc-example/src/test
[root@localhost test]# protoc --go_out=plugins=grpc:. service.proto

# 多出来一个文件
[root@localhost proto]# ll
total 16
-rw-r--r-- 1 root root 8664 Nov 11 16:02 service.pb.go
-rw-r--r-- 1 root root  254 Nov 11 16:00 service.proto

# 看一下 service.pb.go 文件的片段
package test

import (
        context "context"
        fmt "fmt"
        proto "github.com/golang/protobuf/proto"
        grpc "google.golang.org/grpc"
        codes "google.golang.org/grpc/codes"
        status "google.golang.org/grpc/status"
        math "math"
)

# 使用的包为我们之前安装的 grpc/protobuf

5. 编写 server 端代码

package main

import (
	"log"
	"math"
	"net"
	"strconv"

	"google.golang.org/grpc"

	"grpcproto/pb"
)

// 参考 /root/go/src/google.golang.org/grpc/examples/route_guide

// 定义了一个空的结构体,这是go语言的一个技巧。
type server struct{}

// Echo函数是server类的一个成员函数。
// 这个server类 必须实现 proto文件中定义的所有rpc方法。

/*
// 在service.pb.go文件中,MaxSizeServer由proto中service名称MaxSize + Server后缀拼成的!
// MaxSizeServer is the server API for MaxSize service.
// MaxSizeServer 是一个interface,只要实现了Echo方法,就是这个interface的实现。
// 我们的 func (s *server) Echo(in *pb.Empty, stream pb.MaxSize_EchoServer) error 实现了这个接口,注意:参数、返回值保持与定义的一样。
type MaxSizeServer interface {
    Echo(*Empty, MaxSize_EchoServer) error
}
*/

func (s *server) Echo(in *pb.Empty, out pb.MaxSize_EchoServer) error {
	// proto 中定义 rpc Echo(Empty) returns (stream StringMessage) {};
	/*
	   in *pb.Empty 就是 Empty。
	   out pb.MaxSize_EchoServer 是提供给用户的,能够调用send的一个object,这个是精妙的设计。提供给用户的
	   该代码中,要组织 *StringMessage 类型的返回值,使用out.send发送出去。

	   注意:pb 是我们引用包的代号,import pb "grpcproto/pb"

	   那么 pb.Empty 是什么呢?
	   // service.pb.go 定义的
	   type Empty struct {
	       XXX_NoUnkeyedLiteral struct{} `json:"-"`
	       XXX_unrecognized     []byte   `json:"-"`
	       XXX_sizecache        int32    `json:"-"`
	   }

	   那么 pb.MaxSize_EchoServer 是什么?
	   // service.pb.go 定义的
	   type MaxSize_EchoServer interface {
	       Send(*StringMessage) error
	       grpc.ServerStream
	   }

	   但是,是否有人实现了这个接口呢?当然
	   // 在 service.pb.go 中:
	   type maxSizeEchoServer struct {
	       grpc.ServerStream
	   }

	   func (x *maxSizeEchoServer) Send(m *StringMessage) error {
	       return x.ServerStream.SendMsg(m)
	   }

	   从此,可知,pb.MaxSize_EchoServer 有send方法,可以将StringMessage发送出去。

	   那么 pb.StringMessage 是什么呢?
	   // service.pb.go 定义的
	   type StringMessage struct {
	       Ss                   []*StringSingle `protobuf:"bytes,1,rep,name=ss,proto3" json:"ss,omitempty"`
	       XXX_NoUnkeyedLiteral struct{}        `json:"-"`
	       XXX_unrecognized     []byte          `json:"-"`
	       XXX_sizecache        int32           `json:"-"`
	   }

	   注意:Ss和proto中的:
	   message StringMessage {
	       repeated StringSingle ss = 1;
	   }

	   有十分重大的关系,因为是repeated,所以是Ss []*StringSingle。
	*/

	log.Printf("Received from client")
	var err error
	list := pb.StringMessage{}
	for i := 0; i < 5; i++ {
		feature := pb.StringSingle{
			Id:   "id" + strconv.Itoa(i),
			Name: "jack",
		}
		list.Ss = append(list.Ss, &feature)
	}
	err = out.Send(&list)

	// 函数要求返回 error 类型
	return err
}

func run() error {
	sock, err := net.Listen("unix", "/tmp/lib/test.socket")
	if err != nil {
		return err
	}

	var options = []grpc.ServerOption{
		grpc.MaxRecvMsgSize(math.MaxInt32),
		grpc.MaxSendMsgSize(1073741824),
	}
	s := grpc.NewServer(options...)

	myServer := &server{}
	/*
	   在service.pb.go中
	   func RegisterMaxSizeServer(s *grpc.Server, srv MaxSizeServer) {
	       s.RegisterService(&_MaxSize_serviceDesc, srv)
	   }
	   前者是grpc server,后者是实现了MaxSizeServer中所有方法的实例,即 &server{}。
	   注册方法将grpc server 和 handler绑定在了一起。

	   RegisterMaxSizeServer的命名规则为:Register(注册) + MaxSize(service MaxSize {} in proto 文件) + Server(服务器)
	*/
	pb.RegisterMaxSizeServer(s, myServer)

	/*
	   在s.Serve(sock)上监听服务
	*/
	if err := s.Serve(sock); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
	return nil
}

func main() {
	err := run()
	if err != nil {
		log.Fatalf("main run failed: %v", err)
	}
}

6. 编写 client 端代码

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"grpcproto/pb"

	"google.golang.org/grpc"
)

func main() {
	// 通过grpc.Dial获得一个连接。
	conn, err := grpc.Dial("unix:///tmp/lib/test.socket", grpc.WithInsecure())
	// 如果要增加Recv可以接受的一个消息的数据量,必须增加 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100000000))
	//conn, err := grpc.Dial("unix:///var/lib/test.socket", grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100000000)))
	if err != nil {
		log.Fatalf("fail to dial: %v", err)
	}
	defer conn.Close()

	/*
	   在service.pb.go中

	   // 接口 interface
	   type MaxSizeClient interface {
	       Echo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (MaxSize_EchoClient, error)
	   }

	   type maxSizeClient struct {
	       cc *grpc.ClientConn
	   }

	   // 传入一个连接,返回一个 maxSizeClient结构体 的实例。这个实例实现了MaxSizeClient接口的Echo方法,是MaxSizeClient接口的一个实例。
	   func NewMaxSizeClient(cc *grpc.ClientConn) MaxSizeClient {
	       return &maxSizeClient{cc}
	   }

	   注意名字,NewMaxSizeClient = New + MaxSize(service MaxSize {} in proto 文件)+ Client。
	*/
	client := pb.NewMaxSizeClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), 10000*time.Second)
	defer cancel()

	/*
	   在service.pb.go中,参数是 1 context,2 Empty,返回值是 MaxSize_EchoClient, error。
	   func (c *maxSizeClient) Echo(ctx context.Context, in *Empty, opts ...grpc.CallOption) (MaxSize_EchoClient, error) {
	       stream, err := c.cc.NewStream(ctx, &_MaxSize_serviceDesc.Streams[0], "/test.MaxSize/Echo", opts...)
	       if err != nil {
	           return nil, err
	       }
	       x := &maxSizeEchoClient{stream}
	       if err := x.ClientStream.SendMsg(in); err != nil {
	           return nil, err
	       }
	       if err := x.ClientStream.CloseSend(); err != nil {
	           return nil, err
	       }
	       return x, nil
	   }

	   // MaxSize_EchoClient是一个interface,包含Recv方法。
	   type MaxSize_EchoClient interface {
	       Recv() (*StringMessage, error)
	       grpc.ClientStream
	   }

	   type maxSizeEchoClient struct {
	       grpc.ClientStream
	   }

	   func (x *maxSizeEchoClient) Recv() (*StringMessage, error) {
	       m := new(StringMessage)
	       if err := x.ClientStream.RecvMsg(m); err != nil {
	           return nil, err
	       }
	       return m, nil
	   }
	*/

	//stream 是实现 MaxSize_EchoClient 的实例
	stream, err := client.Echo(ctx, &pb.Empty{})

	for {
		// stream 有一个最重要的方法,就是 Recv(),Recv 的返回值就是 *pb.StringMessage,这里面包含了多个 Ss []*StringSingle
		data, err := stream.Recv()
		if err != nil {
			fmt.Printf("error %v\n", err)
			return
		}
		fmt.Printf("%v\n", data)
	}

}

7. 执行 server & client

首先,将代码放置到正确的位置上。

# 将 server 端代码保存成 server.go,放置到 /root/jack/grpc-example/server 下
# 将 client 端代码保存成 client.go,放置到 /root/jack/grpc-example/client 下
# 修改 GOPATH
[root@localhost server]# export GOPATH=$GOPATH:/root/jack/grpc-example


# 代码结构为:
[root@localhost grpc-example]# pwd
/root/jack/grpc-example
[root@localhost grpc-example]# tree
.
├── client
│   └── client.go
├── server
│   └── server.go
└── pb
    ├── service.pb.go
    └── service.proto

4 directories, 4 files

然后,编译 server & client。

# 编译 server
[root@localhost server]# cd /root/jack/grpc-example
[root@localhost server]# go build -o server_grpc server/server.go
[root@localhost server]# ll
total 24670
-rwxr-xr-x  1 root  root    12M May 10 14:52 server_grpc
-rw-r--r--  1 root  root   4.0K May 10 14:35 server.go


# 编译 client
[root@localhost ~]# cd /root/jack/grpc-example/client/
[root@localhost client]# go build -o client_grpc client.go
[root@localhost client]# ll
total 24744
-rw-r--r--  1 root  root   2.8K Apr 29 19:58 client.go
-rwxr-xr-x  1 root  root    12M May 10 14:54 client_grpc

最后,运行 server & client。

# 打开两个 bash 窗口
# 第一个执行
[root@localhost ~]# cd /root/jack/grpc-example/server
# 清除之前的 unix socket,很重要!!!
[root@localhost server]# rm -rf /tmp/lib/test.socket
[root@localhost server]# ./server_grpc

# 第二个执行
[root@localhost ~]# cd /root/jack/grpc-example/client
[root@localhost server]# ./client_grpc

# 此时,两个窗口会出现交互的内容,实验成功
[root@localhost server]# ./server_grpc
2019/11/12 10:02:45 Received from client

[root@localhost client]# ./client_grpc
&StringMessage{Ss:[]*StringSingle{&StringSingle{Id:id0,Name:jack,},&StringSingle{Id:id1,Name:jack,},&StringSingle{Id:id2,Name:jack,},&StringSingle{Id:id3,Name:jack,},&StringSingle{Id:id4,Name:jack,},},}
error EOF

8. 总结

最好的参考文档不是网上的文档,而是 gRPC 的 example,它提供了所有最常见的操作,而且保证一定是最正确、最佳的实践方式。所以,需要进一步学习的同学一定要去看 /root/go/src/google.golang.org/grpc/examples/route_guide 下的例子,当然 /root/go 是我们示例的 GOPATH。

本文的初衷是一个被困扰的问题,gRPC 的 send/recv 的一条记录都是有最大长度的,

# /root/go/src/google.golang.org/grpc/server.go
const (
    defaultServerMaxReceiveMessageSize = 1024 * 1024 * 4
    defaultServerMaxSendMessageSize    = math.MaxInt32
)

默认可以发送一条非常大的记录,但是只能接受一条 4MB 的数据。对于什么是一条数据,我之前不是很了解,gRPC server 和 client 交互有 4 种模式

# 官方例子:/root/go/src/google.golang.org/grpc/examples/route_guide/routeguide/route_guide.proto
service RouteGuide {
  // A simple RPC.
  //
  // Obtains the feature at a given position.
  //
  // A feature with an empty name is returned if there's no feature at the given
  // position.
  // 传入一个 Point,得到一个返回的 Feature
  rpc GetFeature(Point) returns (Feature) {}

  // A server-to-client streaming RPC.
  //
  // Obtains the Features available within the given Rectangle.  Results are
  // streamed rather than returned at once (e.g. in a response message with a
  // repeated field), as the rectangle may cover a large area and contain a
  // huge number of features.
  // 传入一个 Rectangle,返回流式的 Feature,我们的例子就是这种模式;
  rpc ListFeatures(Rectangle) returns (stream Feature) {}

  // A client-to-server streaming RPC.
  //
  // Accepts a stream of Points on a route being traversed, returning a
  // RouteSummary when traversal is completed.
  // 传入流式的 Point,返回单个 RouteSummary
  rpc RecordRoute(stream Point) returns (RouteSummary) {}

  // A Bidirectional streaming RPC.
  //
  // Accepts a stream of RouteNotes sent while a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  // 双向都是流式的
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

对于第一种模式,大家都不会有任何疑问。对于第二种模式(传入一个 Point,返回流),我当时产生了疑惑,这种模式下:

client ----- send Point -----> server
   |                             |
   |                             |
   <--------- stream Feature-----

stream Feature 的意思就是大量的多个 Feature,这样久而久之,client Recv 的数据一定会超过 4MB,难道就会报错么?

实际上是我理解错误了,Recv 默认的 4MB 限制是指,整个流可以超过 4MB,但是单个 Feature 必须小于 4MB。

尝试修改 server.go 中的代码,并重新编译:

    # 将 server 发送的数量从 5 -> 5*1024*1024
    for i := 0; i < 5 * 1024 * 1024; i++ {
        feature := pb.StringSingle{
            Id:   "sssss",
            Name: "jack",
        }
        list.Ss = append(list.Ss, &feature)
    }

得到的结果是:

[root@localhost client]# ./client
error rpc error: code = ResourceExhausted desc = grpc: received message larger than max (83886080 vs. 4194304)

除此之外,还有一件非常重要的事情,就是 client 和 server 端都有 send/recv 的限制:

client(send limit) ---------> server(recv limit)
   |                             |
   |(recv limit)                 |(send limit)
   <------------------------------

因此,当遇到 received message larger than max (83886080 vs. 4194304) 错误的时候,一定要仔细分析,看是哪一端超过了限制,对于我们自己的代码例子来说:

  • client 发送请求是 Empty,因此肯定不会超过 math.MaxInt32 的限制。
  • server recv Empty,不会超过 defaultServerMaxReceiveMessageSize(4MB) 的限制。
  • server send stream StringMessage,每一个 StringMessage 为 83886080 Bytes,依然没有超过 math.MaxInt32 的限制。
  • client recv stream StringMessage 时,StringMessage 为 83886080 Bytes 超过了 4MB 的限制,因此报错。

因此,需要修改的是 client recv 的 limit:

conn, err := grpc.Dial("unix:///var/lib/test.socket", grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100000000)))

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值