
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今天第一次嘗試. 未來有更多體驗跟心得, 會補充在網誌上的.