【Pitaya-官方Demo解读笔记】2. cluster demo

8 篇文章 0 订阅


项目目录: examples/demo/cluster

吐槽一下,pitaya 这个框架虽然非常好,但是文档确实太少了,chat demo 好歹还有个 README,到了 cluster 这个复杂的例子反而什么文档也没有了……

吐槽完毕,让我们先看一下,这个例子跑起来是什么样子的

运行

代码里有关于 Jaeger,但这不是我们的主线,而且配置起来比较麻烦,这里先略过,只研究我们的主线逻辑,之后有时间再回过头来看吧

先 cd 到项目目录下

运行前端、后端服务器

  1. 运行前端服务(不加 flag 默认就是前端服务,待会看代码的时候再说)

    go run main.go
    
  2. 运行后端服务,type 为后端服务名,也就是待会客户端路由到的服务名,这里设定为 room (main.go:51 说明了后端服务名为 room)

    go run main.go -type=room -frontend=false
    

运行 pitaya-cli 客户端

pitaya-cli 安装

这个命令行客户端是 pitaya 内置的,我们这里已经把源码拉下来了,可以直接 cd 到项目的 pitaya-cli 目录下,使用 go install 直接安装
在这里插入图片描述

然后在命令行界面就可以使用 pitaya-cli 直接使用了

PS:如果报错找不到这个命令,一般 go install 是安装到了 GOPATH 目录下。看一下 go env 的 GOPATH 配置在哪里,设置一下系统的环境变量即可

客户端CLI测试

两个客户端大概的输入输出如下

客户端1:

> pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:3250
Using json client
connected!
>>> request room.room.entry
>>> sv->{"result":"ok"}
>>> request room.room.join
>>> sv->{"Members":["e34fbbbf-9d86-412d-924e-b2994a32efa3"]}
sv->{"result":"success"}
sv->{"content":"New user: 2"}
sv->{"content":"New user: 4"}
>>> notify room.room.message {"name":"test", "content":"hello world"}
>>> sv->{"name":"test","content":"helloworld"}

客户端2:

> pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:3250
Using json client
connected!
>>> request room.room.entry
>>> sv->{"result":"ok"}
>>> request room.room.join
>>> sv->{"result":"success"}
sv->{"Members":["e34fbbbf-9d86-412d-924e-b2994a32efa3","791d0a3a-0929-46bc-82c4-7c501fa8db8a"]}
sv->{"content":"New user: 4"}
sv->{"name":"test","content":"helloworld"}
  1. connect 127.0.0.1:3250 连接到前端服务
  2. room.room.entry 绑定会话,为该会话赋值了一个 UID
  3. room.room.join 加入到聊天房间,并收到房间当前所有玩家的 UID 列表
  4. room.room.message 推送消息到服务器,服务器将消息广播到所有人

代码分析

前3行就是命令行的解析,这里就顺便学习一下 flag 的用法

flag

port := flag.Int("port", 3250, "the port to listen")
svType := flag.String("type", "connector", "the server type")
isFrontend := flag.Bool("frontend", true, "if server is frontend")
flag.Parse()

flag包提供了对命令行的解析方法,以上三个方法的参数列表都是一样的,分别表示:命令行参数名、默认值和用法描述。我们可以使用 -help 来输出这些内容,比如我们使用 go -help 可以打印出 go 支持的参数及其用法:

> go -help
Go is a tool for managing Go source code.

Usage:

        go <command> [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         add dependencies to current module and install them
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        work        workspace maintenance
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

Use "go help <command>" for more information about a command

这里我们试一下打印该项目支持的命令行参数

  1. 把项目编译成 .exe

    go build -o cluster.exe .
    
  2. 输出命令行参数

    ./cluster.exe -help
    

在这里插入图片描述
frontend 默认值为 true,所有刚才我们运行前端服务时,不需要附加任何参数

frontend

if *isFrontend {
    tcp := acceptor.NewTCPAcceptor(fmt.Sprintf(":%d", *port))
    builder.AddAcceptor(tcp)
}

isFrontend 为 true 时表明了这是一个前端服务,前端服务需要接收客户端连接,所以这里创建了一个 acceptor,而且只有前端服务可以创建 acceptor,在前面一个 demo 里我们也提到过,Builder AddAcceptor 时会判断服务是否为前端服务。

关于前后端服务,我们这里再理解一下含义

pitaya 官方文档有相关描述:

Frontend and backend servers

In cluster mode servers can either be a frontend or backend server.

在集群模式下,服务器可以是前端服务器,也可以是后端服务器。

Frontend servers must specify listeners for receiving incoming client connections. They are capable of forwarding received messages to the appropriate servers according to the routing logic.

前端服务器必须指定接收传入客户端连接的侦听器。它们能够根据路由逻辑将收到的消息转发到适当的服务器。

Backend servers don’t listen for connections, they only receive RPCs, either forwarded client messages (sys rpc) or RPCs from other servers (user rpc).

后端服务器不侦听连接,它们接受 RPC,要么是转发的客户端消息(sys rpc) ,要么是来自其他服务器(user rpc)的 RPC。

前端服务、后端服务,应该是 Pitaya 框架给出的概念,可以将前端服务理解为 Proxy 或者 Gate,它是面向客户端连接的,具有转发消息到后端服务的功能和职责。

假设有一个简单的游戏服务器架构如下:
在这里插入图片描述

登录服务器 LoginServer、转发服务器 ProxyServer 和 战斗服务器 BattleServer 作为前端服务器,与客户端直接建立连接,而大厅服务器(或者说逻辑服务器)LobbyServer 和 聊天服务器 ChatServer 是后端服务器,由 Proxy 进行消息转发。

Register 和 RegisterRemote

在前后端服务器中,都有两种类型的注册:RegisterRegisterRemote

Register 我们在前文已经说过,最终是将消息处理函数整理到了 HandlerServiceHandlerPool 结构体里,当 acceptor 收到消息时,根据消息路由key,获取到对应的 handler 进行处理。再看 RegisterRemote,其实最终落地也是在 HandlerPool,不同的只是 Remote 是在 RemoteService 的结构体中。

我们看一下这两个结构体的定义:
在这里插入图片描述
可以发现差异很大,RemoteService 既包含 RPCServer 也包含 RPCClient,也就是说,这个 Service 是具有 RPC 收发能力的,既可以接受 RPC,也可以发起 RPC。

再看 HandlerService 里还包含有一个 RemoteService,查找引用发现有这个调用链:

// handler.go:319
// processMessage
if r.SvType == h.server.Type {
		h.chLocalProcess <- message
} else {
    if h.remoteService != nil {
        h.chRemoteProcess <- message
    } else {
        logger.Log.Warnf("request made to another server type but no remoteService running")
    }
}

当服务器收到一个目标 Type 不是自己的消息时,将消息放入 chRemoteProcess 中,继续跟踪:

// handler.go:119
// Dispatch
func (h *HandlerService) Dispatch(thread int) {
	...
	for {
		select {
		case rm := <-h.chRemoteProcess:
			metrics.ReportMessageProcessDelayFromCtx(rm.ctx, h.metricsReporters, "remote")
			h.remoteService.remoteProcess(rm.ctx, nil, rm.agent, rm.route, rm.msg)
        ...
		}
	}
}

这条消息被分发到 RemoteService 去处理,即发起一个RPC:

// remote.go:105
// remoteProcess
func (r *RemoteService) remoteProcess(
	ctx context.Context,
	server *cluster.Server,
	a agent.Agent,
	route *route.Route,
	msg *message.Message,
) {
	res, err := r.remoteCall(ctx, server, protos.RPCType_Sys, route, a.GetSession(), msg)
	...
}

到这里就捋顺了,串起来理解一下。

RegisterRemote 使得该组件的消息处理函数可以被远程调用,这个 RPC 调用是前端服务器发起的。

AddRoute

pitaya 是可动态扩展的,同一个 type 的服务器可能有多个,当前端服务器收到一个目标为其他 serverType 的消息时,就需要转发到对应的服务器,AddRoute 用来设置路由策略:如何从多个服务器中选择一个进行转发。

在本例中,就是选择了第一个,正式环境可以在这里做负载均衡等策略。

SetDictionary

设置压缩路由

err = app.SetDictionary(map[string]uint16{
    "connector.getsessiondata": 1,
    "connector.setsessiondata": 2,
    "room.room.getsessiondata": 3,
    "onMessage":                4,
    "onMembers":                5,
})

对于示例里设置的压缩路由,如果对端发送的路由消息是 room.room.getsessiondata,那么在序列化消息时,会查询底层的压缩路由表 router 是否有对应的信息,如果有的话采用压缩路由,即 room.room.getsessiondata 对应的 ID:3。并设置压缩位 flag。

// message_encoder.go:64
// Encode
func (me *MessagesEncoder) Encode(message *Message) ([]byte, error) {
	...
	routesCodesMutex.RLock()
	code, compressed := routes[message.Route]
	routesCodesMutex.RUnlock()
	if compressed {
		flag |= msgRouteCompressMask
	}
	...
	if routable(message.Type) {
		if compressed {
			buf = append(buf, byte((code>>8)&0xFF))
			buf = append(buf, byte(code&0xFF))
		} else {
			buf = append(buf, byte(len(message.Route)))
			buf = append(buf, []byte(message.Route)...)
		}
	}
	...
}

当服务器收到后反序列化消息时,会先取出 flag,判断是否有压缩位,有的话就查询自己本地的压缩路由表,从 ID 还原成字符串的路由信息。

// message_encoder.go:133
// Decode
func Decode(data []byte) (*Message, error) {
	...
	if routable(m.Type) {
		if flag&msgRouteCompressMask == 1 {
			if offset > size || (offset+2) > size {
				return nil, ErrInvalidMessage
			}

			m.compressed = true
			code := binary.BigEndian.Uint16(data[offset:(offset + 2)])
			routesCodesMutex.RLock()
			route, ok := codes[code]
			routesCodesMutex.RUnlock()
			if !ok {
				return nil, ErrRouteInfoNotFound
			}
			m.Route = route
			offset += 2
		} else {
			m.compressed = false
			...
		}
	}
    ...
}

但是压缩路由表是设置在服务器上的,对端是如何获取这个路由表的?

这些在 Pitaya 底层已经做了保证,当客户端连接上来的时候,底层会自动 Handshake 握手,HandshakeResponse 即握手回复包里,服务器就附带上自己的压缩路由表了。

// client.go:138
// sendHandshakeRequest
func (c *Client) sendHandshakeRequest() error {
	enc, err := json.Marshal(c.clientHandshakeData)
	if err != nil {
		return err
	}

	p, err := c.packetEncoder.Encode(packet.Handshake, enc)
	if err != nil {
		return err
	}

	_, err = c.conn.Write(p)
	return err
}

func (c *Client) handleHandshakeResponse() error {
	...
	if compression.IsCompressed(handshakePacket.Data) {
		handshakePacket.Data, err = compression.InflateData(handshakePacket.Data)
		if err != nil {
			return err
		}
	}
	...
	if handshake.Sys.Dict != nil {
		message.SetDictionary(handshake.Sys.Dict)
	}
	...
}

GetSessionData / SetSessionData

前端服务器的 Connector 和后端服务器的 Room,都实现了对 GetSessionData 与 SetSessionData 的消息处理。跑个命令行测试一下:

pitaya-cli
Pitaya REPL Client
>>> connect 127.0.0.1:3250
Using json client
connected!
>>> request room.room.entry
>>> sv->{"result":"ok"}
>>>
>>> request connector.setsessiondata {"data":{"key1":"value1"}}
>>> sv->{"code":200,"msg":"success"}
>>>
>>> request connector.getsessiondata
>>> sv->{"Data":{"key1":"value1"}}
>>>
>>> request room.room.setsessiondata {"data":{"key1":"value1","key2":"value2"}}
>>> sv->success
>>>
>>> request connector.getsessiondata
>>> sv->{"Data":{"key1":"value1","key2":"value2"}}
>>>
>>> request room.room.getsessiondata
>>> sv->{"Data":{"key1":"value1","key2":"value2"}}

前后端服务器都可以 get/set 这个 sessiondata,其中的区别在哪里?上代码!
在这里插入图片描述

可以看到前后端的实现都差不多,但是 Room.SetSessionData 里多了一个 API 调用 PushToFront

PushToFront

后端服务器如果想修改会话数据,必须要推送到前端服务器,这个在官方文档也有说明:

Backend sessions have access to the sessions through the handler’s methods, but they have some limitations and special characteristics. Changes to session variables must be pushed to the frontend server by calling s.PushToFront (this is not needed for s.Bind operations), setting callbacks to session lifecycle operations is also not allowed. One can also not retrieve a session by user ID from a backend server.

后端会话可以通过处理方法来访问,但是它们有一些限制和特殊的特征。会话变量的更改必须通过调用 s.PushToFront 推送到前端服务器(s.Bind操作不需要这样做) ,也不允许为会话生命周期设置回调。也不能通过用户 ID 从后端服务器检索会话。

把推送的代码注释掉,再重新测试,会发现在 Room 侧的 sessiondata 修改并没有生效:

>>> request room.room.setsessiondata {"data":{"key1":"value1","key2":"value2"}}
>>> request room.room.getsessiondata
>>> sv->{"Data":{"ipversion":"ipv4"}}
>>> request connector.getsessiondata
>>> sv->{"Data":{"ipversion":"ipv4"}

SendRPC

后端服务器 Room 可以使用 RPCTo 远程调用前端服务器的开放接口,我们之前说过,RegisterRemote 就是注册了被远程调用的API,本例中,Room 后端服务器就有这样的接口,而前端服务器的 ConnectorRemote 也被注册为了远程服务。

func (r *Room) SendRPC(ctx context.Context, msg *protos.SendRPCMsg) (*protos.RPCRes, error) {
	logger := pitaya.GetDefaultLoggerFromCtx(ctx)
	ret := &protos.RPCRes{}
	err := r.app.RPCTo(ctx, msg.ServerId, msg.Route, ret, &protos.RPCMsg{Msg: msg.Msg})
	if err != nil {
		logger.Errorf("Failed to execute RPCTo %s - %s", msg.ServerId, msg.Route)
		logger.Error(err)
		return nil, pitaya.Error(err, "RPC-000")
	}
	return ret, nil
}

我们迂回一下,通过命令行调用 Room.SendRPC,通过远程调用来访问 ConnectorRemote.RemoteFunc,至于 RPCTo 需要传入的参数 ServerId,就在服务器每次开启时会在后台打印的信息里:
在这里插入图片描述
需要远程调用 Connector 前端服务器,这里的 ServerId 就是 type 为 connector 的数据

接下来看看命令行的请求与回复,还有服务器里的日志打印:

>>> request room.room.entry
>>> request room.room.sendrpc {"server_id":"b19b69d0-4def-4766-aa9f-b8395a2fbcd0", "route":"connector.connectorremote.remotefunc", "msg":"this is a remote call"}
>>> sv->{"Msg":"thisisaremotecall"}

# 前端服务器打印出了日志
received a remote call with this message: thisisaremotecall

需要注意一点,ServerId 每次开启服务器都会不一样,实际生产环境不是这样去写死调用的,这里只是做测试

Doc / Descriptor

connector 中还有 Doc 和 Descriptor 处理函数,在 pitaya-cli README 的解释是:

For connecting to a server that uses protobuf as serializer the server must implement two routes:

  • Docs: responsible for returning all handlers and the protos used on input and output;
  • Descriptors: The list of protos descriptions, this will be used by the CLI to encode/decode the messages.

也就是说,如果服务器是使用 protobuf 来序列化(默认使用 json),那么命令行要连接服务器,就需要服务器实现这两个接口

但是本例中,我一直没有跑通这个代码,而且要使用 protobuf,是需要在 Build App 之前重新设置序列化器,即:

builder.Serializer = protobuf.NewSerializer()

这个在本例中也没有调用,感觉这个例子是有点问题的,暂时先不研究这个了,之后再自己写 demo 测一下 protobuf 作为序列化器的功能

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值