Quick Start
建议从这个工程入手:zezeboot
进程本身直接提供网络服务,这里的例子快速展现zeze的核心能力。这里没有考虑完整的 网络框架支持,稍后讲到的Arch是zeze的网络框架,包含网关和主逻辑服务的连接以及架 构。Zeze最核心的能力是事务的支持,只需要三个步骤就能开始使用zeze事务。
- 定义数据结构
- 生成代码
- 访问Table以及使用自定义数据结构访问修改数据
下面稍微详细的用例子介绍三个步骤。
- 定义数据结构
<solution name="QuickStart" ModuleIdAllowRanges="1-100,101">
<module name="Role" id="1">
<bean name="BRole">
<variable id="1" name="Level" type="int"/>
<variable id="2" name="Experience" type="long"/>
</bean>
<bean name="BBag">
<variable id="1" name="Items" type="list[int]"/>
</bean>
<table name="tRole" key="long" value="BRole"/>
key is roleid
<table name="tBag" key="long" value="BBag"/>
key is roleid
<bean name="BAddExperience">
<variable id="1" name="Experience" type="long"/>
</bean>
<rpc name="AddExperience" argument="BAddExperience" transactionLevel="Serializable" handle="server"/>
</module>
<project name="GameServer" scriptdir="src" platform="java">
<service name="Server" handle="server">
<module ref=" Role"/>
</service>
</project>
</solution>
- 生成代码
把上面的xml保存为solution.xml,切换到文件所在目录,执行Gen.exe solution.xml。在当 前目录下会创建GameServer目录(project name),在里面会找到生成的代码。这里先不细说 了。
- 使用例子 (java)
// Gen.exe 会根据定义自动生成空的处理函数。里面的代码就是自己的实现了。
@Override
long ProcessAddExperience(QuickStart.Role.AddExperience r) {
var session = ProviderUserSession.get(r); // 这是个魔法,反正拿到会话了。
var roleId = session.getRoleId();
long newExperience = r.Argument.getExperience();
var role = _tRole.getOrAdd(roleId);
role.setExperience(role.getExperience() + newExperience);
while (role.getExperience() >= ExperienceConfig.get(role.getLevel())) {
role.setExperience(role.getExperience() - ExperienceConfig.get(role.getLevel()));
role.setLevel(role.getLevel() + 1);
if (role.getLevel() % 10 == 0) {
addItemToBag(roleId, LevelRewardItemConfig.get(role.getLevel()));
}
}
return 0;
}
// 多数时候,Bag是另一个模块(module)。这里都定义到一起了。
void addItemToBag(long roleid, int itemId) {
var bag = _tBag.getOrAdd(roleId);
if (bag.getItems().size() > 100)
throw new RuntimeException("Bag Is Full"); // 别担心,包满了,所有修改都会回滚。
bag.getItems().add(itemId);
}
上手准备
- git clone https://gitee.com/e2wugui/zeze
- 运行 zeze\gen_use_publish.bat。
- 再次 Build All,应该就正常了。
- Csharp版使用vs2022或其他兼容编辑器打开zeze/Zeze.sln
- Java版使用idea打开 zeze/ZezeJava即可正常Build All。
Java开发准备 (如果只关注C#开发,可跳过)
- JDK 21 任意发行版均可
- 推荐: https://adoptium.net/zh-CN/temurin/archive/?version=21
- 备选: https://jdk.java.net/21/
- IntelliJ IDEA 免费社区版(Community)即可, 2023.3版以上
- https://www.jetbrains.com/idea/download/
- (可选) Maven: https://maven.apache.org/download.cgi
C#开发准备 (如果只关注Java开发,可只安装.NET SDK)
- .NET 8 SDK (如果安装下面的Visual Studio,可以不用单独安装这个)
- https://dotnet.microsoft.com/en-us/download/dotnet/8.0 (通常选择Windows, x64)
- Visual Studio 2022 (可用免费社区版,但需要联网激活,且只能个人或小规模商用)
- Windows版本: https://visualstudio.microsoft.com/zh-hans/downloads/ (必选组件: .NET桌面开发; 可选组件: Node.js开发)
- (可选) VSCode (完全免费的轻量级IDE)
- https://code.visualstudio.com/ (安装官方C#插件)
Java编译
- 启动IDEA, 打开zeze框架中的 ZezeJava 目录, 会自动加载4个模块(框架核心+测试+2个示例)
- 如果编译找不到某些类, 需要执行下面的”生成代码”
C#编译
- 仅用SDK: 在zeze框架根目录下使用命令行编译: dotnet build Zeze.sln
- 使用VS2022: 启动VS2022, 打开zeze框架根目录下的Zeze.sln, 执行编译命令
- 如果编译找不到某些类, 需要执行下面的”生成代码”
Java Sample
- 需要先执行上面的C#编译Gen工程
- 在 ZezeJava/ZezexJava, ZezeJava/ZezeJavaTest 目录下执行 gen.bat
C# Sample
- 需要先执行上面的C#编译Gen工程
- 在 Sample, UnitTest 目录下执行 gen.bat
分布式架构
上图是Zeze默认的框架结构,具有一定通用性。某些情况下,自己可以搭建全新的架构。
- Linkd 连接进程,负责负载分配。Zeze提供一个默认实现,配置以后,生成代码,即可零 开发直接使用。生成代码的目的是为了支持重载某些方法,实现定制。一般来说Arch提供 的linkd功能足够了,实际开发量很低。除非你需要对linkd规则做很大的定制。
- GameServer 主服务器,实现业务逻辑。开发的主要产出。
- ServiceManager 服务注册和发现服务器。Zeze提供,不需要开发。
- GlobalCacheManager 一致性缓存支持服务器。Zeze提供,不需要开发。
- Database 后端数据库,支持Mysql,Sqlserver,Tikv。将来Zeze会根据需要支持更多的数据库。
单实例架构
系统内只有一个GameServer,客户端直接连接GameServer。此时不需要ServiceManager 和GlobalCacheManager。由于分布式架构本质上也包含了单实例架构,所以这个模式的例 子被删除了(可用从git的历史中找到)。需要说明的是,不管那种架构,业务开发代码几 乎一样,架构应该仅影响程序框架。如果需要实现全新的架构,最好不要影响业务开发,方 便需要的时候进行迁移。
创建Zeze应用(Java)
跟创建普通Idea项目一样创建工程,把项目到Zeze依赖建立好,一个普通的Zeze应用就 创建好了。这里实际上不需要什么特殊操作,因为Zeze虽然是一个框架,但没有太多侵入 要求,本身是作为一个库提供的。当然,为了发挥Zeze的能力,solution.xml是必要的。下 面是第一个简单solution.xml。
<?xml version="1.0" encoding="utf-8"?>
<solution name="Game" ModuleIdAllowRanges="1-1000"> Game是解决方案的名字,它将作为生成代码的根名字空间。
<import file="../ZezeJava/solution.zeze.xml"/>
路径需要修改为正确的相对目录。
<module name="Login" id="1" hot="true">自定义模块
<bean name="BCreateRole">自定义Bean
<variable id="1" name="Name" type="string"/>
</bean>
<bean name="BRole">
<variable id="1" name="Id" type="long"/>
<variable id="2" name="Name" type="string"/>
</bean>
<bean name="BRoles">
<variable id="1" name="RoleList" type="list" value="BRole"/>
<variable id="2" name="LastLoginRoleId" type="long"/>
</bean>
<rpc name="CreateRole" argument="BCreateRole" result="BRole" handle="server"/>
自定义Rpc,客户端发送给服务器请求数据。
<rpc name="GetRoleList" result="BRoles" handle="server"/>
<bean name="BRoleId">
<variable id="1" name="Id" type="long"/>
</bean>
<bean name="BAccount">
<variable id="1" name="Roles" type="set[long]"/>
<variable id="2" name="LastLoginRoleId" type="long"/>
</bean>
<table name="taccount" key="string" value="BAccount"/>
<table name="trole" key="long" value="BRole"/>存储Role的数据表
<table name="trolename" key="string" value="BRoleId"/>
</module>
<project name="server" gendir="." scriptdir="src" platform="java" GenTables="">
name=”server” 项目名字,必须和idea创建的功能名字一样。以后生成代码会放到这个工程目录下面。
gendir="." 生成代码输出的根目录,solution.xml放在server的上一级目录时,就是当前目录。
scriptdir="src" 源代码目录相对路径。默认是src,一般idea需要改成“src/main/java/”
<service name="Server" handle="server" base="Zeze.Arch.ProviderService">
<module ref="Login"/>这个网络服务包含的模块
</service>
定义网络服务
<ModuleStartOrder>
</ModuleStartOrder>
<service name="ServerDirect" handle="server,client"
base="Zeze.Arch.ProviderDirectService">
</service>分布式情况下,Server之间互联的网络服务定义。
</project>
</solution>
代码生成以后,工程server/Gen包含生成代码,这里的每次生成都会被覆盖,这些代码主 要是Bean,Table,Protocok,Rpc等。MyApp/src里面也会生成一些代码,这些代码是协 议处理框架的实现部分,不会全部被覆盖。其中最主要的是(对应上面的solution.xml) MyApp.Login.ModuleLogin.java。一般如下样子:
package MyApp.Login;
import Game.App;
import Zeze.Arch.ProviderUserSession;
import Zeze.Component.AutoKey;
import Zeze.Hot.HotService;
import Zeze.Transaction.Procedure;
public final class ModuleLogin extends AbstractModule {
public void Start(App app) {
}
public void Stop(App app) {
}
@Override
protected long ProcessCreateRoleRequest(CreateRole rpc) {
// 在这里编写实现逻辑,实现例子参见
// zeze/ZezeJava/ZezexJava/server/src/Game/Login/ModuleLogin.java
return Procedure.NotImplement;
}
@Override
protected long ProcessGetRoleListRequest(GetRoleList rpc) {
// 在这里编写实现逻辑,实现例子参见
// zeze/ZezeJava/ZezexJava/server/src/Game/Login/ModuleLogin.java
return Procedure.NotImplement;
}
}
创建Arch.Linkd(Java)
- Solution.linkd.xml
<?xml version="1.0" encoding="utf-8"?>
<solution name="Zezex" ModuleIdAllowRanges="10000-10999">
Zezex是解决方案的名字,用于生成代码的根名字空间。整个解决方案使用一个名字是
可以的,参考上节的solution.xml的例子,这里也可以使用Game。为了避免给阅读ZezexJava
例子工程造成困扰,这里保留了Zezex的名字。
<module name="Linkd" id="10000"> linkd专有的模块
<bean name="BAuth">
<variable id="1" name="Account" type="string"/>
<variable id="2" name="Token" type="string"/> security. maybe password
</bean>
<rpc name="Auth" argument="BAuth" handle="server">
<enum name="Success" value="0"/>
<enum name="Error" value="1"/>
</rpc>
</module>
<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>
</solution>
- App.java::Start
Solution.linkd.xml生成代码时,会生成linkd/src/Zezex/App.java,其中Start方法是程序初 始化启动的函数。默认生成的Start是没有引入Zeze.Arch模块的,使用它需要自己加入几 行代码。下面是来自Zezex例子,变动部分看斜体部分。
public void Start(int serverId, int linkPort, int providerPort) throws Exception {
//1. 默认生成的Start是没有参数的,由于Start是用户代码,
// Zezex已经把它修改成支持更多参数。
//2. 下面Config相关的设置已经被定制了。
var config = Config.load("linkd.xml");
if (serverId != -1)
config.setServerId(serverId);
if (linkPort != -1) {
config.getServiceConfMap().get("LinkdService").forEachAcceptor((a) ->
a.setPort(linkPort));
}
if (linkPort != -1) {
config.getServiceConfMap().get("ProviderService").forEachAcceptor((a) ->
a.setPort(providerPort));
}
createZeze(config);
createService();
// 3. 这几行代码不是生成的,而是使用Zeze.Arch的初始化代码。
LinkdProvider = new LinkdProvider();
LinkdApp = new LinkdApp("Game.Linkd", Zeze, LinkdProvider, ProviderService,
LinkdService, LoadConfig());
createModules();
// Start
Zeze.start(); // 启动数据库
startModules(); // 启动模块,装载配置什么的。
//4. 这样代码不是生成的,是应用特殊的初始化。
AsyncSocket.setSessionIdGenFunc(PersistentAtomicLong.getOrAdd(LinkdApp.getName
())::next);
startService(); // 启动网络. after setSessionIdGenFunc
//5. 这是Zeze.Arch的初始化代码。
LinkdApp.registerService(null);
}
先说声抱歉,由于没有花时间去整理第一次空项目生成代码的样子,上面的例子直接来自 ZezexJava。如果你发现你的第一次生成的代码,和斜体部分除外还对应不上,请自行判断 一下。总的来说,第一次生成的代码就是一个完整的Zeze应用,但是不包括Zeze.Arch, 上面斜体部分的代码主要是使用Zeze.Arch需要做的改动。
创建Arch.Serverd(Java)
Solution.xml 参见上上节的创建Zeze应用。这里只说明一下App.java::Start怎么引入 Zeze.Arch。
public void Start(int serverId, int providerDirectPort) throws Exception {
// 1. 默认生成的Start是没有参数的,由于Start是用户代码,Zezex已经把它修改成支持更多参数。
// 2. 下面Config相关的设置已经被定制了。
var config = Config.load("server.xml");
if (serverId != -1) {
config.setServerId(serverId); // replace from args
}
var commitService = config.getServiceConf("Zeze.Dbh2.Commit");
commitService.forEachAcceptor((a) -> {
a.setPort(a.getPort() + config.getServerId());
});
if (providerDirectPort != -1) {
final int port = providerDirectPort;
config.getServiceConfMap().get("ServerDirect").forEachAcceptor((a) ->
a.setPort(port));
}
// create
createZeze(config);
createService();
// 3. 引入Zeze.Arch的代码
Provider = new ProviderWithOnline();
Provider.setControlKick(BKick.eControlReportClient);
ProviderDirect = new ProviderDirectWithTransmit();
ProviderApp = new ProviderApp(Zeze, Provider, Server,
"Game.Server.Module#",
ProviderDirect, ServerDirect, "Game.Linkd", LoadConfig());
Provider.create(this);
createModules();
// 4. 程序运行过程中也会动态生成代码并编译,这些代码一般看不到,这里把代码生成出来,用来调试的。
if (GenModule.instance.genFileSrcRoot != null) {
System.out.println("---------------");
System.out.println("New Source File Has Generate. Re-Compile Need.");
System.exit(0);
}
// 5. Zeze组件的引入,组件的功能参见后面Component,Collections相关部分文档。
taskModule = new TaskBase.Module(getZeze());
LinkedMapModule = new LinkedMap.Module(Zeze);
DepartmentTreeModule = new DepartmentTree.Module(Zeze, LinkedMapModule);
Zeze.getTimer().initializeOnlineTimer(ProviderApp);
// start
Zeze.start(); // 启动数据库
startModules(); // 启动模块,装载配置什么的。
// 6. Zeze.Arch 初始化
Provider.start();
PersistentAtomicLong socketSessionIdGen =
PersistentAtomicLong.getOrAdd("Game.Server." + config.getServerId());
AsyncSocket.setSessionIdGenFunc(socketSessionIdGen::next);
startService(); // 启动网络
// 7. Zeze.Arch 启动
// 服务准备好以后才注册和订阅。
ProviderApp.startLast(ProviderModuleBinds.load(), modules);
}