前言
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
}
未完待續。。。