网上有很多的页面都提供 golang gRPC 的简单例子,可是有些问题:python
新手最须要的是手把手教,不然挫折感会让他失去尝试的信心。网上的文章要么是新手抄来抄去,要么老手不屑于写。致使文档质量奇差无比。linux
go 语言比较好的地方在于他是一个编译型的语言,一旦编译(linux)好后,就能够独立运行,没有任何附加依赖。这比 python 的部署方便太多,之前从事 openstack 开发,最怕解决依赖、部署环境的问题。基于 golang 的 k8s 的部署比 openstack 简单无数倍。不多出现依赖的问题。git
golang 语言编译器等自己也仅仅是一个可执行文件,所以安装十分方便:github
# 建立下载目录 [root@localhost /]# mkdir /root/lihao04/ && mkdir /root/go && cd /root/lihao04 # 下载 golang [root@localhost lihao04]# wget https://dl.google.com/go/go1.13.4.linux-amd64.tar.gz # 解压 [root@localhost lihao04]# tar -zxvf go1.13.4.linux-amd64.tar.gz # 设置必要的环境变量 [root@localhost lihao04]# export GOPATH=/root/go [root@localhost lihao04]# export PATH=$PATH:/root/lihao04/go/bin/:/root/go/bin # 检查是否安装成功 [root@localhost /]# go version go version go1.13.4 linux/amd64
# go 使用 grpc 的 SDK [root@localhost /]# go get google.golang.org/grpc # 下载 protoc 编译器 [root@localhost lihao04]# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.10.1/protoc-3.10.1-linux-x86_64.zip [root@localhost lihao04]# cp bin/protoc /usr/bin/ [root@localhost lihao04]# protoc --version libprotoc 3.10.1 # 安装 protoc go 插件 [root@localhost lihao04]# go get -u github.com/golang/protobuf/protoc-gen-go
[root@localhost grpc-example]# cat /root/lihao04/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) {}; }
# 建立项目的文件夹 # 建立 src/test 的目的是咱们在 proto 文件中,填写了 package test; 所以编译出来的 go 文件属于 test project # 建立 src 是 go 语言的标准,go 语言经过 $GOPATH/src/ 下寻找依赖 [root@localhost /]# mkdir -p /root/lihao04/grpc-example/src/test [root@localhost /]# mkdir -p /root/lihao04/grpc-example/server [root@localhost /]# mkdir -p /root/lihao04/grpc-example/client # 将 protobuf 文件写入 /root/lihao04/grpc-example/proto/service.proto # 执行 [root@localhost /]# cd /root/lihao04/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
package main import ( "log" "math" "net" "google.golang.org/grpc" pb "test" ) // 参考 /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 的 MaxSize + Server 拼成的! // MaxSizeServer is the server API for MaxSize service. // 他是一个 interface,只要实现了 Echo,就是这个 interface 的实现。可见,咱们的 func (s *server) Echo(in *pb.Empty, stream pb.MaxSize_EchoServer) error { 实现了这个接口。注意,参数和返回值是否是和 interface 定义的如出一辙? 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 "test" 那么 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: "sssss", Name: "lihao", } list.Ss = append(list.Ss, &feature) } err = out.Send(&list) // 函数要求返回 error 类型 return err } func run() error { sock, err := net.Listen("unix", "/var/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 全部 interface 的实例,即 &server{} 感受就是将 grpc server 和 handler 绑定在了一块儿的意思。 RegisterMaxSizeServer 的命名颇有意思,Register(固定) + MaxSize(service MaxSize {} in proto 文件) + Server(固定) */ pb.RegisterMaxSizeServer(s, myServer) if err != nil { return err } /* 在 s.Serve(sock) 上监听服务 */ if err := s.Serve(sock); err != nil { log.Fatalf("failed to serve: %v", err) } return nil } func main() { run() }
package main import ( "context" "fmt" "log" "time" pb "test" "google.golang.org/grpc" ) func main() { // 经过 grpc.Dial 得到一条链接 conn, err := grpc.Dial("unix:///var/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", err) return } fmt.Printf("%v", data) } }
首先,将代码放置到正确的位置上golang
# 将 server 端代码保存成 server.go,放置到 /root/lihao04/grpc-example/server 下 # 将 client 端代码保存成 client.go,放置到 /root/lihao04/grpc-example/client 下 # 修改 GOPATH [root@localhost server]# export GOPATH=$GOPATH:/root/lihao04/grpc-example # 形如: [root@localhost grpc-example]# pwd /root/lihao04/grpc-example [root@localhost grpc-example]# tree . ├── client │ └── client.go ├── server │ └── server.go └── src └── test ├── service.pb.go └── service.proto 4 directories, 4 files
而后,编译 server & clientjson
# 编译 server [root@localhost server]# cd /root/lihao04/grpc-example/server [root@localhost server]# go build server.go [root@localhost server]# ll total 12344 -rwxr-xr-x 1 root root 12634890 Nov 12 10:01 server -rw-r--r-- 1 root root 3900 Nov 12 10:01 server.go # 编译 client [root@localhost ~]# cd /root/lihao04/grpc-example/client/ [root@localhost client]# go build client.go [root@localhost client]# ll total 12068 -rwxr-xr-x 1 root root 12351720 Nov 12 10:00 client -rw-r--r-- 1 root root 2431 Nov 12 09:55 client.go
最后,运行 server & clientbash
# 打开两个 bash 窗口 # 第一个执行 [root@localhost ~]# cd /root/lihao04/grpc-example/server # 清除以前的 unix socket,很重要!!! [root@localhost server]# rm -rf /var/lib/test.socket [root@localhost server]# ./server # 第二个执行 [root@localhost ~]# cd /root/lihao04/grpc-example/client [root@localhost server]# ./client # 此时,两个窗口会出现交互的内容,实验成功 [root@localhost server]# ./server 2019/11/12 10:02:45 Received from client [root@localhost client]# ./client ss:<id:"sssss" name:"lihao" > ss:<id:"sssss" name:"lihao" > ss:<id:"sssss" name:"lihao" > ss:<id:"sssss" name:"lihao" > ss:<id:"sssss" name:"lihao" > error EOF
最好的参考文档不是网上的文档,而是 gRPC 的 example,它提供了全部最多见的操做,并且保证必定是最正确、最佳的实践方式,因此,须要进一步学习的同窗必定要去看 /root/go/src/google.golang.org/grpc/examples/route_guide
下的例子,固然 /root/go
是咱们示例的 GOPATH。app
本文的初衷是一个被困扰的问题,gRPC 的 send/recv 的一条记录都是有最大长度的socket
# /root/go/src/google.golang.org/grpc/server.go const ( defaultServerMaxReceiveMessageSize = 1024 * 1024 * 4 defaultServerMaxSendMessageSize = math.MaxInt32 )
默承认以发送一条很是大的记录,可是只能接受一条 4MB 的数据,对于什么是一条数据,我以前不是很了解,gRPC server 和 client 交互有 4 种模式:ide
# 官方例子:/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: "lihao", } 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 recv 的 limit:
conn, err := grpc.Dial("unix:///var/lib/test.socket", grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100000000)))