承接自己《中小型棋牌类网络游戏服务端架构》博文,用Golang实现基础架构逻辑后,准备再次谈谈我的想法。
已实现的逻辑与前文描述有几点不同:
1. Gateway更名为Proxy,DBProxy更名为DB
2. Proxy同时持有与(Login, Game)不同类型服务器的多条连接
3. DB不参与负载均衡,考虑是棋牌数据库负载不高,即使需要扩展多个也可以通过不同服务器配置指向不同的DB来扩展
4. 消息头格式以源码实现的为主
5. 心跳机制在不考虑客户端的前提下,服务端会主动发送心跳包,但并非总是特定间隔时间发送
如果阅读起来感觉晦涩难懂,那就不妨直接看源码
Network
包含Server,Client,RPC三个组件。
Server
网络服务端组件,持有监听套接字,管理所有客户端连接(map存储,value可以存储自定义值),一个客户端连接到来起一个Goroutines接收消息,调用Stop将等待所有Goroutines退出才返回,只通知业务逻辑“收到消息”“连接关闭”两种事件。
Client
网络客户端组件,持有与服务端的连接,实现补偿策略重连机制,只通知业务逻辑“客户端连接成功”“客户端收到消息”两种事件。
RPC
远程过程调用,只实现同步调用,同一时间一个调用占用一条连接,实现连接池,暂时没有限制最大连接数。
注:服务端重启,一次调用将会关闭所有已经建立的连接,创建新的连接完成调用,若服务端重启历时过长,此时调用将因为连接服务端失败而返回错误。
Error
有必要详说通用的错误机制,错误类型是跨服务器跨网络的,可以直接返回error或MyError类型通知通讯调用方错误,错误会一级一级返回,Golang内置error类型终会创建相应的MyError自定义错误类型进行返回,RPC接收方收到消息将会检查若为错误消息,则创建MyError返回标识调用失败。
Server
Proxy(代理), Manager(管理), Login(登陆), Game(游戏), DB(数据库代理)。
Proxy
持有Server组件:接受客户端连接,管理客户端连接,事件到来通知业务逻辑。
持有Client组件:连接Manager服务器,事件到来通知业务逻辑。
连接Manager成功,将会发送注册服务消息(编号,监听地址,服务类型)。
客户端消息到来,检查conn上是否绑定的有session,没有则创建绑定,最后消息总是交由session处理。
客户端连接关闭,检查conn上是否绑定的有session,若有则关闭session。
收到Manager通知服务消息,合理更新本地可用服务信息。
Session
会话目前共持有client(客户端), login(登陆), game(游戏)三个连接,存在最基本的目的就是消息转发。
session共有三种连接状态,安全状态(说明客户端一直给服务端发消息,平均间隔时间不超过十秒),警告状态(客户端至少最近十秒内没有给服务端发消息),死亡状态(客户端至少最近二十秒内没有给服务端发消息,且连服务端十秒前发送的心跳包都不回复),session收到客户端消息,总是将session状态设置为安全状态。
目前也只有这层实现心跳检测保活(服务端和服务端之间是没有心跳的),一个session起一个Goroutines间歇循环保活(原本也想所有session统一保活,最大的好处就是只用起一个Goroutines,但是这样必然要维护session集合,因为牵涉增加删除session,必然又要加锁,不想这样所以采用了当前的方案),每十秒检查一下session状态,总是将状态值递减至死亡状态,当session状态是警告状态,尝试向客户端发送心跳包,当session状态是死亡状态,则关闭session。
总是在收到客户端快速注册消息时,关闭原有与登陆建立的连接,重新创建连接,起一个Goroutines接收登陆服务端消息,不在客户端连接建立的时候连接登陆服务端,这样做会多一层保护,防止攻击者恶意建立连接,穿透至登陆服务端,这里会重新序列化客户端消息,填充客户端地址,毕竟只有这里知道客户端的真实地址,发向登陆服务端的其它类型消息不允许先于快速注册消息,否则直接关闭session,接收登陆服务端快速注册消息返回时会解析消息,取出用户编号记录到session里,便于快速登陆游戏服务端时填充用户编号。
总是在收到客户端快速登陆消息时,关闭原有与游戏建立的连接,重新创建连接,起一个Goroutines接收游戏服务端的消息,客户端快速登陆消息只需要提供“游戏类型(斗地主)”“游戏等级(新手房)”,这里通过查找对应服务建立正确连接,这里会重新序列化消息,新增“用户编号”“时间戳”“签名”,时间戳和签名只是想保证游戏服务端收到的快速登陆消息一定是我的代理服务端发出的(详细请参看源码,思路可参考微信公众平台接入验证),发向游戏服务端的其它类型消息不允许先于快速登陆消息,否则直接关闭session。
用户断开与代理的连接,将会触发关闭session,通知保活Goroutines退出,关闭与登陆游戏之间的连接,两个接收消息Goroutines将退出,与客户端的连接接下来会被释放,不再有谁绑定该session,所以该session将会被GC(垃圾回收)。
不管接收登陆还是游戏消息失败,接收Goroutines都会退出,将会关闭与客户端连接,进而关闭session。
收到用户登出消息,session直接关闭与游戏服务端的连接。
Manager
持有Server组件:接受客户端(Proxy, Login, Game)连接,管理客户端连接,事件到来通知业务逻辑。
维护两个服务集合,一是所有服务(所有已开启的服务),二是已选服务(负载均衡策略后选择的服务)。
收到注册服务消息,记录对应网络连接,所有服务表记录该服务,若已选服务表中不存在类似服务,则同时已选服务表记录该服务,若注册服务类型是代理,则通知当前已选服务,用来初始化代理本地可用服务。
连接关闭,将通过连接查找服务,所有服务表删除该服务,已选服务表若存在该服务则删除,尝试从所有服务表中获取相似服务,若获取到则添加至已选服务表。
收到更新计数消息,更新对应服务的计数值,只有当计数值高于容量阈值时才会触发后面的逻辑,已选服务表若存在该服务则删除,尝试从所有服务表中获取相似服务,若获取到则添加至已选服务表,但若准备删除和增加的是同一个服务,则不触发前面的删除添加服务逻辑。
收到开启服务消息,设置对应服务开启标识,若已选服务表中不存在类似服务,则已选服务表记录该服务。
收到关闭服务消息,设置对应服务关闭标识,已选服务表若存在该服务则删除,尝试从所有服务表中获取相似服务,若获取到则添加至已选服务表。
前面有三处用到获取相似服务,其实这里实现了负载均衡,每次调用总会返回计数值最小的Proxy, Login服务,计数值最大的却又不超过容量阈值的Game服务(若都超过阈值返回最小的),让用户尽可能在相同游戏服务中玩,便于快速组桌开始游戏。
前面提到的不管是增加已选服务,还是删除已选服务,都会通知所有已注册的代理。
Login
持有Server组件:接受Proxy模拟客户端建立的连接,管理与Proxy建立的连接,事件到来通知业务逻辑。
持有Client组件:连接Manager服务器,事件到来通知业务逻辑。
持有RPC组件:连接DB数据库代理。
连接Manager成功,将会发送注册服务消息(编号,监听地址,服务类型)。
收到快速注册消息,RPC同步调用请求数据库代理,不管是查询已有用户还是创建新用户,数据库代理返回后,直接回复客户端,其实是回复给代理。
Game
持有Server组件:接受Proxy模拟客户端建立的连接,管理与Proxy建立的连接,事件到来通知业务逻辑。
持有Client组件:连接Manager服务器,事件到来通知业务逻辑。
持有RPC组件:连接DB数据库代理。
连接Manager成功,将会发送注册服务消息(编号,监听地址,游戏类型,游戏等级,服务类型)。
收到快速登陆消息,通过校验签名确保是由Proxy发来的,RPC同步调用请求数据库代理查询用户信息,数据库代理返回后,直接回复客户端,其实是回复给代理。
DB
持有Server组件:接受客户端(Login, Game)连接,管理客户端连接,事件到来通知业务逻辑。
这里扩展了OnMessage的返回值,支持返回nil表示成功,返回内置error类型表示错误,返回自定义MyError类型表示成功或错误,返回其它数据结构表示回复给客户端的内容。
连接MySQL数据库,连接Redis缓存
注:已经实现定时器模块,定时时间精确到秒,主要逻辑只不过是对标准库Timer、Ticker封装管理而已,通过全局唯一编号添加定时器,支持循环定时器,支持获取到期剩余时间。