gRPC簡介

ssdlh發表於2021-07-01

RPC

對RPC不瞭解的人,或許會糾結其與TCP、HTTP等的關係。後者是網路傳輸中的協議,而RPC是一種設計、實現框架,通訊協議只是其中一部分,RPC不僅要解決協議通訊的問題,還有序列化與反序列化,以及訊息通知。

一個完整的RPC架構裡面包含了四個核心的元件,分別是Client ,Server,ClientOptions以及ServerOptions,這個Options就是RPC需要設計實現的東西。

  • 客戶端(Client):服務的呼叫方。

  • 服務端(Server):真正的服務提供方。

  • 客戶端存根(ClientOption):socket管理,網路收發包的序列化。

  • 服務端存根(ServerOption):socket管理,提醒server層rpc方法呼叫,以及網路收發包的序列化。

RPC的邏輯示意圖如下

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-zsHlMmU3-1612838576573)(./image-20210208113707741.png)\]

什麼是gRPC

gRPC是RPC的一種,它使用Protocol Buffer(簡稱Protobuf)作為序列化格式,Protocol Buffer是來自google的序列化框架,比Json更加輕便高效,同時基於 HTTP/2 標準設計,帶來諸如雙向流、流控、頭部壓縮、單 TCP 連線上的多複用請求等特性。這些特性使得其在移動裝置上表現更好,更省電和節省空間佔用。用protoc就能使用proto檔案幫助我們生成上面的option層程式碼。

在gRPC中,客戶端應用程式可以直接在另一臺計算機上的伺服器應用程式上呼叫方法,就好像它是本地物件一樣,從而使您更輕鬆地建立分散式應用程式和服務。

gRPC的呼叫模型如下

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-XGtHTgq9-1612838576577)(./grpc_concept_diagram_00.png)\]

適用場景

  • 分散式場景 :gRPC設計為低延遲和高吞吐量通訊,非常適用於效率至關重要的輕型微服務。
  • 點對點實時通訊: gRPC對雙向流媒體提供出色的支援,可以實時推送訊息而無需輪詢。
  • 多語言混合開發 :支援主流的開發語言,使gRPC成為多語言開發環境的理想選擇。
  • 網路受限環境 : 使用Protobuf(一種輕量級訊息格式)序列化gRPC訊息。gRPC訊息始終小於等效的JSON訊息。

四種呼叫方式

學習gRPC使用之前,先介紹一下RPC中的客戶端與服務端。在RPC中,服務端會開啟服務供客戶端呼叫,每一句RPC呼叫都是一次客戶端發請求到伺服器獲得相應的過程,中間過程被封裝了,看起來像本地的一次呼叫一樣,一次RPC呼叫也就是一次通訊過程。

RPC呼叫通常根據雙端是否流式互動,分為了單項RPC、服務端流式RPC、客戶端流式RPC、雙向流PRC四種方式。為了便於大家理解四種grpc呼叫的應用場景,這裡舉一個例子,假設你是小超,有一個女朋友叫婷婷,婷婷的每種情緒代表一個微服務,你們之間的每一次對話可以理解為一次PRC呼叫,為了便於畫流程圖,RPC請求被封裝成client.SayHello,請求包為HelloRequest,響應為HelloReply。

1. 單項 RPC

當你在等婷婷回去吃飯,婷婷在加班時,你們之間的rpc呼叫可能是這樣的:

小超:回來吃飯嗎

婷婷:還在加班

這就是單項 RPC,即客戶端傳送一個請求給服務端,從服務端獲取一個應答,就像一次普通的函式呼叫。

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-i2VIawkZ-1612838576579)(./image-20210208113746442.png)\]

  • client層呼叫SayHello介面,把HelloRequest包進行序列化
  • client option將序列化的資料傳送到server端
  • server option接收到rpc請求
  • 將rpc請求返回給server端,server端進行處理,將結果給server option
  • server option將HelloReply進行序列化併發給client
  • client option做反序列化處理,並返回給client層
2. 服務端流式 RPC

當你比賽輸了給婷婷發訊息時:

小超:今天比賽輸了

婷婷:沒事,一次比賽而已

婷婷:晚上帶你去吃好吃的

這就是服務端流式 RPC,即客戶端傳送一個請求給服務端,可獲取一個資料流用來讀取一系列訊息。客戶端從返回的資料流裡一直讀取直到沒有更多訊息為止。

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-notxon7k-1612838576581)(./image-20210208114053248.png)\]

  • client層呼叫SayHello介面,把HelloRequest包進行序列化
  • client option將序列化的資料傳送到server端
  • server option接收到rpc請求
  • 將rpc請求返回給server端,server端進行處理,將將資料流給server option
  • server option將HelloReply進行序列化併發給client
  • client option做反序列化處理,並返回給client層
3. 客戶端流式 RPC

當你惹婷婷生氣的時候:

小超:怎麼了,寶貝

小超:別生氣了,帶你吃好吃的

婷婷:滾

客戶端流式 RPC,即客戶端用提供的一個資料流寫入併傳送一系列訊息給服務端。一旦客戶端完成訊息寫入,就等待服務端讀取這些訊息並返回應答,

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-6EP2uFmD-1612838576583)(./image-20210208114110968.png)\]

  • client層呼叫SayHello介面,把HelloRequest包進行序列化
  • client option將序列化的資料流傳送到server端
  • server option接收到rpc請求
  • 將rpc請求返回給server端,server端進行處理,將結果給server option
  • server option將HelloReply進行序列化併發給client
  • client option做反序列化處理,並返回給client層
4. 雙向流 RPC

當你哄好婷婷時:

小超:今天看了一個超好看的視訊

婷婷:什麼視訊

小超:發給你看看

婷婷:這也叫好看?

雙向流 RPC,即兩邊都可以分別通過一個讀寫資料流來傳送一系列訊息。這兩個資料流操作是相互獨立的,所以客戶端和服務端能按其希望的任意順序讀寫,例如:服務端可以在寫應答前等待所有的客戶端訊息,或者它可以先讀一個訊息再寫一個訊息,或者是讀寫相結合的其他方式。每個資料流裡訊息的順序會被保持

\[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-iyp22RQm-1612838576585)(./image-20210208114138744.png)\]

這幅圖就不做流程介紹了,讀者可以自己試著看圖能不能理解過程,相信理解了客戶端流RPC和服務端流RPC倆種方式,這裡一定可以理解的。

gPRC程式碼實現

gRPC 使用 Protobuf 作為序列化格式,Protobuf 比 Json 更加輕便高效。與Json一樣,它與開發語言和平臺無關,具有良好的可擴充套件性。關於Protobuf 使用請參考官網地址 developers.google.com/protocol-buf...

下面我們實現Go語言版的四種gRPC呼叫方式。

1. 單向RPC實現
編寫proto
//proto3標準
syntax = "proto3";

//包名
package helloworld;

//定義rpc介面
service Greets {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

//HelloReply協議內容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest協議
message HelloRequest {
  string name = 1;
  string message = 2;
}
  1. Greet為定義rpc服務的類名,rpc SayHello (HelloRequest) returns (HelloReply) {}表示定義rpc方法SayHello,傳入HelloRequest,返回HelloReply
  2. 進入proto資料夾,執行命令protoc -I . –go_out=plugins=grpc:. ./helloworld.proto在.目錄中生成helloworld.pb.go檔案
編寫server
type Server struct {
}

//實現SayHello介面
func (s *Server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Println(in.Name, in.Message)
    return &pb.HelloReply{Name: "婷婷", Message: "不回來了"}, nil
}

func main() {
    //協議型別以及ip,port
    lis, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println(err)
        return
    }

    //定義一個rpc的server
    server := grpc.NewServer()
    //註冊服務,相當與註冊SayHello介面
    pb.RegisterGreetsServer(server, &Server{})
    //進行對映繫結
    reflection.Register(server)

    //啟動服務
    err = server.Serve(lis)
    if err != nil {
        fmt.Println(err)
        return
    }
}
  1. pb為proto檔案生成的檔案別名

  2. 定義server結構體作為rpc呼叫的結構體,這個結構體必須實現SayHello這個介面

  3. listen -> grpc.NewServer() -> pb.RegisterGreetsServer(server, &Server{}) -> s.Serve(lis)

編寫client
func main() {
    //建立一個grpc連線
    conn, err := grpc.Dial("localhost:8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()

    //建立RPC客戶端
    client := pb.NewGreetsClient(conn)
    //設定超時時間
    _, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 呼叫方法
    reply, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "小超", Message: "回來吃飯嗎"})
    if err != nil {
        log.Fatalf("couldn not greet: %v", err)
    }
    log.Println(reply.Name, reply.Message)
}
  1. grpc.Dial(“localhost:8002”, grpc.WithInsecure())連線到伺服器,grpc.WithInsecure()取消明文檢測
  2. context.WithTimeout(context.Background(), time.Second)設定超時時間
  3. c := pb.NewGreetsClient(conn)建立rpc呼叫的客戶端
  4. c.SayHello(context.Background(), &pb.HelloRequest{Name: name})進行rpc呼叫
抽象介面

其實也就是要實現這個介面,因為倆邊都是單項呼叫,所以呼叫和實現的介面都是這個

type GreetsClient interface {
   SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
2. 服務端流RPC
編寫proto
//proto3標準
syntax = "proto3";

//包名
package helloworld;

//定義rpc介面

service Greet{
  rpc SayHello (HelloRequest) returns(stream HelloReply) {}
}

//HelloReply協議內容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest協議
message HelloRequest {
  string name = 1;
  string message = 2;
}

相比於單項RPC呼叫,因為是客戶端流,所以在HelloRequest多了一個stream

編寫server
type Server struct {
}

//實現rpc介面
func (*Server) SayHello(request *pb.HelloRequest, server pb.Greet_SayHelloServer) error {
    fmt.Println(request)
    var err error
    for i := 0; i < 2; i++ {
        if i == 0 {
            err = server.Send(&pb.HelloReply{Name: "小超", Message: "沒事,一次比賽而已"})
        } else {
            err = server.Send(&pb.HelloReply{Name: "小超", Message: "晚上帶你去吃好吃的"})
        }
        if err != nil {
            fmt.Println(err)
            return err
        }
    }
    return nil
}

func main() {
    //協議型別以及ip,port
    listen, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println(err)
        return
    }

    //定義一個rpc的server
    s := grpc.NewServer()
    //註冊服務,相當與註冊SayHello介面
    pb.RegisterGreetServer(s, &Server{})
    //進行對映繫結
    reflection.Register(s)

    //啟動服務
    err = s.Serve(listen)
    if err != nil {
        fmt.Println(err)
    }
}
編寫client

client傳送的是一個流,與單項RPC方式不同,他通過rpc呼叫獲得的是一個流傳輸物件greetClient,可以用流傳輸物件不停的往對端傳送資料

func main() {
    //建立一個grpc的連線
    grpcConn, err := grpc.Dial("127.0.0.1"+":8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }

    //建立grpc的client
    client := pb.NewGreetClient(grpcConn)
    //設定超時時間
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    //呼叫rpc方法,獲得流介面
    res, err := client.SayHello(ctx, &pb.HelloRequest{Name: "小超", Message: "今天比賽輸了"})
    if err != nil {
        fmt.Println(err)
        return
    }

    //迴圈接收資料
    for {
        recv, err := res.Recv()
        if err != nil {
            fmt.Println(err)
            break
        }
        fmt.Println(recv)
    }
}
抽象介面

伺服器要實現的介面

// GreetsServer is the server API for Greets service.
type GreetsServer interface {
   SayHello(Greets_SayHelloServer) error
}

客戶端呼叫的介面

type GreetsClient interface {
   SayHello(ctx context.Context, opts ...grpc.CallOption) (Greets_SayHelloClient, error)
}
3. 服務端流RPC
編寫proto
//proto3標準
syntax = "proto3";

//包名
package helloworld;

//定義rpc介面
service Greets{
  rpc SayHello (stream HelloRequest) returns (HelloReply) {}
}

//HelloReply協議內容
message HelloReply {
  string name = 1;
  string message = 2;
}

//HelloRequest協議
message HelloRequest {
  string name = 1;
  string message = 2;
}
編寫伺服器
type Server struct{}

//實現rpc方法,直到對端呼叫CloseAndRecv就會讀到EOF
func (*Server) SayHello(in pb.Greets_SayHelloServer) error {
    for {
        recv, err := in.Recv()
        //接收完資料之後傳送響應
        if err == io.EOF {
            err := in.SendAndClose(&pb.HelloReply{Name: "婷婷", Message: "滾"})
            if err != nil {
                return err
            }
            return nil
        } else if err != nil {
            return err
        }
        fmt.Println(recv)
    }
}

func main() {
    //繫結協議,ip以及埠
    lis, err := net.Listen("tcp", ":8002")
    if err != nil {
        fmt.Println("failed to listen: %v", err)
        return
    }

    //建立一個grpc服務物件
    server := grpc.NewServer()
    //註冊rpc服務
    pb.RegisterGreetsServer(server, &Server{})
    //註冊服務端反射
    reflection.Register(server)

    //啟動伺服器
    err = server.Serve(lis)
    if err != nil {
        fmt.Println(err)
        return
    }
}
編寫客戶端
func main() {
    //建立一個grpc的連線
    grpcConn, err := grpc.Dial("127.0.0.1"+":8002", grpc.WithInsecure())
    if err != nil {
        fmt.Println(err)
        return
    }

    //建立grpc的client
    client := pb.NewGreetsClient(grpcConn)

    //設定超時時間
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    //呼叫rpc方法,得到一個客戶端用於迴圈傳送資料
    greetClient, err := client.SayHello(ctx)

    if err != nil {
        fmt.Println("sayHello error")
        fmt.Println(err)
        return
    }

    maxCount := 2
    curCount := 0

    //迴圈傳送
    //調了CloseAndRecv()服務端就會讀到EOF,server端可根據是否讀到EOF來判斷客戶端是否將資料發完
    for {
        if curCount == 0 {
            err = greetClient.Send(&pb.HelloRequest{Name: "小超", Message: "怎麼了,寶貝"})
        } else {
            err = greetClient.Send(&pb.HelloRequest{Name: "小超", Message: "別生氣了,帶你吃好吃的"})
        }

        if err != nil {
            fmt.Println("send error")
            fmt.Println(err)
            return
        }
        curCount += 1
        if curCount >= maxCount {
            res, err := greetClient.CloseAndRecv()
            if err != nil {
                fmt.Println(err)
                break
            }
            fmt.Println(res)
            break
        }
    }
}
抽象介面

客戶端介面

type GreetsClient interface {
   SayHello(ctx context.Context, opts ...grpc.CallOption) (Greets_SayHelloClient, error)
}

伺服器介面

// GreetsServer is the server API for Greets service.
type GreetsServer interface {
   SayHello(Greets_SayHelloServer) error
}

雙向流RPC

雙向流RPC就交給讀者自己練習吧,相信理解了單項RPC,客戶端流RPC,服務端流RPC三種傳輸方式,寫出雙向流RPC應該沒任何問題。

實現總結

其實弄懂了單項RPC、服務端流式RPC、客戶端流式RPC、雙向流PRC四種grpc應用場景,實現起來非常容易

  1. 根據應用場景選擇好哪種gRPC服務
  2. 寫好proto檔案,用protoc生成.pb.go檔案
  3. 服務端實現介面->listen -> grpc.NewServer() -> pb.RegisterGreetsServer(server, &Server{}) -> s.Serve(lis)
  4. 客戶端grpc.Dial->pb.NewGreetsClient->context.WithTimeout->client.SayHello(呼叫介面)->如果是流傳輸則迴圈讀取資料

歡迎關注我的公眾號,檢視超超後續內容更新!
在這裡插入圖片描述

本作品採用《CC 協議》,轉載必須註明作者和本文連結