五邑隐侠,本名关健昌,12年游戏生涯。 本教程以Go语言为例。
RPC指远程方法调用,游戏里引入RPC目的是降低跨进程交互的复杂度。 游戏业务设计为多go routine,一个玩家一个go routine。游戏里RPC客户端阻塞式调用远程(服务进程)方法,这样处理的好处是跨进程交互的业务也可以按照单线程顺序执行的思路实现。 RPC请求包由以下几部分组成:标记(字符串,用于区分是哪类调用)、序列号(一次调用的唯一标记)、方法编号(用于映射调用的方法)、参数。 RPC响应包由以下几部分组成:标记、序列号、方法编号、返回值。
type RpcRequest struct { Mark []byte FuncNo uint16 SerialNo uint16 Params []byte } type RpcResponse struct { Mark []byte FuncNo uint16 SerialNo uint16 ReturnVal []byte }
RPC建立在P2P网络之上,在 P2pListener interface 的 OnP2pCall(p2p *P2pNet, pack *P2pPack) 实现方法,根据包头前几个字节与 Mark 对比,如果一致则调用对应的RPC客户端/服务端进行处理。 对于RPC服务,可以参考http服务,建立方法编号到处理方法的映射。
type RpcHandler func(params []byte) ([]byte, error) type RpcServ struct { p2p *P2pNet mark []byte mapFuncNo2Handler map[uint16]RpcHandler } func (s *RpcServ) HandleFunc(funcNo uint16, handler func(params []byte) ([]byte, error)) { if handler == nil { return }
s.mapFuncNo2Handler[funcNo] = handler }
当收到请求后,解析出funcNo和参数Params,调用对应的处理器进行处理。
handler, ok := s.mapFuncNo2Handler[rpc.FuncNo] if !ok { return fmt.Errorf("no handler for funcNo %d", rpc.FuncNo) }
returnVal, err := handler(rpc.Params)
RPC服务对请求的处理可以是非阻塞的。 一般只对RPC客户端做成阻塞的,这样在客户端调用远程方法时,客户端会一直等待服务端返回才继续往下执行,整个逻辑流程跟单线程执行一样。 通过序列号SerialNo、方法号 FuncNo 双重确认响应包对应于当前请求。 在go语言里,可以通过定义一个长度为1的 chan byte,请求发出后,读取通道,如果还没有收到响应就会阻塞,响应返回时往通道里写个1,唤醒调用流程继续执行。
func Wait(e chan byte) { <-e } func Signal(e chan byte) { e <- 1 }
调用方法的参数和返回值都是二进制,业务可以根据自己的需要用json或 protobuff 进行序列化/反序列化。
func (c *RpcClient) invoke(params []byte) ([]byte, error)
RPC调用介绍到这里。接下来介绍下游戏业务进程的实现。