網上有很多的頁面都提供 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)))