ioGame21 网络编程框架发布,史诗级增强
相关示例仓库已同步更新,升级过程可参考示例仓库。
ioGame21 首发计划
功能支持 | 完成 | 描述 | issu |
---|---|---|---|
游戏对外服开放自定义协议 | ✅ | 功能增强 | #213 |
游戏对外服缓存 | ✅ | 功能增强、性能提升 | #76 |
FlowContext 增加通信能力,提供同步、异步、异步回调的便捷使用 | ✅ | 功能增强 | #235 |
虚拟线程支持; 各逻辑服之间通信阻塞部分,改为使用虚拟线程, 避免阻塞业务线程 | ✅ | 功能增强、性能提升 | |
默认不使用 bolt 线程池,减少上下文切换。 ioGame17:netty --> bolt 线程池 --> ioGame 线程池。 ioGame21: 1. netty --> ioGame 线程池。 2. 部分业务将直接在 netty 线程中消费业务。 | ✅ | 性能提升 | |
全链路调用日志跟踪;日志增强 traceId | ✅ | 功能增强 | #230 |
移除文档自动生成,改为由开发者调用触发。 | ✅ | 整理 | |
移除过期代码 | ✅ | 整理 | #237 |
分布式事件总线 可以代替 redis pub sub 、 MQ ,并且具备全链路调用日志跟踪,这点是中间件产品做不到的。 | ✅ | 功能增强 | #228 |
日志库使用新版本 slf4j 2.0 | ✅ | ||
Fury 支持。 Fury 是一个基于JIT动态编译和零拷贝的高性能多语言序列化框架 | 观望中 | 在计划内,不一定会支持 | 因在发布 ioGame21 时,Fury 还未发布稳定版本,所以这里暂不支持。 |
心跳响应前的回调 | ✅ | 功能增强 | #234 |
FlowContext 增加更新、获取元信息的便捷使用 | ✅ | 功能增强 | #236 |
ioGame21 首发内容简介
在 ioGame21 中,该版本做了数百项优化及史诗级增强。
- 文档方面
- 线程管理域方面的开放与统一、减少线程池上下文切换
- FlowContext 增强
- 新增通讯方式 - 分布式事件总线
- 游戏对外服方面增强
- 全链路调用日志跟踪
- 各逻辑服之间通信阻塞部分,改为使用虚拟线程, 避免阻塞业务线程,从而使得框架的吞吐量得到了巨大的提升。
游戏对外服相关
#76 游戏对外服缓存
更多的介绍,请阅读游戏对外服缓存文档。
游戏对外服缓存,可以将一些热点的业务数据缓存在游戏对外服中,玩家每次访问相关路由时,会直接从游戏对外服的内存中取数据。这样可以避免反复请求游戏逻辑服,从而达到性能的超级提升;
private static void extractedExternalCache() {
// 框架内置的缓存实现类
DefaultExternalCmdCache externalCmdCache = new DefaultExternalCmdCache();
// 添加到配置中
ExternalGlobalConfig.externalCmdCache = externalCmdCache;
// 配置缓存 3-1
externalCmdCache.addCmd(3, 1);
}
#213 游戏对外服开放自定义协议
更多的介绍,请阅读对外服的协议说明文档。
开发者可自定义游戏对外服协议,用于代替框架默认的 ExternalMessage 公共对外协议。
#234 心跳响应前的回调
更多的介绍,请阅读心跳设置与心跳钩子文档。
在部分场景下,在响应心跳前可添加当前时间,使得客户端与服务器时间同步。
@Slf4j
public class DemoIdleHook implements SocketIdleHook {
... ... 省略部分代码
volatile byte[] timeBytes;
public DemoIdleHook() {
updateTime();
// 每秒更新当前时间
TaskKit.runInterval(this::updateTime, 1, TimeUnit.SECONDS);
}
private void updateTime() {
LongValue data = LongValue.of(TimeKit.currentTimeMillis());
// 避免重复序列化,这里提前序列化好时间数据
timeBytes = DataCodecKit.encode(data);
}
@Override
public void pongBefore(BarMessage idleMessage) {
// 把当前时间戳给到心跳接收端
idleMessage.setData(timeBytes);
}
}
FlowContext
#235 FlowContext 增加通信能力,提供同步、异步、异步回调的便捷使用
更多的介绍,请阅读 FlowContext 文档。
// 跨服请求 - 同步、异步回调演示
void invokeModuleMessage() {
// 路由、请求参数
ResponseMessage responseMessage = flowContext.invokeModuleMessage(cmdInfo, yourData);
RoomNumMsg roomNumMsg = responseMessage.getData(RoomNumMsg.class);
log.info("同步调用 : {}", roomNumMsg.roomCount);
// --- 此回调写法,具备全链路调用日志跟踪 ---
// 路由、请求参数、回调
flowContext.invokeModuleMessageAsync(cmdInfo, yourData, responseMessage -> {
RoomNumMsg roomNumMsg = responseMessage.getData(RoomNumMsg.class);
log.info("异步回调 : {}", roomNumMsg.roomCount);
});
}
// 广播
public void broadcast(FlowContext flowContext) {
// 全服广播 - 路由、业务数据
flowContext.broadcast(cmdInfo, yourData);
// 广播消息给单个用户 - 路由、业务数据、userId
long userId = 100;
flowContext.broadcast(cmdInfo, yourData, userId);
// 广播消息给指定用户列表 - 路由、业务数据、userIdList
List<Long> userIdList = new ArrayList<>();
userIdList.add(100L);
userIdList.add(200L);
flowContext.broadcast(cmdInfo, yourData, userIdList);
// 给自己发送消息 - 路由、业务数据
flowContext.broadcastMe(cmdInfo, yourData);
// 给自己发送消息 - 业务数据
// 路由则使用当前 action 的路由。
flowContext.broadcastMe(yourData);
}
#236 FlowContext 增加更新、获取元信息的便捷使用
更多的介绍,请阅读 FlowContext 文档。
void test(MyFlowContext flowContext) {
// 获取元信息
MyAttachment attachment = flowContext.getAttachment();
attachment.nickname = "渔民小镇";
// [同步]更新 - 将元信息同步到玩家所在的游戏对外服中
flowContext.updateAttachment();
// [异步无阻塞]更新 - 将元信息同步到玩家所在的游戏对外服中
flowContext.updateAttachmentAsync();
}
public class MyFlowContext extends FlowContext {
MyAttachment attachment;
@Override
@SuppressWarnings("unchecked")
public MyAttachment getAttachment() {
if (Objects.isNull(attachment)) {
this.attachment = this.getAttachment(MyAttachment.class);
}
return this.attachment;
}
}
线程相关
更多的介绍,请阅读 ioGame 线程相关文档。
虚拟线程支持,各逻辑服之间通信阻塞部分使用虚拟线程来处理,避免阻塞业务线程。
默认不使用 bolt 线程池,减少上下文切换。ioGame21 业务消费的线程相关内容如下:
- netty --> ioGame 线程池。
- 部分业务将直接在 netty 线程中消费业务。
在 ioGame21 中,框架内置了 3 个线程执行器管理域,分别是
- UserThreadExecutorRegion ,用户线程执行器管理域。
- UserVirtualThreadExecutorRegion ,用户虚拟线程执行器管理域。
- SimpleThreadExecutorRegion ,简单的线程执行器管理域。
从工具类中得到与用户(玩家)所关联的线程执行器
@Test
public void userThreadExecutor() {
long userId = 1;
ThreadExecutor userThreadExecutor = ExecutorRegionKit.getUserThreadExecutor(userId);
userThreadExecutor.execute(() -> {
// print 1
log.info("userThreadExecutor : 1");
});
userThreadExecutor.execute(() -> {
// print 2
log.info("userThreadExecutor : 2");
});
}
@Test
public void getUserVirtualThreadExecutor() {
long userId = 1;
ThreadExecutor userVirtualThreadExecutor = ExecutorRegionKit.getUserVirtualThreadExecutor(userId);
userVirtualThreadExecutor.execute(() -> {
// print 1
log.info("userVirtualThreadExecutor : 1");
});
userVirtualThreadExecutor.execute(() -> {
// print 2
log.info("userVirtualThreadExecutor : 2");
});
}
@Test
public void getSimpleThreadExecutor() {
long userId = 1;
ThreadExecutor simpleThreadExecutor = ExecutorRegionKit.getSimpleThreadExecutor(userId);
simpleThreadExecutor.execute(() -> {
// print 1
log.info("simpleThreadExecutor : 1");
});
simpleThreadExecutor.execute(() -> {
// print 2
log.info("simpleThreadExecutor : 2");
});
}
从 FlowContext 中得到与用户(玩家)所关联的线程执行器
void executor() {
// 该方法具备全链路调用日志跟踪
flowContext.execute(() -> {
log.info("用户线程执行器");
});
// 正常提交任务到用户线程执行器中
// getExecutor() 用户线程执行器
flowContext.getExecutor().execute(() -> {
log.info("用户线程执行器");
});
}
void executeVirtual() {
// 该方法具备全链路调用日志跟踪
flowContext.executeVirtual(() -> {
log.info("用户虚拟线程执行器");
});
// 正常提交任务到用户虚拟线程执行器中
// getVirtualExecutor() 用户虚拟线程执行器
flowContext.getVirtualExecutor().execute(() -> {
log.info("用户虚拟线程执行器");
});
// 示例演示 - 更新元信息(可以使用虚拟线程执行完成一些耗时的操作)
flowContext.executeVirtual(() -> {
log.info("用户虚拟线程执行器");
// 更新元信息
flowContext.updateAttachment();
// ... ... 其他业务逻辑
});
}
日志相关
日志库使用新版本 slf4j 2.x
#230 支持全链路调用日志跟踪;
更多的介绍,请阅读全链路调用日志跟踪文档。
开启 traceId 特性
该配置需要在游戏对外服中设置,因为游戏对外服是玩家请求的入口。
// true 表示开启 traceId 特性
IoGameGlobalConfig.openTraceId = true;
将全链路调用日志跟踪插件 TraceIdInOut 添加到业务框架中,表示该游戏逻辑服需要支持全链路调用日志跟踪。如果游戏逻辑服没有添加该插件的,表示不需要记录日志跟踪。
BarSkeletonBuilder builder = ...;
// traceId
TraceIdInOut traceIdInOut = new TraceIdInOut();
builder.addInOut(traceIdInOut);
分布式事件总线
#228 分布式事件总线是新增的通讯方式,可以代替 redis pub sub 、 MQ ...等中间件产品;分布式事件总线具备全链路调用日志跟踪,这点是中间件产品所做不到的。
文档 - 分布式事件总线
ioGame 分布式事件总线,特点
- 使用方式与 Guava EventBus 类似
- 具备全链路调用日志跟踪。(这点是中间件产品做不到的)
- 支持跨多个机器、多个进程通信
- 支持与多种不同类型的多个逻辑服通信
- 纯 javaSE,不依赖其他服务,耦合性低。(不需要安装任何中间件)
- 事件源和事件监听器之间通过事件进行通信,从而实现了模块之间的解耦
- 当没有任何远程订阅者时,将不会触发网络请求。(这点是中间件产品做不到的)
下面两个订阅者是分别在不同的进程中的,当事件发布后,这两个订阅者都能接收到 UserLoginEventMessage 消息。
@ActionController(UserCmd.cmd)
public class UserAction {
... 省略部分代码
@ActionMethod(UserCmd.fireEvent)
public String fireEventUser(FlowContext flowContext) {
long userId = flowContext.getUserId();
log.info("fire : {} ", userId);
// 事件源
var userLoginEventMessage = new UserLoginEventMessage(userId);
// 发布事件
flowContext.fire(userLoginEventMessage);
return "fireEventUser";
}
}
// 该订阅者在 【UserLogicStartup 逻辑服】进程中,与 UserAction 同在一个进程
@EventBusSubscriber
public class UserEventBusSubscriber {
@EventSubscribe(ExecutorSelector.userExecutor)
public void userLogin(UserLoginEventMessage message) {
log.info("event - 玩家[{}]登录,记录登录时间", message.getUserId());
}
}
// 该订阅者在 【EmailLogicStartup 逻辑服】进程中。
@EventBusSubscriber
public class EmailEventBusSubscriber {
@EventSubscribe
public void mail(UserLoginEventMessage message) {
long userId = message.getUserId();
log.info("event - 玩家[{}]登录,发放 email 奖励", userId);
}
}
小结
在 ioGame21 中,该版本做了数百项优化及史诗级增强。
- 在线文档方面
- 线程管理域方面的开放与统一、减少线程池上下文切换
- FlowContext 增强
- 新增通讯方式 - 分布式事件总线
- 游戏对外服方面增强
- 全链路调用日志跟踪
ioGame17 迁移到 ioGame21
这里介绍 ioGame17 迁移到 ioGame21。
升级建议
升级建议,先将 ioGame17 升级到 17 系列中的最后一个版本(17.1.61)。查看是否使用了已标记为过期的方法,先将过期方法改为正常的(标记为过期的方法上有注释及推荐使用的代替方法)。
整体破坏性变更内容较少,基本只涉及到游戏对外服,原因(#213)是游戏对外服开放了自定义协议。如果开发者没有在游戏对外服做任何扩展的(如,编解码、心跳、其他 netty handler ...等 ),那么升级将是丝滑的。
游戏对外服 - 变更涉及
#213 游戏对外服开放了自定义协议,所以相关的 netty handler 需要做变更。如果开发者的项目中没有对游戏对外服做扩展的,可以忽略该小节的内容。
1. netty - handler 部分
如果开发者在游戏对外服中扩展了 netty handler 的,需要将参数由 ExternalMessage 改为 BarMessage(内部协议)。
参考示例
更详细的参考框架内置 handler 相关源码。
注意点:netty Handler 参数使用的是 BarMessage。因为在 ioGame21 中,默认的编解码器会将 BarMesage 转为 ExternalMessage。
public class WebSocketExternalCodec extends MessageToMessageCodec<BinaryWebSocketFrame, BarMessage> {
... ...省略部分代码
@Override
protected void encode(ChannelHandlerContext ctx, BarMessage message, List<Object> out) {
ExternalMessage externalMessage = ExternalCodecKit.convertExternalMessage(message);
... ...省略部分代码
}
@Override
protected void decode(ChannelHandlerContext ctx, BinaryWebSocketFrame binary, List<Object> out) {
... ...省略部分代码
ExternalMessage externalMessage = DataCodecKit.decode(bytes, ExternalMessage.class);
BarMessage message = ExternalCodecKit.convertRequestMessage(externalMessage);
//【游戏对外服】接收【游戏客户端】的消息
out.add(message);
}
}
public final class CmdCheckHandler extends SimpleChannelInboundHandler<BarMessage>
implements CmdRegionsAware {
... ...省略部分代码
@Override
protected void channelRead0(ChannelHandlerContext ctx, BarMessage message) {
}
}
2. 心跳钩子部分
#234 新增心跳响应前的回调方法 pongBefore,开发者可以按需使用;比如,可以在心跳响应前添加上当前时间。
注意点:给客户端主动发送消息时,需要发送 BarMesage,不能使用 ExternalMessage。因为在 ioGame21 中,默认的编解码器会将 BarMesage 转为 ExternalMessage。
参考代码如下
public final class YourSocketIdleHook implements SocketIdleHook {
@Override
public void pongBefore(BarMessage idleMessage) {
// 把当前时间戳给到心跳接收端
LongValue data = LongValue.of(TimeKit.currentTimeMillis());
idleMessage.setData(data);
}
@Override
public boolean callback(UserSession userSession, IdleStateEvent event) {
IdleState state = event.state();
if (state == IdleState.READER_IDLE) {
/* 读超时 */
log.debug("READER_IDLE 读超时");
} else if (state == IdleState.WRITER_IDLE) {
/* 写超时 */
log.debug("WRITER_IDLE 写超时");
} else if (state == IdleState.ALL_IDLE) {
/* 总超时 */
log.debug("ALL_IDLE 总超时");
}
BarMessage message = ExternalCodecKit.createErrorIdleMessage(ActionErrorEnum.idleErrorCode);
// 错误消息
message.setValidatorMsg(ActionErrorEnum.idleErrorCode.getMsg() + " : " + state.name());
// 通知客户端,触发了心跳事件
userSession.writeAndFlush(message);
// 返回 true 表示通知框架将当前的用户(玩家)连接关闭
return true;
}
}
日志库使用新版本 slf4j 2.x。
示例库中,提供了 logback 1.4.14 的使用参考。
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
注意事项,ioGame 框架只做了 logback 的兼容。如果你使用了其他的日志库,可参考 LoggerSpaceFactory4LogbackBuilder ,使用类路径覆盖的方式来兼容(因为 bolt 目前 slf4j 默认实现只有 1.x 相关的)。
所以,较为方便的升级方法是统一使用 logback 日志库。否则,需要你自己做其他日志库的兼容实现扩展。
业务框架插件包名变更
flow interal 包名纠正为 internal,这部分只涉及到业务框架插件相关的;将旧的包名删除后,重新引入新的包名即可。
对接文档生成
在 ioGame21 中,我们移除了在启动时对接文档的自动生成。如有需要的,开发者可主动调用对接文档的生成,使用参考:
public static void main(String[] args) {
... 省略部分代码
new NettyRunOne()
... ...
.startup();
// 生成对接文档
BarSkeletonDoc.me().buildDoc();
}