golang gRPC 入門
網上有不少的頁面都提供 golang gRPC 的簡單例子,但是有些問題:
- 給出的例子可以看,但是自己運行總是失敗
- 不告訴大家怎么配置環境,執行什么命令,直接就講 gRPC 語法,不疼不癢
- 關鍵步驟不告訴大家為什么這么做,就是貼代碼
新手最需要的是手把手教,否則挫折感會讓他失去嘗試的信心。網上的文章要么是新手抄來抄去,要么老手不屑於寫。導致文檔質量奇差無比。
安裝 golang
go 語言比較好的地方在於他是一個編譯型的語言,一旦編譯(linux)好后,就可以獨立運行,沒有任何附加依賴。這比 python 的部署方便太多,以前從事 openstack 開發,最怕解決依賴、部署環境的問題。基於 golang 的 k8s 的部署比 openstack 簡單無數倍。很少出現依賴的問題。
golang 語言編譯器等本身也僅僅是一個可執行文件,因此安裝十分方便:
# 創建下載目錄
[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
安裝 gRPC
# 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
定義 protobuf 文件
[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) {};
}
編譯 proto 文件
# 創建項目的文件夾
# 創建 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
編寫 server 端代碼
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()
}
編寫 client 端代碼
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)
}
}
執行 server & client
首先,將代碼放置到正確的位置上
# 將 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 & client
# 編譯 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 & client
# 打開兩個 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。
本文的初衷是一個被困擾的問題,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: "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 發送請求是 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)))