主要基于官網介紹的文檔總結而來。
需要先了解 protocol buffers
為什么使用gRPC
通過gPRC,我們可以僅僅定義一次service 到.proto文件中,然后使用gRPC支持的任何開發語言開發客戶端或服務器。
樣例代碼和環境的建立
首先要確保golang開發環境的正確配置,go1.5+。
$ go get -u -v google.golang.org/grpc
本人在測試中遇到報錯,主要原因在于樣例需要
"golang.org/x/net"
"golang.org/x/text"
的支持,本人的解決方法如下
到
$GOPATH/src/golang.org/x/
目錄下,如果golang.org/x/ 不存在則手動創建一個。
然后
git clone https://github.com/golang/net.git
git clone https://github.com/golang/text.git
樣例測試
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide
$ go run server/server.go
$ go run client/client.go
下面對樣例的代碼進行分析
服務定義
gRPC使用 protocol buffers定義服務。
要定義服務,需要在.proto文件中做service定義如下:
service RouteGuide {
...
}
然后可以在servie的定義rpc方法,指定對應的request和response類型。gPRC允許開發者定義4中service方法,這4中方法在樣例RouteGuide 中都有用到。
- 最簡單的RPC方法,客戶端通過調用該方法發送request到服務端,等待服務器的response,類似正常的函數調用。
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
- 服務端單邊stream的RPC( server-side streaming RPC):客戶端調用該方法到服務端,服務器返回一個stream,客戶端從這個stream中讀取數據直到沒有數據可讀。從樣例代碼中可以看到該方法的主要特點是在response類型前加stream。
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
- 客戶端單邊stream的RPC(A client-side streaming RPC):客戶端通過使用stream將一系列的數據發送到服務端。客戶端數據發送完畢后就等待服務端把數據全部讀完后發送相應過來。從樣例代碼中可以看到該方法主要特點是在request類型前面加stream.:
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
- 雙邊stream RPC(bidirectional streaming RPC)。客戶端和服務端都通過讀寫流(read-write stream)向對方發送一系列的消息。這兩個streams是完全獨立的,所以客戶端和服務端可以隨意的進行讀寫操作:例如,服務端可以等待客戶端的是數據都接收完畢后再往response里寫數據,或者可以先讀取一條消息再寫入一條信息或者是其他的一些讀寫組合方式。從樣例代碼中可以看到該方法的主要特點就是在request和response前面都加stream。
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
樣例中的.proto文件包含了服務端方法中使用的request和response類型所使用的類型的協議池消息類型定義( protocol buffer message type definitions )。
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
生成客戶端和服務端代碼
根據.proto文件生成客戶端和服務端所需的gRPC接口代碼
protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide
創建服務端
服務端代碼主要做兩方面的工作:
- 實現上一步驟.proto生成的服務端接口。
- 運行一個gRPC服務來監聽客戶端的請求并且把請求分發到正確的服務端實現里。
實現RouteGuide
As you can see, our server has a routeGuideServer struct type that implements the generated RouteGuideServer interface:
可以看出我們的服務端有一個routeGuideServer 的結構體類型實現了RouteGuideServer 的接口。
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
...
Simple RPC
GetFeature,從客戶端獲取一個Point然后從數據庫中返回對應的特征信息。
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
// No feature was found, return an unnamed feature
return &pb.Feature{"", point}, nil
}
這個方法輸入參數是一個RPC的context對象以及客戶端發過來的點協議池(Point protocol buffer)請求。這個方法返回一個特征協議池(Feature protocol buffer)對象,對象中包含響應信息和錯誤。在這個方法中,我們為Feature轉入了正確的信息然后和nil error一起返回,告訴gRPC服務器已經完成對RPC的處理,Feature可以返回給客戶端了。
Server-side streaming RPC
ListFeatures是一個服務端stream的RPC,所以我們需要返回多個Features到客戶端。
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
可以看出,該方法獲取一個request對象以及一個特殊的RouteGuide_ListFeaturesServer 來寫相應。這個方法中我們用Send方法把所有需要返回的Feature特征寫入到RouteGuide_ListFeaturesServer 中。最后返回一個nil error告訴gRPC服務端已經寫好相應。如果期間有什么錯誤發生,我們返回一個非nil的error,gRPC會轉換為正確的RPC狀態發送到線路中。
Client-side streaming RPC
.
客戶端流方法RecordRoute中,我們從客戶端獲取一系列的Point然后返回一個RouteSummary 對象包含旅行信息。從代碼中可以看到該方法里面沒有任何的請求參數,而是一個RouteGuide_RecordRouteServer 流對象。服務端可以用Rev()方法從RouteGuide_RecordRouteServer 對象中讀取消息并使用Write()方法往里面寫消息。
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
在這個方法中,我們使用RouteGuide_RecordRouteServer’s 的Recv方法不停的從客戶端的請求中讀取數據到requesst對象直到沒有數據可讀。服務器需要檢測每次Recv返回的error,如果是nil,表示這個stream正常可以繼續讀,如果是io.EOF表示流已經停止了此時服務端可以返回RouteSummary。
Bidirectional streaming RPC
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
... // look for notes to be sent to client
for _, note := range s.routeNotes[key] {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
這個方法中使用RouteGuide_RouteChatServer 流對象,可以用來讀消息和寫消息。然而這次我們通過流返回數據的同時客戶端仍然在往他們的消息流中寫消息。
該方法中往消息流中寫消息使用的是Send() 方法而不是 SendAndClose()
官網中介紹原因如下:具體意思暫時沒有搞明白。
TODO:The syntax for reading and writing here is very similar to our client-streaming method, except the server uses the stream’s Send() method rather than SendAndClose() because it’s writing multiple responses. Although each side will always get the other’s messages in the order they were written, both the client and server can read and write in any order — the streams operate completely independently.
Starting the server
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
... // determine whether to use TLS
grpcServer.Serve(lis)
如代碼所示,我們創建和啟動一個服務器需要下面4個步驟:
- 指定端口號,用來監聽客戶端的請求,使用
err := net.Listen("tcp", fmt.Sprintf(":%d", *port)).
- 創建一個gRPC服務器實例
grpc.NewServer().
注冊服務器實現到上一步驟創建的gRPC服務器實例上。
調用Serve啟動服務,阻塞等待直到該進程被殺死或服務器的stop被調用。
使用TLS
func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
if *tls {
creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
opts = []grpc.ServerOption{grpc.Creds(creds)}
}
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)
}
Creating the client
創建客戶端
flag.Parse()
var opts []grpc.DialOption
if *tls {
var sn string
if *serverHostOverride != "" {
sn = *serverHostOverride
}
var creds credentials.TransportCredentials
if *caFile != "" {
var err error
creds, err = credentials.NewClientTLSFromFile(*caFile, sn)
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
} else {
creds = credentials.NewClientTLSFromCert(nil, sn)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
grpclog.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
client := pb.NewRouteGuideClient(conn)
為了能夠調用服務端的方法,我們首先創建一個gRPC通道來和服務端溝通。通過傳入服務器地址和端口號給grpc.Dial()來創建。如代碼,我們還可以使用DialOptions來設置grpc中的認證方法。
一旦gRPC通道建立起來后,我們需要一個客戶端來執行RPC,通過.proto創建的pb包中提供的NewRouteGuideClient方法來創建。
Calling service methods
對應服務端的四種方法,客戶端也要采用不同的調用方法。
Simple RPC
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
...
}
從代碼中看出,客戶端調用方法GetFeature(在),傳遞協議池(protocol buffer object)對象pb.Point作為參數,同時傳遞一個context.Context 對象,可以讓我們方便的改變RPC的行為,例如超時或取消RPC。
Server-side streaming RPC
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
...
}
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
log.Println(feature)
}
cient.ListFeaturens參見.proto生成的route_guide.pb.go
func (c *routeGuideClient) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) {
在這個方法中,同樣的傳遞一個context對象和一個請求,但是返回一個RouteGuide_ListFeaturesClient實例,客戶端可以從這個實例中讀取得到服務端的響應。
我們使用RouteGuide_ListFeaturesClient的Recv方法來從服務端的響應中讀取到協議池對象Feature中直到沒有數據可讀。同樣的客戶端在讀取時需要檢測返回的err,如果為nil,說明此時stream是正常的繼續可讀,如果為io.EOF表示數據已經到結尾了。
Client-side streaming RPC
// Create a random number of random points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())
if err != nil {
log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
if err := stream.Send(point); err != nil {
log.Fatalf("%v.Send(%v) = %v", stream, point, err)
}
}
reply, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)
同樣參見route_guide.pb.go中RecordRoute的定義
func (c *routeGuideClient) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) {
stream, err := grpc.NewClientStream(ctx, &_RouteGuide_serviceDesc.Streams[1], c.cc, "/routeguide.RouteGuide/RecordRoute", opts...)
RecordRoute方法僅僅需要傳遞一個context參數,然后返回一個RouteGuide_RecordRouteClient流對象用于客戶端寫消息和讀消息。
RouteGuide_RecordRouteClient的Send()方法用于向客戶端發送請求,一旦完成客戶端的所有請求,客戶端需要調用CloseAndRecv方法來讓gRPC知道客戶端已經完成請求并且期望獲得一個響應。
如果CloseAndRecv()返回的err不為nil,那么返回的第一個值就是一個有效的服務端響應。
Bidirectional streaming RPC
stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("Failed to receive a note : %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
和RecordRoute類型,方法RouteChat僅需要傳遞一個context對象,返回一個RouteGuide_RouteChatClient用于客戶端讀消息和寫消息。
func (c *routeGuideClient) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) {
stream, err := grpc.NewClientStream(ctx, &_RouteGuide_serviceDesc.Streams[2], c.cc, "/routeguide.RouteGuide/RouteChat", opts...)
不過和RecordRoute不同的是,客戶端在往客戶端的stream里寫消息的同時,服務端也在往服務端的stream中寫消息。另外,該方法中客戶端中讀和寫是分開獨立運行的,沒有先后順序,還有就是客戶端寫消息完畢后使用CloseSend而不是CloseAndRecv
后記
之前一直在CSDN上寫文章,后面會逐步轉換到簡書上,還請大家多多支持。