当前位置: 首页 > 工具软件 > gonet2 > 使用案例 >

Gonet2 游戏服务器框架解析之gRPC提高(5)

宋翔
2023-12-01

上一篇blog是关于gRPC框架的基本使用,如果说gRPC只是远程发几个参数,那和一个普通的http请求也没多大差别了。所以今天我就来学习一下gRPC高级一点的使用方法。流!

流可以根据使用方法,分为单向和双向:

  • 单向
    – Client->Server
    – Server->Client
  • 双向
    – Client<=>Server

下面是一个新的例子,三种服务分别使用了几种流服务方式:
1. 参数表示一块地,而返回的是这块地上面的建筑。
2. 客户端不停的发送新的点,最后在服务端构成一个路径,返回。
3. 客户端发送新的点,服务端在做位置操作后返回新的点。

syntax="proto3";

message Point{}
message Path{}
message Ground{}
message Construction{}

service MapService {
    // Server Side Stream
    rpc ListConstructions(Ground) returns (stream Construction) {}
    // Client Side Stream
    rpc RecordPath(stream Point) returns (Path){}
    // Bidirectional streaming
    rpc Offset(stream Point) returns (stream Point){}
}

运行命令生成代码:

protoc --go_out=plugins=grpc:. test.proto

生成的代码太长了,一段一段帖吧,首先帖对象定义部分,这里应该稍微简单:

package test

import proto "github.com/golang/protobuf/proto"

import (
    context "golang.org/x/net/context"
    grpc "google.golang.org/grpc"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal

type Point struct {
}

func (m *Point) Reset()         { *m = Point{} }
func (m *Point) String() string { return proto.CompactTextString(m) }
func (*Point) ProtoMessage()    {}

type Path struct {
}

func (m *Path) Reset()         { *m = Path{} }
func (m *Path) String() string { return proto.CompactTextString(m) }
func (*Path) ProtoMessage()    {}

type Ground struct {
}

func (m *Ground) Reset()         { *m = Ground{} }
func (m *Ground) String() string { return proto.CompactTextString(m) }
func (*Ground) ProtoMessage()    {}

type Construction struct {
}

func (m *Construction) Reset()         { *m = Construction{} }
func (m *Construction) String() string { return proto.CompactTextString(m) }
func (*Construction) ProtoMessage()    {}

// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn

生成的Go语言的几种struct定义,正好对应了在proto文件中的4个message定义。对比上和篇中的例子,除了多几个对象,并没有更复杂。跳过!

服务端

刚一看到这段代码,高高我又有一点蒙。不过想想之前那句话,“同样的代码,仔细看的话,觉得难度是5,不仔细看,一下就蒙了,那难度可能是8。”所以,根源不是难,而是懒得看。

我在打这上面这段话的时候,发现了一段很熟悉的代码,位于生成文件的最后一段,就是

var _MapService_serviceDesc = grpc.ServiceDesc{}

因为这段就是服务的名称与对应handler的映射嘛,所以,这一下子已经读懂了23行代码了。但有一点不同的是,这一次不是Method数组,而是Streams数组,很明显,这一次的服务是流的形式,所以gRPC是把服务的方法名,作为流的名称,而为每一个流,都相应的生成了一个Handler方法:

  • _MapService_ListConstructions_Handler
  • _MapService_RecordPath_Handler
  • _MapService_Offset_Handler

经过上面的分析得出,大致的结构还是没有变化的。看生成代码最上面两段代码,已加注释,就不多附文解释了,相信看过上一篇博客的朋友很容易懂:

// Server API for MapService service
// 我们要实现的服务方法
type MapServiceServer interface {
    ListConstructions(*Ground, MapService_ListConstructionsServer) error
    RecordPath(MapService_RecordPathServer) error
    Offset(MapService_OffsetServer) error
}

// 把我们实现的服务端对象实例,告诉gRPC框架
func RegisterMapServiceServer(s *grpc.Server, srv MapServiceServer) {
    s.RegisterService(&_MapService_serviceDesc, srv)
}
  • 服务方法一,Server Side单向流。
    顾名思义,服务端向客户端有一个单向的通道,入口在服务端,出口在客户端,而服务端自然会有一个向这个入口写数据的操作。
// Server Side Stream
// rpc ListConstructions(Ground) returns (stream Construction) {}
func _MapService_ListConstructions_Handler(srv interface{}, stream grpc.ServerStream) error {
    m := new(Ground)
    if err := stream.RecvMsg(m); err != nil {
        return err
    }
    return srv.(MapServiceServer).ListConstructions(m, &mapServiceListConstructionsServer{stream})
}

type MapService_ListConstructionsServer interface {
    Send(*Construction) error
    grpc.ServerStream
}

type mapServiceListConstructionsServer struct {
    grpc.ServerStream
}

func (x *mapServiceListConstructionsServer) Send(m *Construction) error {
    return x.ServerStream.SendMsg(m)
}

首先_MapService_ListConstructions_Handler方法,在服务端接收到请求时调用,将数据解析出来,然后生成一个mapServiceListConstructionsServer,提供服务。
mapServiceListConstructionsServer实现了MapService_ListConstructionsServer接口,包含了一个grpc封装好的ServerStream,这是通道的入口,和一个用于发送消息的Send方法。

创建Server Side单向流服务:

type mapServiceServer struct {
        ...
}
func (s *mapServiceServer) ListConstructions(ground *Ground, stream MapService_ListConstructionsServer) error {
    var constructions:= constructionsInGround(ground)
    for _, construction := range constructions {
        stream.Send(building)
    }
    return nil
}
  • 服务方法二,Client Side单向流。
    同样在Client & Server之间有一条通道,而这一次服务端要接收客户端发来的Point数据。
func _MapService_RecordPath_Handler(srv interface{}, stream grpc.ServerStream) error {
    return srv.(MapServiceServer).RecordPath(&mapServiceRecordPathServer{stream})
}

type MapService_RecordPathServer interface {
    SendAndClose(*Path) error
    Recv() (*Point, error)
    grpc.ServerStream
}

type mapServiceRecordPathServer struct {
    grpc.ServerStream
}

func (x *mapServiceRecordPathServer) SendAndClose(m *Path) error {
    return x.ServerStream.SendMsg(m)
}

func (x *mapServiceRecordPathServer) Recv() (*Point, error) {
    m := new(Point)
    if err := x.ServerStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

Handler用于接收客户端的请求,生成服务对象来做具体的处理。
服务的定义包函一个流对象,接收方法,用来接收客户端不停传来的Point数据,最后返回路径,由于是一次性返回,因此命名为SendAndClose。

创建Client Side单向流服务:

func (s *mapServiceServer) RecordPath(stream MapService_RecordPathServer) error {
    for {
        point, err := stream.Recv()
        if err == io.EOF {
            return stream.SendAndClose(path)
        } else {
            path.append(point)
        }
    }
    return nil
}
  • 双向流
func _MapService_Offset_Handler(srv interface{}, stream grpc.ServerStream) error {
    return srv.(MapServiceServer).Offset(&mapServiceOffsetServer{stream})
}

type MapService_OffsetServer interface {
    Send(*Point) error
    Recv() (*Point, error)
    grpc.ServerStream
}

type mapServiceOffsetServer struct {
    grpc.ServerStream
}

func (x *mapServiceOffsetServer) Send(m *Point) error {
    return x.ServerStream.SendMsg(m)
}

func (x *mapServiceOffsetServer) Recv() (*Point, error) {
    m := new(Point)
    if err := x.ServerStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

经过上面对单向流和双向流的代码解读之后,这一部分,似乎是一看就懂了!
来创建一个双向流的服务方法:

func (s *mapServiceServer) Offset(stream MapService_OffsetServer) error {
    for {
        point, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        offsetPoint := offset(point)
        if err := stream.Send(offsetPoint); err != nil {
            return err
        }
    }
}

和上一篇一样,这篇blog主要在于对生成代码的解析,以上代码,除了proto生成go文件是真实的,下面的代码我都没跑过!用番茄扔我吧!
用了两篇Blog,基本对gRPC框架有了一个了解,下一篇,就要回到gonet2框架了!看看在框架里,是如何使用gRPC的吧!

 类似资料: