Skip to content

ioGame21 网络编程框架发布,史诗级增强

Compare
Choose a tag to compare
@iohao iohao released this 21 Feb 03:17
· 170 commits to main since this release

框架版本更新日志 (yuque.com)

相关示例仓库已同步更新,升级过程可参考示例仓库。

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 业务消费的线程相关内容如下:

  1. netty --> ioGame 线程池。
  2. 部分业务将直接在 netty 线程中消费业务。

在 ioGame21 中,框架内置了 3 个线程执行器管理域,分别是

  1. UserThreadExecutorRegion ,用户线程执行器管理域。
  2. UserVirtualThreadExecutorRegion ,用户虚拟线程执行器管理域。
  3. 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。

文档:17 迁移到 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();
}