iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
1

gRPC vs HTTP API

來源

gRPC

gRPC是Google基於HTTP/2跟Protobuf所設計出來的RPC(Remote Prcedure Call)框架.
主要場景是用在microservice之間的通訊, 和mobile app與server之間的通訊.
gRPC在用戶端就是可以直接在不同服務器上調用其方法, 使用方式就跟一般方法雷同.
服務端這裡就是實現gRPC接口然後運作等人來呼叫.

還能雙向通訊, 跟WebSocket一樣的互動情境.
一樣也是能省掉多次的交握, 讓傳輸的payload夠小, 傳完數據的時間就縮短, 頻寬更能被有效的利用.

來寫看看一些網路常見的範例...因為gRPC小弟我今天第一次寫.

安裝gRPC

go get -u google.golang.org/grpc

Unary RPC

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!!

Server Streaming RPC

客戶端發出請求, 服務端傳回一個stream, 客戶端就從這steam一直讀取一系列的資料, 直到結束. 然後多次回給對方.
在response加上stream就是了.

Cleint Streaming RPC

就反過來囉, 變成服務端發出一個stream請求, 服務端這裡一直讀取stream.
然後一次回應給對方
在request加上stream就是了.

Bidirectional Streaming RPC

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


上一篇
Go Websocket 長連線
下一篇
Gin With Swagger, 懶人API Doc生成神器
系列文
下班加減學點Golang與Docker30

尚未有邦友留言

立即登入留言