gRPC是Google基於HTTP/2跟Protobuf所設計出來的RPC(Remote Prcedure Call)框架.
主要場景是用在microservice之間的通訊, 和mobile app與server之間的通訊.
gRPC在用戶端就是可以直接在不同服務器上調用其方法, 使用方式就跟一般方法雷同.
服務端這裡就是實現gRPC接口然後運作等人來呼叫.
還能雙向通訊, 跟WebSocket一樣的互動情境.
一樣也是能省掉多次的交握, 讓傳輸的payload夠小, 傳完數據的時間就縮短, 頻寬更能被有效的利用.
來寫看看一些網路常見的範例...因為gRPC小弟我今天第一次寫.
go get -u google.golang.org/grpc
user.proto
syntax = "proto3";
// Unary RPC : 客戶端發出一個請求到服務端, 服務端就回應一次
package grpc.simple;
// 定義 UserService 服務
service UserService {
// RPC方法, 透過UserID 取得用戶資料, 並返回UserName、Age
rpc GetUserInfo (UserRequest) returns (UserResponse);
}
// 客戶端請求的格式
message UserRequest {
int32 ID = 1;
}
// 服務端返回的格式
message UserResponse {
string name = 1;
int32 age = 2;
}
用protoc-gen-go該工具的grpc插件來生成gRPC程式
protoc --go_out=plugins=grpc:. hello.proto
這是生成出來的程式碼的一部分,
主要是生成序列化的結構與序列化用的tag.
還有server side用的接口跟client side用的接口.
// 客戶端請求的格式
type UserRequest struct {
ID int32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
// 服務端返回的格式
type UserResponse struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
// 客戶端的方法接口
type UserServiceClient interface {
// RPC方法, 透過UserID 取得用戶資料, 並返回UserName、Age
GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error)
}
// 服務端的方法接口
type UserServiceServer interface {
// RPC方法, 透過UserID 取得用戶資料, 並返回UserName、Age
GetUserInfo(context.Context, *UserRequest) (*UserResponse, error)
}
// 註冊gRPC服務端實例, 跟實現該接口的實例
func RegisterUserServiceServer(s *grpc.Server, srv UserServiceServer) {
s.RegisterService(&_UserService_serviceDesc, srv)
}
接著來寫一下服務端跟客戶端server.go
package main
import (
"context"
"errors"
"log"
"net"
pb "github.com/tedmax100/gin-angular/grpcSimple/proto"
"google.golang.org/grpc"
)
// 準備幾個fake user
var users = map[int32]pb.UserResponse{
1: {
Name: "It Home",
Age: 18,
},
2: {
Name: "Iron Man",
Age: 11,
},
}
type Server struct {
}
// 之前提到Go只要有完成interface的方法, 就等於繼承了該接口
// GetUserInfo(context.Context, *UserRequest) (*UserResponse, error)
func (s *Server) GetUserInfo(ctx context.Context, req *pb.UserRequest) (res *pb.UserResponse, err error) {
// 查找map有沒有該user, 有就回覆, 否則就回錯誤
if user, ok := users[req.GetID()]; ok {
res = &user
return res, nil
}
log.Printf("req : %v\n", req)
return nil, errors.New("user not found")
}
func main() {
// 建構一個gRPC服務端實例
grpcServer := grpc.NewServer()
// 註冊服務
pb.RegisterUserServiceServer(grpcServer, &Server{})
// 註冊端口來提供gRPC服務
listen, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatal(err)
}
grpcServer.Serve(listen)
}
client.go
package main
import (
"context"
"fmt"
"log"
pb "github.com/tedmax100/gin-angular/grpcSimple/proto"
"google.golang.org/grpc"
)
func main() {
// 透過Dial()負責跟gRPC服務端建立起連線
conn, err := grpc.Dial(":8081", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 注入連線, 返回UserServiceClient對象
client := pb.NewUserServiceClient(conn)
// 接著就能像一般調用方法那樣呼叫了
reply, err := client.GetUserInfo(context.Background(), &pb.UserRequest{ID: 1})
if err != nil {
log.Fatal(err)
}
fmt.Printf("reply : %v\n", reply)
reply, err = client.GetUserInfo(context.Background(), &pb.UserRequest{ID: 2})
if err != nil {
log.Fatal(err)
}
fmt.Printf("reply : %v\n", reply)
reply, err = client.GetUserInfo(context.Background(), &pb.UserRequest{ID: 3})
if err != nil {
log.Fatal(err)
}
fmt.Printf("reply : %v\n", reply)
}
這種模式稱為Unary RPC, 就是客戶端同步的發送一次請求, 同步的等待服務端返回.
也因為是機制本身是同步的, 是可以透過goroutine作非同步處理.
gRPC就這樣?
我看官網他還支援streaming!!
客戶端發出請求, 服務端傳回一個stream, 客戶端就從這steam一直讀取一系列的資料, 直到結束. 然後多次回給對方.
在response加上stream就是了.
就反過來囉, 變成服務端發出一個stream請求, 服務端這裡一直讀取stream.
然後一次回應給對方
在request加上stream就是了.
來玩這個, 雙方都傳跟讀stream.
變成可以傳入多個, 然後也是回應多個.
就參數跟回傳都帶stream
就會啟動stream特性.
編寫user.proto
syntax = "proto3";
// 客戶端發出一個請求到服務端, 服務端就回應一次
package grpc.bidirectional.stream;
// 定義 UserService 服務
service UserService {
// RPC方法, 透過UserID 取得用戶資料, 並返回UserName、Age
// 在參數 和 回傳 都加上`stream` 表示回傳和傳入的都是stream
rpc GetUserInfo (stream UserRequest) returns (stream UserResponse);
}
// 客戶端請求的格式
message UserRequest {
int32 ID = 1;
}
// 服務端返回的格式
message UserResponse {
string name = 1;
int32 age = 2;
}
不論是Server還是Client回傳的都是一組接口,
都有Send(), Recv(), 還組合了grpc.ClientStream的一些方法.
也因為雙方都有Send(), Recv()所以彼此都能發訊和收訊.
type UserServiceClient interface {
GetUserInfo(ctx context.Context, opts ...grpc.CallOption) (UserService_GetUserInfoClient, error)
}
type UserService_GetUserInfoClient interface {
Send(*UserRequest) error
Recv() (*UserResponse, error)
grpc.ClientStream
}
type UserServiceServer interface {
GetUserInfo(UserService_GetUserInfoServer) error
}
type UserService_GetUserInfoServer interface {
Send(*UserResponse) error
Recv() (*UserRequest, error)
grpc.ServerStream
}
server.go
package main
import (
"errors"
"fmt"
"io"
"log"
"net"
pb "github.com/tedmax100/gin-angular/grpcBidiretionalStreaming/proto"
"google.golang.org/grpc"
)
var users = map[int32]pb.UserResponse{
1: {
Name: "It Home",
Age: 18,
},
2: {
Name: "Iron Man",
Age: 11,
},
}
type Server struct {
}
// Server 實現了UserServiceServer接口
func (s *Server) GetUserInfo(stream pb.UserService_GetUserInfoServer) error {
// 開一個for 一直收或是發, 直到我們自己想離開為止.
for {
// 透過Recv(), 從stream收取cleint打來的資料
req, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
log.Fatal(err)
return err
}
if user, ok := users[req.GetID()]; ok {
// server主動送回給client
err = stream.Send(&user)
if err != nil {
log.Fatal(err)
return err
}
} else {
log.Printf("req : %v\n", req)
return errors.New(fmt.Sprintf("user not found: %d\n", req.GetID()))
}
}
return nil
}
func main() {
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &Server{})
listen, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatal(err)
}
grpcServer.Serve(listen)
}
cleint.go
package main
import (
"context"
"fmt"
"log"
pb "github.com/tedmax100/gin-angular/grpcBidiretionalStreaming/proto"
"google.golang.org/grpc"
)
func main() {
conn, err := grpc.Dial(":8081", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 返回一個userServiceClient實例, 它實現了UserServiceClient接口
client := pb.NewUserServiceClient(conn)
stream, err := client.GetUserInfo(context.Background())
if err != nil {
log.Fatal(err)
}
var userID int32
for userID = 1; userID < 4; userID++ {
// 發送多筆
stream.Send(&pb.UserRequest{
ID: userID,
})
fmt.Println("send:", userID)
}
fmt.Println("send finish")
time.Sleep(1 * time.Second)
fmt.Println("start receive")
for {
reply, err := stream.Recv()
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("reply : %v\n", reply)
}
}
}
/*
send: 1
send: 2
send: 3
send finish
start receive
reply : name:"It Home" age:18
reply : name:"Iron Man" age:11
2019/10/03 00:17:03 rpc error: code = Unknown desc = user not found: 3
exit status 1
*/
gRPC也是有辦法服務RESTful API的請求的.
只要安裝gRPC-gateway
但gRPC今天第一次嘗試. 未來有更多體驗跟心得, 會補充在網誌上的.