- Arch
Arch
Arch详细说明和使用。
Provider & Module
在Arch中,Provider是真正的逻辑实现服务,它躲在Linkd后面。用一套协议和Linkd交互。 下面的Server是一个Provider的实现,用来实现主逻辑。Server后端有一个共享的数据库 (如,mysql)。其他自定义的Provider实现Arch没有直接提供支持。自己根据需要使用 Provider协议和Linkd交互即可。
- 每个Provider是一个project,独立进程。
- Module是逻辑划分单位,里面可以包含Bean,协议,数据(表)。Linkd转发的时候,同一 个Module的协议会派发到同一个Provider实例。每个Module也可以看作是一个微服务。
- 整个系统可以包含多个Provider。Provider包含多个Module实现。Module有一个唯一编号, 在整个系统内唯一,即每个Module只能在一个Provider内实现。
- 一个Provider可能存在多个实际运行进程。此时同一个Module会运行在多个进程中,但没 有违反上一点。Module的唯一性是代码在Provider-Project中,实际运行可以是多进程。
- Provider 的 Service.type 一般都是 “server”,但是它主动连接 linkd,并注册支持的Module。
- 绑定亲缘性。当linkd给client选择provider-instance时,会把该Provider支持的Module 全部都一起绑定到client.sesion中。
Linkd
连接负载分配服务器。
- LinkdApp
管理Arch模块内部依赖类的引用。也完成一些初始化工作。
- LinkdProvider
Linkd面向内部Provider(GameServer是一个Provider)实现模块。包含负载选择,Provider 协议的处理。Provider协议是定义内部服务怎么跟Linkd通讯的协议。
- LinkdProviderService
Linkd面向内部Provider的网络模块,管理跟内部Provider之间的网络连接和网络事件派发。
- LinkdService
Linkd面向客户端的网络模块,管理客户端连接。dispatchUnknownProtocol是这个服务总的 转发协议的处理入口,应用可以重载这个方法实现自己特殊的转发规则。比如一个群组聊天 的派发,根据hash(GroupId),固定的转发到相应的服务器上。实现这个需要Decode出协 议,并根据参数按特殊规则选择服务器。为了避免Decode完整协议,有个办法就是所有的 群组协议开头都有固定的公共参数,如GroupId,Linkd这里可以偷偷的只偷出这一部分来 进行处理。
Linkd Initialize
定义服务(solution.linkd.xml)
<project name="linkd" gendir="." scriptdir="src" platform="java">
<service name="LinkdService" handle="server" base="Zeze.Arch.LinkdService">
<module ref="Linkd"/>
</service>
<service name="ProviderService" handle="client" base="Zeze.Arch.LinkdProviderService">
</service>
</project>
Linkd服务需要使用Arch里面的两个服务,在base里面指定。如果应用需要对网络事件进 行拦截处理,在生成的服务类LinkdService,ProviderService中重载相应的函数。Linkd服务 作为一个项目,可以自由的增加自己的模块。参考 zeze/ZezeJava/Zege/solution.linkd.xml
配置(linkd.xml)
<?xml version="1.0" encoding="utf-8"?>
<zeze GlobalCacheManagerPort="5002" CheckpointPeriod="0" ServerId="-1">
Linkd通常不需要数据库,但是验证可能要,先占个坑。
<DatabaseConf Name="" DatabaseType="Memory" DatabaseUrl=""/>
<ServiceConf Name="LinkdService"
InputBufferMaxProtocolSize="2097152"
SocketLogLevel="Trace">
<Acceptor Port="5100"/>
</ServiceConf>
<ServiceConf Name="ProviderService"
InputBufferMaxProtocolSize="2097152"
SocketLogLevel="Trace">
如果linkd运行在双网(内外网)机器上,这里可以配置Ip为内部网络的地址,不允许外部连接。
<Acceptor Ip="" Port="5101"/>
</ServiceConf>
<ServiceConf Name="Zeze.Services.ServiceManager.Agent">
<Connector HostNameOrAddress="127.0.0.1" Port="5001"/>
</ServiceConf>
</zeze>
Linkd需要的Arch模块初始化
下面的初始化代码部分是生成的,应用需要加入Arch模块的初始化,当然也可以加入任意 自己需要的初始化。Arch模块作为全局变量定义在App中。
public void Start(String conf) throws Throwable {
var config = Config.Load(conf);
// 生成的初始化
CreateZeze(config);
CreateService();
// Arch模块初始化
LinkdProvider = new Zeze.Arch.LinkdProvider();
LinkdApp = new Zeze.Arch.LinkdApp("Zege.Linkd", Zeze, LinkdProvider,
ProviderService, LinkdService, LoadConfig());
// 生成的初始化
CreateModules();
Zeze.Start(); // 启动数据库
StartModules(); // 启动模块,装载配置什么的。
// 设置Session生成器
AsyncSocket.setSessionIdGenFunc(PersistentAtomicLong.getOrAdd(
LinkdApp.GetName())::next);
StartService(); // 启动网络
// 注册服务。
LinkdApp.RegisterService(null);
}
Server
Provider的主实现。一个App框架中只有一个主Server。在主Server中实现主要的应用逻 辑。Arch框架不包括自定义的Provider实现。
ProviderApp
管理Arch模块内部依赖类的引用。也完成一些初始化工作。
ProviderImplement
处理Linkd转发的来自客户端的请求。客户端请求通过Provider协议包装。
ProviderService
跟Linkd通讯的网络模块。
ProviderDirect
处理Server之间互联协议。主要包括ModuleRedirect相关处理。
ProviderDirectService
Server之间互联网络模块。
ProviderModuleBinds
Server转发相关配置处理。
Online
基于账号的在线管理模块。提供给任意在线用户发送消息的接口。
- 本机数据
账号登录在某一个Server实例上时,可以保存一个仅当前Server可见的本机数据。本机数据 每个登录保存一份。
* SetLocalBean 设置本机数据
* GetLocalBean 获取本机数据
* WalkLocal 遍历本机所有的数据,必须在事务外执行。
- 在线事件
- LoginEvents登录发生时触发。
- ReloginEvents Relogin发生时触发。这个是可选的功能,如果应用不需要这个逻辑, 不要在客户端发送Relogin协议即可。这个协议的功能和最小化数据同步相关。当客户端 异常断线(没有主动发送Logout协议)时,服务器不会马上删除用户的在线状态,会保持 一段时间。如果用户在这段时间内Relogin,就可以把这段时间内发生的数据变化同步给客 户端,而且仅同步差异数据。这样就可以快速Relogin。这个功能需要同步差异数据,还需 要配合ReliableNotify,ChangeListener一起使用。
- LogoutEvents登出时触发。登出事件可能丢失,在服务器关闭时补发一个Logout的处理 非常困难(因为程序还可能异常关闭)。现在的逻辑是下一次Login发生时,发现上一个 Login没有Logout,就补发一个额外的Logout事件。这个特性虽然完成了Login,Logout 的匹配,但只能用于跟时间无关的逻辑中。如果逻辑需要Login,Logout匹配来完成时间 统计,由于补发的Logout可能很迟,已经没法正确即使。这种需求需要注意,请自行处理。
- LocalRemoveEvents LocalData被删除时触发。
- ReliableNotify(在线可靠消息) 在线可靠消息被保存在在线数据中,它发送给客户端并需要确认。没有确认的消息在Relogin 时会被同步给客户端。
- AddReliableNotifyMark 从这个点开始,启用参数listenerName指定的消息,即调用 了这个之后的,SendReliableNotifyXXX才真正生效。
- RemoveReliableNotifyMark 停止参数listenerName指定的可靠消息。
- SendReliableNotify 发送可靠消息。
- SendReliableNotifyWhileCommit 事务成功时发送可靠消息。
- SendReliableNotifyWhileRollback 事务失败时发送可靠消息。
- SendToLogin(给某个登录发送消息) 给某个登录发送消息,只有这个登录能收到。
- SendEmbed 直接在当前事务中执行发送消息逻辑,回滚不会发送。
- Send 开启一个新的事务发送消息,可以在事务外使用。
- SendWhileCommit 事务提交时发送消息。
- SendWhileRollback 事务回滚时发送消息。
- SentToAccount(给某个账号发送消息) 给某个账号发送消息,这个账号所有的登录都能收到。账号发送方法提供了回调函数。应用 可以在回调中挨个处理每个登录,进行特殊的处理。没有提供回调函数时(参数是null),默 认实现给账号所有的登录发送消息。
SendAccountsEmbed 直接在当前事务内执行发送消息,回滚不发送。 SendAccount 开启一个新的事务给单个账号发送消息,可以在事务外使用。 SendAccounts 开启一个新的事务给多个账号发送消息,可以在事务外使用。 SendAccountWhileCommit 事务提交时发送。 SendAccountsWhileCommit 事务提交时发送。 SendAccountWhileRollback 事务回滚时发送消息。 SendAccountsWhileRollback 事务回滚时发送消息。
- Transmit
在分布式架构中,用户被分到了多台Server实例中。当用户A需要查询用户B的数据,发 送给用户X(实际上通常X就是A)。虽然每个Server实例都能直接看到所有用户的数据, 但是如果这个数据查询量大,并且改动非常频繁,那么此时从异机查询会导致Cache失效, 命中率下降。所以提供了这个功能。
Transmit(string account, string clientId, string actionName, string target,
Serializable parameter = null)
• Sender: Account,ClientId 请求发起者,数据结果发送给他。
• actionName:具体的查询操作,需要注册。
• Target:查询目标用户,
• Parameter:查询参数,可选。
Transmit会查找目标用户在哪台Server实例中,然后把请求转到他所在的server中执行。 执行的结果直接发回给Sender,不会返回Sender所在服务器。这样,修改和查询都发生在 Target用户所在的服务器,Cache命中率极高。而且整个操作代价不大,仅仅多一个Rpc 转发。
Transmit
TransmitWhileCommit
TransmitWhileRollback
- Broadcast 整个世界广播,慎用!
LoadConfig
负载配置。
RedirectHash
这是一个注解,被标记的函数会被重定向到另一个Server进程执行。重定向使用第一个函 数的参数hash进行选择。
@RedirectHash(ConcurrentLevelSource="getConcurrentLevel(arg1.getRankType())")
public RedirectFuture<Long> updateRank(int hash, BConcurrentKey keyHint, long roleId,
long value, Binary valueEx){}
ConcurrentLevelSource用来获得总的hash分组的数量。 Hash 选择依据,在上面这个例子,hash = roleId.hashCode(); 这个功能的使用后面全球同服会继续说明。
RedirectAll
Server之间直连协议的一种。给所有的分组数据发送广播请求,并处理结果(可能)。有点 像MapReduce。这是RedirectHash的分组广播的注解,用来发送或者接收所有分组的数据。
@RedirectAll
protected RedirectAllFuture<RRankList> getRankAll(int hashCount, BConcurrentKey keyHint){}
RedirectToServer
这是一个注解,把标记的函数重定向到另一个Server进程执行。相当于一种便利的Rpc机 制。
@RedirectToServer
public void redirectNotify(int serverId, String account) throws Throwable {
// some operate
}
ServerA里调用redirectNotify,实际会执行参数serverId所指定的服务器实例(有可能就是 ServerA)。
Server Initialize
定义服务(solution.xml)
<project name="server" gendir="." scriptdir="src" platform="java" GenTables="">
<!--
这里引用的模块不该定义协议,定义了也不会被生成,一般为纯逻辑或者数据库模块。
<module ref="CommonModule"/>
-->
<!-- service 生成到 solution 名字空间下 -->
<service name="Server" handle="server" base="Zeze.Arch.ProviderService">
<module ref="User"/>
<module ref="Friend"/>
<module ref="Message"/>
</service>
<ModuleStartOrder>
</ModuleStartOrder>
<service name="ServerDirect" handle="server,client"
base="Zeze.Arch.ProviderDirectService">
</service>
</project>
Server需要使用Arch中的两个网络模块。在base中指定。需要拦截网路事件,可以在生成 的类中重载相应函数。
配置(server.xml)
<?xml version="1.0" encoding="utf-8"?>
<!--
GlobalCacheManagerHostNameOrAddress: server 启用 cache-sync,必须指定。所有的
server 必须配置一样。
ServerId 每个 server 必须配置不一样,范围 [0, AutoKeyLocalStep)
AutoKeyLocalStep: 自增长步长。server 实例数量上限。
-->
<zeze
GlobalCacheManagerHostNameOrAddress="127.0.0.1"
GlobalCacheManagerPort="5002"
CheckpointPeriod="60000"
ServerId="0"
>
先配一个内存数据库,调试。
<DatabaseConf Name="" DatabaseType="Memory" DatabaseUrl=""/>
<ServiceConf Name="Server"
InputBufferMaxProtocolSize="2097152"
SocketLogLevel="Trace">
</ServiceConf>
<ServiceConf Name="Zeze.Services.ServiceManager.Agent">
<Connector HostNameOrAddress="127.0.0.1" Port="5001"/>
</ServiceConf>
<ServiceConf Name="ServerDirect"
InputBufferMaxProtocolSize="2097152"
SocketLogLevel="Trace">
<Acceptor Ip="" Port="5102"/>
</ServiceConf>
</zeze>
Server需要的Arch模块初始化
下面的初始化代码部分是生成的,应用需要加入Arch模块的初始化,当然也可以加入任意 自己需要的初始化。Arch模块作为全局变量定义在App中。
public void Start(String conf) throws Throwable {
var config = Config.Load(conf);
CreateZeze(config);
CreateService();
// 初始化Arch模块。
Provider = new Zeze.Arch.ProviderWithOnline();
ProviderDirect = new Zeze.Arch.ProviderDirect();
ProviderApp = new Zeze.Arch.ProviderApp(Zeze, Provider, Server,
"Zege.Server.Module#", ProviderDirect, ServerDirect,
"Zege.Linkd", LoadConfig());
// 初始化可选的Arch模块
Provider.Online = GenModule.Instance.ReplaceModuleInstance(
this, new Online(this));
// 初始化可选的Zeze模块
LinkedMaps = new LinkedMap.Module(Zeze);
DepartmentTrees = new DepartmentTree.Module(Zeze, LinkedMaps);
// 生成的代码
CreateModules();
Zeze.Start(); // 启动数据库
StartModules(); // 启动模块,装载配置什么的。
// Online初始化
Provider.Online.Start();
// 其他初始化
PersistentAtomicLong socketSessionIdGen = PersistentAtomicLong.getOrAdd("Zege.Server."
+ Zeze.getConfig().getServerId());
AsyncSocket.setSessionIdGenFunc(socketSessionIdGen::next);
StartService(); // 启动网络
ProviderApp.StartLast(ProviderModuleBinds.Load(), Modules);
}
发现Linkd,Server流程
- Linkd.Startup
{ ServiceName = "Game.Linkd"; // 应用自定义。 ServiceIdentity = Ip + ":" + Port; // Linkd监听地址和端口,接受来自Provider的连接。 RegisterService(ServiceName, ServiceIdentity, Ip, Port, extra); // Linkd一开始不会订阅Server的服务信息,在后面的发现流程中才会订阅。 }
- Server.Startup
{ // 每个模块作为一个服务。下面注册和订阅模块服务。 // 每个Server的CurrentModules是可以配置的。 Foreach (var Module in CurrentModules) { ServiceName = "Game.Server.Module#" + Module.Id; // 名字前缀应用自定义。 ServiceIdentity = Zeze.Config.ServerId; // Zeze服务器编号。 // ProvideIp, ProviderPort:Server监听地址和端口,接受来自其他Server的连接。 RegisterService(ServiceName, ServiceIdentity, ProvideIp, ProviderPort, extra); SubscribeService(ServiceName, Module.SubscribeType); } // 订阅Linkd服务器列表 SubscribeService("Game.Linkd", Simple); // 报告一次初始负载。 Load.Report(0, 0); // 0 online 0 new }
- Server发现Linkd
1. Linkd.Startup向ServiceManager注册自己。 2. ServiceManager广播,Server会收到。 3. Server连接Linkd。 4. Server向Linkd发送Bind协议,马上绑定自己支持的静态模块。 5. Linkd收到Bind。完成模块绑定,并订阅(SubscribeService)该模块服务。
- 发现Server
1. Server.Startup向ServiceManager注册自己支持的所有模块服务。 2. ServiceManager广播,所有订阅者(Linkd和Server)都会收到。 3. Linkd收到Server列表变更不做任何处理,只是等待Server连接过来并处理Bind。 4. Server收到待定的列表时,开始连接新的服务器。 5. 连接建立后,主动方发送自己的地址和端口给被动方,设置本地状态为Ready。 6. 连接建立后,被动放收到主动方的地址和端口,设置本地状态为Ready。
Session & UserState
- AsyncSocket.UserState连接上下文,一般在连接创建的时候初始化,用来保存跟链接相关的 自定义状态,用来实现Session功能。
- Protocol.UserState 协议上下文,从某个连接收到的协议的上下文,默认设置为该连接的 UserState。框架根据自己的需要可以设置新的Protocol.UserState。
- Procedure.UserState存储过程上下文,为协议的处理而创建的存储过程,默认设置为该协议 的UserState。框架可以根据自己的需要修改存储过程的UserState。
See ProviderImplement. ProcessDispatch
Provider Protocol
Bind
Provider发送给Linkd,绑定自己支持的Module。
- 静态绑定 绑定时不指定客户端会话,绑定信息存储在Linkd的ProviderSession中。Linkd收到 静态绑定内的模块的第一个协议请求时,根据这个模块的配置进行负载选择,选择 一个Provider实例派发请求,同时所有静态绑定的模块都和客户端会话关联起来。 以后所有静态绑定的模块的请求都会派发给这个Provider实例。 TODO静态绑定现在被主Server用来注册通用模块。目前静态绑定只有一个唯一的 集合,所以不能跨越多个Provider。考虑支持多个集合的静态绑定。
- 动态绑定 绑定模块到客户端会话。以后这个模块的所有协议都会被Linkd派发给这个Provider。 这个模块也被称作动态模块。
UnBind
Provider发送给Linkd,解除绑定。
Subscribe
Provider发送给Linkd,通知Linkd订阅动态绑定的模块的服务信息。动态绑定的模块没有 只能绑定到某个客户端会话,在绑定前,Linkd一无所知。某些Linkd的功能可能需要动态 模块的服务信息。这时Provider可以通过Subsrcibe协议告知Linkd。这个能力是可选的, 目前Arch实现了这点,但没有真正用到它,算是未来扩充Linkd功能的准备。
Kick
Provider发送给Linkd,踢掉某个客户端会话。
Send
Provider发送给Linkd,转发协议给指定的客户端。
Broadcast
Provider发送给Linkd,向Linkd内所有客户端会话广播。
SetUserState
Provider发送给Linkd,设置客户端会话的状态。这个状态以后在Linkd发送客户端会话相 关的请求给Provider时,原样带上。这个功能一般用来实现LoginSession。
Dispatch
Linkd发送给Provider,转发客户端请求。
LinkBroken
Linkd发送给Provider,报告客户端连接断开。
AnnounceLinkInfo
Linkd发送给Provider,通告Linkd的信息。这是一条保留协议,目前没有报告任何信息。
AnnounceProviderInfo
Provider发送给Linkd,报告Provider的基本信息。
全系统维护启动顺序
- 启动ServiceManage
- 启动GlobalCacheManager
- Linkd Server任意顺序启动。
全系统维护停止顺序
- 停止ServiceManager 必须
- Linkd.AcceptorClient.close() 阻止新用户进来
- Server发送广播通知用户下线
- Server等待一定时间后关闭
- Linkd关闭
- GlobalCacheManager关闭
ProtocolRef
Linkd一般是没有数据库的,它提供给客户端使用的服务LinkdService里面的协议一般需要 转给其他系统实现,如果有少量的协议需要转给GameServer处理(比如Auth协议),可以 使用protocolref把相关协议的处理句柄定义到GameServer的模块内。比如LinkdService的 协议Auth进行验证服务,可以在GameServer的模块User内定义<protocolref ref=”Linkd.Auth”>,然后Linkd的ProcessAuthRequest把请求转给GameServer并在其中处 理。 Linkd可能对协议的转发一般是透明的,有时候需要拦截客户端协议,填充一些只有Linkd 知道的信息(比如填充Linkd看到的客户端的Ip),然后再Dispatch给GameServer。此时可 以在linkd的任意模块内定义<protocolref import=”GameServer.SomeNeedIpReq”>。 这个定 义将把GameServer的相关协议以及Bean引入到linkd内,然后Linkd的定制 LinkdService.DispatchUnknownProtocol,拦截这条协议,并填充,再重新Encode,再调用 基类方法派发(super.DispatchUnknownProtocol)。
Linkd-GameServer内部信息服务
当应用自己的linkd有信息需要暴露给GameServer查询,或者linkd需要查询GameServer 的信息,可以定义一个自己的模块,并把这个模块引入ProviderService。这样linkd和 GameServer之间就可以互相提供服务了。这个功能的例子,比如,linkd提供操纵黑名单的 功能。
影响Linkd选择Provider的开关
- ProviderService. setDisableChoiceFromLinks
禁止或允许linkd选择自己。一般用于优雅的关闭服务器,设置了这个选项以后,新的登录 不会再分配过来,然后等这台服务器上现有的登录处理的差不多了,就可以安全关闭了。
- maxAppVersion
应用定义自己的版本号,如 enum AppVersion { Version = 1 },这个版本号每个版本手动修 改。在初始化的时候,通过Schemas的方法setAppPublishVersion设置到zeze中。Zeze.Arch 会自动报告给Linkd,linkd只会给最新版本的服务器派发新登录。这个可用于慢慢的一台一 台方式重启更新服务器,保证在更新过程中,新登录不会被派发到老版本中。
- 节流过载保护
这个功能是自动的。当provider过载时,linkd自动不会转发新的登录过来。Online组件会 注册过载配置的线程池,当线程池堆积的任务超过配置值,会设置节流过载,并通过 ServiceManager报告自己的负载,Linkd订阅得到负载以后会根据它的负载决定派发。相关 配置Config. providerThreshold = 3000, Config.providerOverload = 5000.
- 服务发现
Zeze的逻辑服务主要实现在Server中,是以模块为单位注册和发现的。在Server启动准备 好之前,一般需要避免请求到达。默认情况下,当用户在App.Start()里面调用 providerApp.startLast()之后,服务就会被其他使用的地方发现,请求就能到达了。但是Server 有一些复杂初始化可能会在startLast之后初始化,此时服务还没有完全准备好。Server被 Linkd发现,完全准备好,并能处理新的来自客户端用户的请求,提供了一些设置方法。 禁止在程序完全准备好之前Linkd派发新的用户请求。
App.Start() {
ProviderService.initDisableChoice(true); // 需要在startService()之前设置。
startService();
providerApp.startLast();
// startLast之后的初始化
ProviderService.setDisableChoiceFromLinks(false); // 开门放狗。
}
- Redirect服务准备好的说明
Redirect是Server之间直连的服务,虽然服务发现也遵循模块注册(startLast),但是Server 之间不容易实现明确的禁止选择策略,所以Redirect也会碰到服务没有准备好的问题。对这 个问题,暂时不提供解决方案,仅提供几条说明。
- redirect推荐按不可靠服务来使用。就是服务不存在或者失败时,调用者应该可以忽略 错误。按这个原则,启动过程中出错或者拒绝也是合理的了。
- 对于按ServerId注册到自己的注册中心,然后RedirectToServer方式使用的服务,定义 为应用自行处理。比如地图服务器分配地图实例的Redirect,服务启动完全准备好才注 册到自己的分配中心,中途避免新的分配进来。这样可以绕过Zeze不提供Redirect手 动控制客户端请求的问题。
- startLast才会开启模块注册,所以此后服务器(模块)才会被其他服务器知道。按这个 规则,推荐redirect的实现,在startLast之前准备好。对于无法这样处理的Redirect 服务,也就是说startLast之后的初始化对于redirect来说,需要自己在收到请求后处理 并拒绝。