1什么是RPC
RPC:Remote Procedure Call,遠程過程調用。簡單來說就是兩個進程之間的數據交互。正常服務端的接口服務是提供給用戶端(在Web開發中就是瀏覽器)或者自身調用的,也就是本地過程調用。和本地過程調用相對的就是:假如兩個服務端不在一個進程內怎么進行數據交互?使用RPC。尤其是現在微服務的大量實踐,服務與服務之間的調用不可避免,RPC更顯得尤為重要
上圖描述了一個RPC的完整調用流程:
1:client向client stub發起方法調用請求。
2:client stub接收到請求后,將方法名,請求參數等信息進行編碼序列化。
3:client stub通過配置的ip和端口使用socket通過網絡向遠程服務器server發起請求。
4:遠程服務器server接收到請求,解碼反序列化請求信息。
5:server將請求信息交給server stub,server stub找到對應的本地真實方法實現。
6:本地方法處理調用請求并將返回的數據交給server stub。
7:server stub 將數據編碼序列化交給操作系統內核,使用socket將數據返回。
8:client端socket接收到遠程服務器的返回信息。
9:client stub將信息進行解碼反序列化。
10:client收到遠程服務器返回的信息。
上圖中有一個stub(存根)的概念。stub負責接收本地方法調用,并將它們委托給各自的具體實現對象。server端stub又被稱為skeleton(骨架)。可以理解為代理類。而實際上基于Java的RPC框架stub基本上也都是使用動態代理。我們所說的client端和server端在RPC中一般也都是相對的概念。
而所謂的RPC框架也就是封裝了上述流程中2-9的過程,讓開發者調用遠程方法就像調用本地方法一樣。
2. gRPC的原理
gRPC是Google的開源產品,是跨語言的通用型RPC框架,使用Go語言編寫。 Java語言的應用同樣使用了Netty做網絡通信,Go采用了Goroutine做網絡通信。序列化方式采用了Google自己開源的Protobuf。請求的調用和返回使用HTTP2的Stream。
一個RPC框架必須有兩個基礎的組成部分:數據的序列化和進程數據通信的交互方式。
對于序列化gRPC采用了自家公司開源的Protobuf。Google Protocol Buffer(簡稱 Protobuf)是一種輕便高效的結構化數據存儲格式,平臺無關、語言無關、可擴展,可用于通訊協議和數據存儲等領域。似乎和我們熟悉的JSON類似,但其實著重點有些本質的區別。JSON主要是用于數據的傳輸,因為它輕量級,可讀性好,解析簡單。Protobuf主要是用于跨語言的IDL,它除了和JSON、XML一樣能定義結構體之外,還可以使用自描述格式定于出接口的特性,并可以使用針對不同語言的protocol編譯器產生不同語言的stub類。所以天然的適用于跨語言的RPC框架中(非常重要)
而關于進程間的通訊,無疑是Socket。Java方面gRPC同樣使用了成熟的開源框架Netty。使用Netty Channel作為數據通道。傳輸協議使用了HTTP2。
通過以上的分析,我們可以將一個完整的gRPC流程總結為以下幾步:
● 通過.proto文件定義傳輸的接口和消息體。
● 通過protocol編譯器生成server端和client端的stub程序。
● 將請求封裝成HTTP2的Stream。
● 通過Channel作為數據通信通道使用Socket進行數據傳輸。
3 實踐開始
下面我們使用代碼基于以上的步驟來實現一個簡單gRPC。我們用Go實現server端,Java作為client端來實現。
3.1 安裝Protocol Buffers,定義.proto文件
下載Protocol Buffers:https://github.com/protocolbuffers/protobuf/releases
檢查安裝
protoc --version
定義一個simple.proto,這也是后續實現gRPC的基礎:
syntax = "proto3"; //定義了我們使用的Protocol Buffers版本。option go_package = "./;simple";//***在java端請注釋本行***//表明我們定義了一個命名為Simple的服務(接口),內部有一個遠程rpc方法,名字為SayHello。//我們只要在server端實現這個接口,在實現類中書寫我們的業務代碼。在client端調用這個接口。service Simple{rpc SayHello(HelloRequest) returns (HelloReplay){}}//請求的結構體message HelloRequest{string name = 1;}//返回的結構體message HelloReplay{string message = 1;}
3.2 在Go端實現server
根據官方文檔使用如下命令安裝針對Go的gRPC:
go get -u google.golang.org/grpc
建立Go的project:go-server-grpc,然后將前面寫的simple.proto放入項目proto的package中。
cd到proto目錄執行如下命令:
protoc --go_out=plugins=grpc:. simple.proto
這樣就將simple.proto編譯成了Go語言對應的stub程序了。
隨后我們就可以寫我們server端的代碼了:main.go。
以下的代碼都是模板代碼,main函數是socket使用Go的標準實現。作為開發者我們只關注遠程服務提供的具體接口實現即可。
我們可以在生成的simple.pb.go中發現需要實現的接口:
// 客戶端調用的接口
type SimpleClient interface {SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error)
}type simpleClient struct {cc grpc.ClientConnInterface
}func NewSimpleClient(cc grpc.ClientConnInterface) SimpleClient {return &simpleClient{cc}
}func (c *simpleClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error) {out := new(HelloReplay)err := c.cc.Invoke(ctx, "/Simple/SayHello", in, out, opts...)if err != nil {return nil, err}return out, nil
}// 服務端需要實現的接口
type SimpleServer interface {SayHello(context.Context, *HelloRequest) (*HelloReplay, error)
}
之后我們在main.go中實現接口:
package mainimport ("context""grpc-server/proto"//引入對應的包"fmt""net""log""google.golang.org/grpc""google.golang.org/grpc/reflection"
)
//定義接下來要開放的socket的端口
const(port = ":50051"
)type server struct{}func (s *server) SayHello(ctx context.Context,req *simple.HelloRequest) (*simple.HelloReplay, error){fmt.Println(req.Name)return &simple.HelloReplay{Message:"hello =======> " + req.Name},nil
}
之后填寫main方法:
func main() {//創建一個socket監聽lis, err := net.Listen("tcp", port)if err != nil {log.Fatal("fail to listen")}//新建一個grpc服務器s := grpc.NewServer()//使用 simple.RegisterSimpleServer 函數將實現了 SimpleServer 接口的 server 對象注冊到 gRPC 服務器(s)上simple.RegisterSimpleServer(s, &server{})//使用 reflection.Register 函數將 gRPC 服務器(s)注冊到反射服務中。這樣,可以通過 gRPC 提供的工具來動態地查看和調用服務器上的服務。reflection.Register(s)//使用 s.Serve 方法啟動 gRPC 服務器(s),開始接受來自客戶端的連接請求并提供服務。如果啟動過程中出現錯誤,程序會輸出一條錯誤信息并終止運行。if err := s.Serve(lis); err != nil {log.Fatal("fail to server")}
}
目前服務端已經完成了。
3.3 在Go端實現client
package mainimport ("context""fmt"simple "go-server-grpc/proto""log""google.golang.org/grpc"
)const (address = "localhost:50051"
)func main() {// 創建與服務器的連接conn, err := grpc.Dial(address, grpc.WithInsecure())if err != nil {log.Fatalf("無法連接到服務器:%v", err)}defer conn.Close()// 創建一個新的 gRPC 客戶端client := simple.NewSimpleClient(conn)// 構建請求request := &simple.HelloRequest{Name: "John",}// 調用 gRPC 方法response, err := client.SayHello(context.Background(), request)if err != nil {log.Fatalf("調用 gRPC 方法失敗:%v", err)}// 打印響應fmt.Println(response.Message)
}
需要先啟動服務端,再啟動客戶端就可以看到效果。
輸出:hello=======>John
4.下次預告
實現Java作為客戶端調用go服務端的服務