网络拓扑结构是保证整个模块能够正确运作的基础。如果本地进程无法感知远程进程,或者结构信息是错误的,则其他所有的事情都无从谈起。
实现的难点在于两个部分:
1.分布式图结构缺少仲裁节点(中心),需要一套机制来保证不同节点的唯一性,并被远程节点接受
2.网络结构是动态的,连接的建立与断开会随时发生,并且任意的节点、连接都有可能发生变化,而这些变化需要同步给整个网络中的所有节点
设计初期有想过将图结构通过某种规则进行删边从而转换为一颗树,以树根作为仲裁节点
不过这样做有两个主要问题:
1.规则的制定依赖于每个具体项目的需求,很难做到统一
2.对于某些特殊的断线情况,树形结构不能完全支持目前的网络传输逻辑
所以最终还是坚持以图结构来维护动态的网络拓扑结构
对于进程的抽象(节点):
需要满足这几个需求:
1.通过全局唯一的oid来标识一个进程,并且同一个进程无论重启多少次都需要保证oid一致
解释一下为什么要保证重启之后的一致性:
以一个跨服分组下的两个游戏服A和B为例,对A来说B只有逻辑上的意义,并且认为只存在一个B,与它是否重启过,重启了多少次都没有关系。
如果B的oid变了,A只会认为是网络中新加入了另外一个游戏服C
2.带有服务器类型标记,可以通过类型筛选出所有满足需要的节点
3.记录所有网络连接信息(有向边),为消息同步和路径规划提供基础
对于连接的抽象(有向边):
需要满足这几个需求:
1.通过边的有无(或者边的状态)来反映实际的网络连接状态
2.远程节点根据同步消息中的边信息,可以更新它所维护的网络拓扑结构
由于同步消息也是依靠分布式网络传输的,同样无法保证有序性,即旧的结构信息可能比新的结构信息晚到达。
这就要求给边加上版本号:收到同步消息时,根据版本号大小来判断是否需要更新对应的边,若当前消息中的版本号比本地存的要小,则不更新对应的边。
实际上也不能单纯的根据版本号大小来判断:
版本号大的就一定是更新的边信息么?这还得看版本号的生成机制。假如是通过进程启动时从1开始计数这类的方式,由于进程重启后边的版本号会重置,对进程直接相连的边来说它的版本号是很小的,但不能被后续到来的同步消息中远程节点存储的“旧版本”的边所替代。
我处理的方法是:
a.每次收到同步包,先按包内的信息更新一遍
b.然后对于本节点的直连边,检查边的实际连接状态,若与信息不一致,则以实际状态为准再更新一次
c.并且令边的版本号+1,再向外广播一次结构更新
///进程节点
struct GraphNode
{
UINT64 m_oid; ///唯一oid
ESvrNodeType m_nodeType;///节点类型
vector<UINT> m_outEdges;///所有出边
vector<UINT> m_inEdges; ///所有入边
}
///有向边
struct GraphEdge
{
bool m_isInUse; ///是否有效
UINT m_id; ///本地编号
UINT64 m_from; ///边起始节点
UINT64 m_to; ///边指向节点
UINT64 m_version; ///版本号
}
关于图节点唯一oid的生成方式,目前是通过:服务器类型+组ID+配置文件ID的方式组合而成
图结构更新、维护的完整逻辑:
(这里的客户端、服务器是相对socket连接来说的,并不是指客户端程序和服务器程序)
1.客户端进程监听到连接建立事件后,向服务器进程发送握手消息,消息中包括:自身节点oid + 本地维护的图结构信息
2.服务器进程收到握手消息后,根据客户端的图结构信息更新自身的图结构信息(通过边的版本号),更新完成后将本地的图结构信息推送给所有相邻节点(包括这个客户端进程对应的节点)
3.相邻进程收到更新消息后,通过消息内的信息更新本地图结构。若发现本地信息需要修改,则将修改后的完整信息再次推送给所有相邻节点;若本地信息已经是最新,则终止信息的传递
连接断开时的逻辑类似,只不过不再需要客户端先发握手消息,直接由断开连接的双方各自处理即可
拓扑结构管理
网友评论