微服務,gRPC 一文全解(五)

前言

RPC

是一個計算機通信協議。該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外地爲這個交互作用編程。
微服務常用更高效的rpc(遠程過程調用協議)通信。

RPC優點:
  • 提高開發效率,開發人員可以把更多精力放在具體的接口實現,而不必考慮數據的底層傳輸問題。
  • 大多數rpc框架都是很多優秀開發人員的智慧結晶,它們的功能實現和執行效率都很優秀。
  • client端和server端必須遵循統一的接口規範,避免產生client和server之間接口或數據局結構不匹配的情況。

gRPC:

  • gRPC是一個高性能、通用的開源RPC框架,其由Google
    2015年主要面向移動應用開發並基於HTTP/2協議標準而設計,基於ProtoBuf(Protocol
    Buffers)序列化協議開發,且支持衆多開發語言。gRPC提供了一種簡單的方法來精確地定義服務和爲iOS、Android和後臺支持服務自動生成可靠性很強的客戶端功能庫。客戶端充分利用高級流和鏈接功能,從而有助於節省帶寬、降低的TCP鏈接次數、節省CPU使用、電池壽命。
  • 最新的Google API支持gRPC
  • 支持 C, C++, Node.js, Python, Ruby, Objective-C,PHP and C#
  • 當前版本Alpha
  • 協議 BSD

ProtoBuf

  • 其由Google 2001年設計,2008年開源。
  • Google內部的服務幾乎都是用的PB協議
  • 久經考驗、充分驗證、良好實現
  • 使用ProtoBuf: Google、Hadoop、ActiveMQ、Netty
  • 當前版本v3.0.0-alpha-3
  • 協議 BSD
    proto文件的具體寫法可以參考官網文檔:https://developers.google.cn/protocol-buffers/docs/gotutorial

gRPC原生例子

proto文件

創建book.proto文件,並輸入

syntax = "proto3"; // 版本聲明,使用Protocol Buffers v3版本

package book; // 包名

// 包含id的一個請求消息
message BookRequest {
    int32 id = 1;
}

// 包含名稱的響應消息
message BookResponse {
    string name = 1;
}

// 定義根據id獲取名稱的服務,對應go中的接口
service BookFun {
    rpc GetBookInfoByID (BookRequest) returns (BookResponse) {}
}

將.proto文件放到book文件夾下,並執行如下命令,生成book.pb.go文件,

protoc --go_out=plugins=grpc:. book.proto

服務端

type BookEntity struct {
}
//實現GetBookInfoByID接口
func (b *BookEntity) GetBookInfoByID(ctx context.Context,requ *book.BookRequest) (*book.BookResponse, error){
	resp := new(book.BookResponse)
	switch requ.Id {
	case 1:
		resp.Name = "西遊記"
	default:
		resp.Name = "金瓶梅"
	}
	return resp,nil
}

func main(){
	ls, _ := net.Listen("tcp", ":666")
	gs := grpc.NewServer()
	//將接口實現註冊到grpc實例中
	book.RegisterBookFunServer(gs,new(BookEntity))
	gs.Serve(ls)
}

客戶端

func main(){
	conn,err := grpc.Dial(":666",grpc.WithInsecure() )
	if err!=nil{
		panic(err)
	}
	defer conn.Close()
	//創建一個BookFun的客戶端實例
	client := book.NewBookFunClient(conn)
	//發送請求
	resp,err := client.GetBookInfoByID(context.Background(),&book.BookRequest{Id:1})

	if err!=nil{
		fmt.Println(err)
	}
	fmt.Println(resp)
}

gRPC+gokit簡單栗子

服務端

1.創建一個book.proto文件

syntax = "proto3"; // 版本聲明,使用Protocol Buffers v3版本

package book; // 包名

// 包含id的一個請求消息
message BookRequest {
    int32 id = 1;
}

// 包含名稱的響應消息
message BookResponse {
    string name = 1;
}

// 定義根據id獲取名稱的服務
service BookFun {
    rpc GetBookInfoByID (BookRequest) returns (BookResponse) {}
}

2.在文件目錄下運行如下命令,生成book.pb.go, 需要提前安裝protobuf並配置環境變量

protoc --go_out=plugins=grpc:. book.proto

3.在service文件夾下創建go文件 並創建接口和實現方法

type BookInter interface {
	GetBookInfoByID(int32)string
}
type Book struct {
}
func(b * Book)GetBookInfoByID(id int32)string{
	switch id {
	case 1:
		return "西遊記"
	case 2:
		return "三國演義"
	case 3:
		return "水滸傳"
	case 4:
		return "紅樓夢"
	case 5:
		return "金瓶梅"
	}
	return "未知id"
}

4.在transport文件夾下實現endpoint的編解碼方法

func DecodeBook(ctx context.Context,inter interface{}) (request interface{}, err error){
	return inter,nil
}

func EncodeBook(ctx context.Context,inter interface{}) (response interface{}, err error){
	return inter,nil
}

5.在epoint文件夾下, 聲明創建endpoint的方法,該方法調用業務邏輯

func GetGrpcEndpointForGetBookIDS(inter service.BookInter)endpoint.Endpoint{
	return func(ctx context.Context, request interface{}) (response interface{}, err error){
		bookresp := new(book.BookResponse)
		bookresp.Name =  inter.GetBookInfoByID(request.(*book.BookRequest).Id)
		return bookresp,nil
	}
}

6.聲明一個結構體,並實現proto中的接口,在實現方法中調用endpoint的ServeGRPC方法。
kitgrpc 是 “github.com/go-kit/kit/transport/grpc”

//調用該方法將GetBookInfoByID方法的實現註冊到gRPC服務中
func InitGRPCRouter(gs * grpc.Server)  {
	book.RegisterBookFunServer(gs,NewGrpcBook())
}
//創建實現接口的函數,此處實現了endpoint的創建,並賦值給結構體中的參數
func NewGrpcBook() book.BookFunServer{
	b :=  &GrpcBook{}
	b.Handler = kitgrpc.NewServer(epoint.GetGrpcEndpointForGetBookIDS(new(service.Book)),
		transport.DecodeBook,transport.EncodeBook)
	return b
}

type GrpcBook struct {
	Handler kitgrpc.Handler
}
//實現proto中的接口
func (g * GrpcBook)GetBookInfoByID(ctx context.Context,request *book.BookRequest)(*book.BookResponse,error){
	_,res ,err := g.Handler.ServeGRPC(ctx,request)
	return res.(*book.BookResponse),err
}

7.創建GRPC實例,並註冊方法

func main(){
	ls, _ := net.Listen("tcp", ":666")
	gs := grpc.NewServer()

	router.InitGRPCRouter(gs)

	gs.Serve(ls)
}

客戶端

1.聲明解壓縮方法,創建一個endpioint實例
import kitgrpc “github.com/go-kit/kit/transport/grpc”

//創建一個grpc客戶端
//第二個參數是接口名稱 .proto文件中的 BookFun 
//第三個參數是方法名稱 .proto文件中的 GetBookInfoByID 
func GetEndpoint(cc *grpc.ClientConn, serviceName string, 	method string,) *kitgrpc.Client{
	//參數grpcReply是grpc返回值,實際上是取類型
	return  kitgrpc.NewClient(cc,serviceName,method,EncodeRequestBook,DecodeResponseBook,book.BookResponse{})
}
//編碼
func EncodeRequestBook (ctx context.Context,inter interface{}) (request interface{}, err error){
	return inter,nil
}
//解碼
func DecodeResponseBook(ctx context.Context,inter interface{}) (response interface{}, err error){
	return inter,nil
}

2.創建grpc實例並調用

func main(){
	conn,err := grpc.Dial(":666",grpc.WithInsecure() )
	if err!=nil{
		panic(err)
	}
	defer conn.Close()
	//創建endpoint,並指明grpc調用的接口名和方法名
	client := GetEndpoint(conn,"book.BookFun", "GetBookInfoByID")
	resp,err:= client.Endpoint()(context.Background(),&book.BookRequest{Id:1})

	if err!=nil{
		fmt.Println(err)
	}
	fmt.Println(resp)
}

測試

將服務運行起來,然後運行客戶端,截圖如下
在這裏插入圖片描述

攔截器

攔截器在作用於每一個 RPC 調用,通常用來做日誌,認證,metric 等等

interfactor 分爲兩種

  • unary interceptor 攔截 unary(一元) RPC 調用
  • stream interceptor 處理 stream RPC

服務端

UnaryServerInterceptor

type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

unary會根據消息進行攔截,todo後,可以調用invoker方法繼續執行,並可以在方法後添加一些滯後操作

err := invoker(ctx, method, req, reply, cc, opts...)

StreamServerInteceptor源碼

type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

stream會根據流進行攔截,
通過調用streamer 可以獲得 ClientStream, 包裝ClientStream 並重載他的 RecvMsg 和 ·SendMsg 方法,即可做一些攔截處理了最後將包裝好的 ClientStream 返回給客戶
例子如下:(偷來的,原文

type wrappedStream struct{
	grpc.ClientStream
}
func (w *wrappedStream) RecvMsg(m interface{})error{
	log.Printf("Receive a message (Type: %T)", m)
	return w.ClientStream.RecvMsg(m)
}
func (w *wrappedStream)SendMsg(m interface{})error{
	log.Printf("Send a message (Type: %T)", m)
	return w.ClientStream.SendMsg(m)
}

func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption)(grpc.ClientStream, error){
	// do soming

	// return ClientStream
	s , err := streamer(ctx, desc, cc, method, opts...)
	if err != nil{
		return nil, err
		}
	return &wrappedStream{s}, nil
}

添加方法

s := grpc.NewServer(
    grpc.UnaryInterceptor(unaryInterceptor),
    grpc.StreamInterceptor(streamInterceptor)
)

鏈式攔截器
調用 grpc.NewServer 時,使用ChainUnaryInterceptor 和 ChainStreamInterceptor 可以設置鏈式的 interceptor

func ChainStreamInterceptor(interceptors ...StreamServerInterceptor) ServerOption
func ChainUnaryInterceptor(interceptors ...UnaryServerInterceptor) ServerOption

第一個 interceptor 是最外層的,最後一個爲最內層
使用UnaryInterceptor, StreamInterceptor·添加的interceptor,總是最先執行

客戶端

對應服務端

UnaryClientInterceptor

type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

StreamClientInterceptor

type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

添加方法

conn, err := grpc.Dial(
    grpc.WithUnaryInterceptor(unaryInterepotr),
    grpc.WithStreamInterceptor(streamInterceptor)
)

鏈式攔截器

func WithChainStreamInterceptor(interceptors ...StreamClientInterceptor) DialOption
func WithChainUnaryInterceptor(interceptors ...UnaryClientInterceptor) DialOption

元數據

grpc能夠通過ctx上下文攜帶元數據
實現方法是通過grpc包下的metadate包實現
“google.golang.org/grpc/metadata”

MD

MD是元數據的基本結構如下,通過操作md實現在ctx上下文中添加提取數據

type MD map[string][]string

創建MD

  • 鍵不允許是“grpc-”開頭,該開頭已內部使用
  • 鍵以“-bin” 爲後綴,這時,值會在傳輸前後以 base64 進行編解碼,進行二進制存儲
  • 提示:
    如果對metadata進行修改,那麼需要用拷貝的副本進行修改 (FromIncomingContext的註釋)
    方法是:func (md MD) Copy() MD
func New(m map[string]string) MD
func Pairs(kv ...string) MD
//將若干個md連接到一起
func Join(mds ...MD) MD
//從上下文中提取一個md
//如果對metadata進行修改,那麼需要用拷貝的副本進行修改
func FromIncomingContext(ctx context.Context) (md MD, ok bool)
//創建一個md副本  
func (md MD) Copy() MD

md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
md = metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2",
    "key2", "val2",
    "key-bin", string([]byte{96, 102}),
)

發送方法

客戶端

//向一個已有的ctx中拼接
func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context
//根據MD創建一個新的ctx上下文
func NewOutgoingContext(ctx context.Context, md MD) context.Context

服務器

  • server 端會把 metadata 分爲 header 和 trailer 發送給 client
  • unary RPC 可以通過 CallOption grpc.SendHeader 和 grpc.SetTriler 來發送 header, trailer metadata
  • stream RPC 則可以直接使用 ServerStream 接口的方法
  • SetTriler 可以被調用多次,並且所有 metadata 會被合併,當 RPC 返回的時候, trailer metadata 會被髮送
//unary rpc
func SendHeader(ctx context.Context, md metadata.MD) error
func SetTrailer(ctx context.Context, md metadata.MD) error
//stream rpc
func SendHeader(md metadata.MD) error
func SetTrailer(md metadata.MD) error

接收方法

客戶端

  • 和服務器對應採用Header和Trailer方法接收,
    它們返回CallOption,放在grpc調用的方法的最後一個參數 opts …grpc.CallOption上
var header,trailer metadata.MD
bookclient := book.NewBookFunClient(conn)
resp,err:=bookclient.GetBookInfoByID(context.Background(),&book.BookRequest{Id:1}, 
grpc.Header(&header),
grpc.Trailer(&trailer))

stream RPC 同理

stream, err := bookclient.GetBookInfoByID(ctx)

// retrieve header
header, err := stream.Header()
// retrieve trailer
trailer := stream.Trailer()

服務端

  • 服務端調用 FromIncomingContext 即可從 context 中接收 client 發送的 metadata
func FromIncomingContext(ctx context.Context) (md MD, ok bool)

func (g * GrpcBook)GetBookInfoByID(ctx context.Context,request *book.BookRequest)(*book.BookResponse,error){
	//在次提示,如需修改md,請先copy
	md,ok := metadata.FromIncomingContext(ctx)
	//TODO
}

未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章