马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
为什么要使用 gRPC?
我们的示例是一个简单的蹊径映射应用程序,它答应客户端获取有关蹊径上的特性的信息,创建蹊径择要,并与服务器和其他客户端互换蹊径信息,例如交通更新。
使用 gRPC,我们可以在 .proto 文件中界说一次服务,并在 gRPC 支持的任何语言中生成客户端和服务器,这些客户端和服务器反过来可以在从大型数据中心的服务器到您自己的平板电脑等各种环境中运行——差异语言和环境之间通信的全部复杂性都由 gRPC 为您处理。我们还得到了使用协议缓冲区的全部优势,包括高效的序列化、简单的 IDL 和简单的接口更新。
设置
您应该已经安装了生成客户端和服务器接口代码所需的工具——如果您还没有,请参阅快速入门中的先决条件部分以获取设置说明。
获取示例代码
示例代码是 grpc-go 仓库的一部分。
- 将仓库下载为 zip 文件 并解压缩,或者克隆仓库
- $ git clone -b v1.63.0 --depth 1 https://github.com/grpc/grpc-go
复制代码 - 更改到示例目次
- $ cd grpc-go/examples/route_guide
复制代码 界说服务
我们的第一步(正如您从gRPC 简介中相识到的那样)是使用协议缓冲区界说 gRPC 服务以及方法哀求和响应范例。有关完整的 .proto 文件,请参见routeguide/route_guide.proto。
要界说服务,您需要在 .proto 文件中指定一个名为 service 的服务
- service RouteGuide {
- ...
- }
复制代码 然后,您在服务界说中界说 rpc 方法,指定它们的哀求和响应范例。gRPC 答应您界说四种服务方法,全部这些方法都在 RouteGuide 服务中使用
- 简单 RPC,其中客户端使用存根将哀求发送到服务器并等待响应返回,就像普通的函数调用一样。
- // Obtains the feature at a given position.
- rpc GetFeature(Point) returns (Feature) {}
复制代码 - 服务器端流式 RPC,其中客户端向服务器发送哀求并得到一个流来读取返回的消息序列。客户端从返回的流中读取,直到没有更多消息为止。正如您在示例中看到的,您可以在响应范例之前放置 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) {}
复制代码 - 客户端流式 RPC,其中客户端写入一系列消息并使用提供的流将它们发送到服务器。客户端完成消息写入后,它将等待服务器读取全部消息并返回其响应。您可以在哀求范例之前放置 stream 关键字来指定客户端流式方法。
- // Accepts a stream of Points on a route being traversed, returning a
- // RouteSummary when traversal is completed.
- rpc RecordRoute(stream Point) returns (RouteSummary) {}
复制代码 - 双向流式 RPC,其中两边使用读写流发送一系列消息。两个流独立运行,因此客户端和服务器可以按任何序次进行读写:例如,服务器可以等待吸收全部客户端消息后再写入其响应,或者它可以瓜代读取一条消息,然后写入一条消息,或者其他一些读写组合。每个流中的消息序次得以保留。您可以在哀求和响应之前都放置 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 文件还包含用于我们的服务方法中使用的全部哀求和响应范例的协议缓冲区消息范例界说——例如,以下是 Point 消息范例
- // 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 和一个特殊的 gRPC Go 插件来完成此操作。这与我们在快速入门中所做的类似。
从 examples/route_guide 目次中,运行以下命令
- $ protoc --go_out=. --go_opt=paths=source_relative \
- --go-grpc_out=. --go-grpc_opt=paths=source_relative \
- routeguide/route_guide.proto
复制代码 运行此命令会在 routeguide 目次中生成以下文件
- route_guide.pb.go,它包含全部协议缓冲区代码来添补、序列化和检索哀求和响应消息范例。
- route_guide_grpc.pb.go,它包含以下内容
- 用于客户端调用的接口范例(或存根),该接口范例包含在 RouteGuide 服务中界说的方法。
- 用于服务器实现的接口范例,也包含在 RouteGuide 服务中界说的方法。
创建服务器
起首让我们看看怎样创建 RouteGuide 服务器。如果您只对创建 gRPC 客户端感爱好,您可以跳过本节,直接前往创建客户端(只管您大概会以为它仍然很风趣!)。
使我们的 RouteGuide 服务发挥作用有两个部分
- 实现从服务界说生成的接口:执行我们服务的实际“工作”。
- 运行一个 gRPC 服务器来监听来自客户端的哀求并将它们调度到正确的服务实现。
您可以在server/server.go中找到我们的示例 RouteGuide 服务器。让我们细致看看它是怎样工作的。
实现 RouteGuide
如您所见,我们的服务器具有一个 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 {
- ...
- }
- ...
复制代码 简单 RPC
routeGuideServer 实现我们全部的服务方法。让我们先看看最简单的范例 GetFeature,它只从客户端获取 Point 并从其数据库中返回相应的特性信息,即 Feature。
- 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{Location: point}, nil
- }
复制代码 该方法为 RPC 传递了一个上下文对象和客户端的 Point 协议缓冲区哀求。它返回一个包含响应信息的 Feature 协议缓冲区对象以及一个 error。在方法中,我们用适当的信息添补 Feature,然后 return 它以及 nil 错误,告诉 gRPC 我们已经完成了处理 RPC,并且可以将 Feature 返回给客户端。
服务器端流式 RPC
现在让我们看看我们的一个流式 RPC。ListFeatures 是一个服务器端流式 RPC,因此我们需要将多个 Feature 发送回我们的客户端。
- 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
- }
复制代码 如您所见,这次我们没有在方法参数中得到简单的哀求和响应对象,而是得到了哀求对象(客户端想要在其中查找 Feature 的 Rectangle)以及一个特殊的 RouteGuide_ListFeaturesServer 对象来写入我们的响应。
在方法中,我们添补了尽大概多的需要返回的 Feature 对象,并使用它的 Send() 方法将其写入 RouteGuide_ListFeaturesServer。最后,就像在我们的简单 RPC 中一样,我们返回 nil 错误,告诉 gRPC 我们已经完成了写入响应。如果在此调用中发生任何错误,我们返回一个非 nil 错误;gRPC 层将将其转换为适当的 RPC 状态,以在网络上传输。
客户端流式 RPC
现在让我们看看一些更复杂的内容:客户端流式方法 RecordRoute,其中我们从客户端得到 Point 的流并返回一个包含其行程信息的 RouteSummary。如您所见,这次方法根本没有哀求参数。相反,它得到了 RouteGuide_RecordRouteServer 流,服务器可以使用该流来读取和写入消息——它可以使用 Recv() 方法吸收客户端消息,并使用 SendAndClose() 方法返回其单个响应。
- 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 的 Recv() 方法重复读取客户端的哀求到一个哀求对象(在本例中为 Point),直到没有更多消息:服务器需要在每次调用后查抄 Recv() 返回的错误。 如果是 nil,则流仍然有用,可以继续读取;如果它是 io.EOF,则消息流已结束,服务器可以返回其 RouteSummary。 如果它有其他值,我们按原样返回错误,以便它被 gRPC 层转换为 RPC 状态。
双向流式 RPC
最后,让我们看看我们的双向流式 RPC RouteChat()。
- 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(),因为它正在写入多个响应。固然两边总是按照写入序次得到对方的邮件,但客户端和服务器都可以按任意序次读写——流是完全独立运行的。
启动服务器
实现完全部方法后,我们还需要启动一个 gRPC 服务器,以便客户端可以使用我们的服务。以下代码段展示了我们如作甚我们的 RouteGuide 服务做到这一点
- lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
- if err != nil {
- log.Fatalf("failed to listen: %v", err)
- }
- var opts []grpc.ServerOption
- ...
- grpcServer := grpc.NewServer(opts...)
- pb.RegisterRouteGuideServer(grpcServer, newServer())
- grpcServer.Serve(lis)
复制代码 要构建并启动服务器,我们需要
- 使用以下方法指定我们要用来监听客户端哀求的端口
lis, err := net.Listen(...).
- 使用 grpc.NewServer(...) 创建一个 gRPC 服务器实例。
- 将我们的服务实现注册到 gRPC 服务器。
- 在服务器上调用 Serve() 并提供我们的端口信息,执行阻塞等待,直到进程被杀死或调用 Stop()。
创建客户端
在本节中,我们将介绍如作甚我们的 RouteGuide 服务创建一个 Go 客户端。您可以在 grpc-go/examples/route_guide/client/client.go 中查察我们完整的示例客户端代码。
创建存根
要调用服务方法,我们起首需要创建一个 gRPC 通道 来与服务器通信。我们通过将服务器地址和端口号传递给 grpc.Dial() 来创建它,如下所示
- var opts []grpc.DialOption
- ...
- conn, err := grpc.Dial(*serverAddr, opts...)
- if err != nil {
- ...
- }
- defer conn.Close()
复制代码 当服务需要身份验证凭据时,您可以在 grpc.Dial 中使用 DialOptions 来设置身份验证凭据(例如,TLS、GCE 凭据或 JWT 凭据)。RouteGuide 服务不需要任何凭据。
设置好 gRPC 通道 后,我们需要一个客户端 存根 来执行 RPC。我们可以使用从示例 .proto 文件生成的 pb 包提供的 NewRouteGuideClient 方法得到它。
- client := pb.NewRouteGuideClient(conn)
复制代码 调用服务方法
现在让我们看看怎样调用服务方法。请注意,在 gRPC-Go 中,RPC 以阻塞/同步模式运行,这意味着 RPC 调用将等待服务器响应,并将返反响应或错误。
简单 RPC
调用简单的 RPC GetFeature 几乎与调用当地方法一样简单。
- feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
- if err != nil {
- ...
- }
复制代码 如您所见,我们在前面得到的存根上调用了该方法。在我们的方法参数中,我们创建并添补了一个哀求协议缓冲区对象(在本例中为 Point)。我们还传递了一个 context.Context 对象,它答应我们在须要时更改 RPC 的举动,例如超时/取消正在进行的 RPC。如果调用没有返回错误,那么我们可以从第一个返回值中读取来自服务器的响应信息。
服务器端流式 RPC
以下是我们调用服务器端流式方法 ListFeatures 的地方,该方法返回地理 Feature 的流。如果您已经阅读了 创建服务器,其中一些内容大概看起来很熟悉 - 流式 RPC 在两端以类似的方式实现。
- rect := &pb.Rectangle{ ... } // initialize a pb.Rectanglestream, 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)
- }
复制代码 与简单 RPC 一样,我们将上下文和哀求传递给该方法。但是,我们没有得到响应对象,而是得到了 RouteGuide_ListFeaturesClient 的实例。客户端可以使用 RouteGuide_ListFeaturesClient 流来读取服务器的响应。
我们使用 RouteGuide_ListFeaturesClient 的 Recv() 方法重复读取服务器的响应到响应协议缓冲区对象(在本例中为 Feature),直到没有更多消息:客户端需要在每次调用后查抄 Recv() 返回的错误 err。如果为 nil,则流仍然有用,可以继续读取;如果它是 io.EOF,则消息流已结束;否则必须存在 RPC 错误,该错误将通过 err 传递。
客户端流式 RPC
客户端流式方法 RecordRoute 与服务器端方法类似,除了我们只将上下文传递给该方法并返回一个 RouteGuide_RecordRouteClient 流,我们可以使用它来写入和读取消息。
- // 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)
复制代码 RouteGuide_RecordRouteClient 具有一个 Send() 方法,我们可以用它向服务器发送哀求。完成使用 Send() 将客户端的哀求写入流后,我们需要在流上调用 CloseAndRecv() 来让 gRPC 知道我们已完成写入并盼望吸收响应。我们从 CloseAndRecv() 返回的 err 中获取 RPC 状态。如果状态为 nil,则 CloseAndRecv() 的第一个返回值将是一个有用的服务器响应。
双向流式 RPC
最后,让我们看看我们的双向流式 RPC RouteChat()。与 RecordRoute 一样,我们只将上下文对象传递给该方法,并返回一个可以用来写入和读取消息的流。但是,这次我们在服务器仍然向其消息流写入消息时,通过方法的流返回值。
- 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
复制代码 这里的读写语法与我们的客户端流式方法非常相似,除了我们在完成调用后使用流的 CloseSend() 方法。固然两边总是按照写入序次得到对方的邮件,但客户端和服务器都可以按任意序次读写——流是完全独立运行的。
试一试!
从 examples/route_guide 目次执行以下命令
- 运行服务器
- $ go run server/server.go
复制代码 - 从另一个终端运行客户端
- $ go run client/client.go
复制代码 您将看到类似以下的输出
- Getting feature for point (409146138, -746188906)
- name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
- Getting feature for point (0, 0)
- location:<>
- Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
- name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
- ...
- name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
- Traversing 56 points.
- Route summary: point_count:56 distance:497013163
- Got message First message at point(0, 1)
- Got message Second message at point(0, 2)
- Got message Third message at point(0, 3)
- Got message First message at point(0, 1)
- Got message Fourth message at point(0, 1)
- Got message Second message at point(0, 2)
- Got message Fifth message at point(0, 2)
- Got message Third message at point(0, 3)
- Got message Sixth message at point(0, 3)
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |