Skip to content

Releases: iohao/ioGame

ioGame 21.8,java 高性能游戏服务器框架;netty 高性能游戏服务器框架

19 May 06:49
Compare
Choose a tag to compare

21.8

文档与日志


版本更新汇总

  • [light-game-room] #278 桌游类、房间类游戏的扩展模块,简化与规范化房间管理相关的、开始游戏流程相关的、玩法操作相关的相关扩展
  • [core] #290 新增广播文档构建器,简化生成广播对接文档
  • [示例集合整理] 将 SimpleExample(文档中所有功能点的示例)、SpringBootExample(综合示例)、ioGameWeb2Game(web 转游戏 - 示例理解篇)、fxglSimpleGame(移动同步 FXGL + netty)合并成一个示例项目。

[core]

#290 新增广播文档构建器,简化生成广播对接文档

下面是使用示例

public class MyLogicServer extends AbstractBrokerClientStartup {
    @Override
    public BarSkeleton createBarSkeleton() {
        // 业务框架构建器
        BarSkeletonBuilder builder = ...
        
        // 错误码、广播、推送对接文档生成
        extractedDco(builder);

        return builder.build();
    }
    
    private void extractedDco(BarSkeletonBuilder builder) {
        // 错误码
        Arrays.stream(GameCode.values()).forEach(builder::addMsgExceptionInfo);

        // UserCmd
        builder.addBroadcastDoc(BroadcastDoc.newBuilder(UserCmd.of(UserCmd.enterSquare))
                .setDataClass(SquarePlayer.class)
                .setDescription("新玩家加入房间,给房间内的其他玩家广播")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(UserCmd.of(UserCmd.move))
                .setDataClass(SquarePlayerMove.class)
                .setDescription("其他玩家的移动")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(UserCmd.of(UserCmd.offline))
                .setDataClass(LongValue.class)
                .setDescription("有玩家下线了。userId")
        );

        // room
        builder.addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.roomUpdateBroadcast))
                .setDataClass(FightRoomNotice.class)
                .setDescription("房间更新通知")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.playerEnterRoomBroadcast))
                .setDataClass(FightPlayer.class)
                .setDescription("有新玩家加入房间")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.enterRoom))
                .setDataClass(FightEnterRoom.class)
                .setDescription("玩家自己进入房间")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.dissolveRoomBroadcast))
                .setDescription("解散房间")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.quitRoom))
                .setDataClass(LongValue.class)
                .setDescription("有玩家退出房间了。userId")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.ready))
                .setDataClass(PlayerReady.class)
                .setDescription("有玩家准备或取消准备了")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.nextRoundBroadcast))
                .setDataClass(IntValue.class)
                .setDescription("对局开始,通知玩家开始选择。round 当前对局数")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.operationBroadcast))
                .setDataClass(LongValue.class)
                .setDescription("通知其他玩家,有玩家做了选择。userId")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.littleSettleBroadcast))
                .setDataClassList(FightRoundPlayerScore.class)
                .setDescription("广播玩家对局分数")
        ).addBroadcastDoc(BroadcastDoc.newBuilder(RoomCmd.of(RoomCmd.gameOverBroadcast))
                .setDescription("游戏结束")
        );
    }
}

其他扩展阅读

下面是生成后的对接文档预览

==================== 游戏文档格式说明 ====================
https://www.yuque.com/iohao/game/irth38#cJLdC

==================== FightHallAction 大厅(类似地图) ====================
路由: 1 - 1  --- 【登录】 --- 【FightHallAction:67】【loginVerify】
    方法参数: LoginVerify loginVerify 登录验证
    方法返回值: UserInfo 玩家信息
 
路由: 1 - 2  --- 【进入大厅】 --- 【FightHallAction:95】【enterSquare】
    方法参数: EnterSquare enterSquare 进入大厅
    方法返回值: ByteValueList<SquarePlayer> 所有玩家
    广播推送: SquarePlayer 新玩家加入房间,给房间内的其他玩家广播
 
路由: 1 - 4  --- 【玩家移动】 --- 【FightHallAction:131】【move】
    方法参数: SquarePlayerMove squarePlayerMove 玩家移动
    方法返回值: void 
    广播推送: SquarePlayerMove 其他玩家的移动
 
路由: 1 - 5  --- 【玩家下线】 --- 【FightHallAction:155】【offline】
    方法返回值: void 
    广播推送: LongValue 有玩家下线了。userId
 

==================== FightRoomAction  ====================
路由: 2 - 1  --- 【玩家创建新房间】 --- 【FightRoomAction:63】【createRoom】
    方法返回值: void 
 
路由: 2 - 2  --- 【玩家进入房间】 --- 【FightRoomAction:96】【enterRoom】
    方法参数: LongValue roomId      房间号
    方法返回值: void 房间信息
    广播推送: FightEnterRoom 玩家自己进入房间
 
路由: 2 - 3  --- 【玩家退出房间】 --- 【FightRoomAction:120】【quitRoom】
    方法返回值: void 
    广播推送: LongValue 有玩家退出房间了。userId
 
路由: 2 - 4  --- 【玩家准备】 --- 【FightRoomAction:146】【ready】
    方法参数: BoolValue ready       true 表示准备,false 则是取消准备
    方法返回值: void 
    广播推送: PlayerReady 有玩家准备或取消准备了
 
路由: 2 - 5  --- 【房间列表】 --- 【FightRoomAction:222】【listRoom】
    方法返回值: ByteValueList<FightRoomNotice> 房间列表
 
路由: 2 - 6  --- 【玩家在游戏中的操作】 --- 【FightRoomAction:191】【operation】
    方法参数: FightOperationCommand command     玩家操作数据
    方法返回值: void 
 
路由: 2 - 7  --- 【开始游戏】 --- 【FightRoomAction:162】【startGame】
    方法返回值: void 
 

==================== 其它广播推送 ====================
路由: 2 - 51  --- 广播推送: FightRoomNotice (房间更新通知)
路由: 2 - 50  --- 广播推送: FightPlayer (有新玩家加入房间)
路由: 2 - 52  --- 广播推送: IntValue (对局开始,通知玩家开始选择。round 当前对局数)
路由: 2 - 53  --- 广播推送: LongValue (通知其他玩家,有玩家做了选择。userId)
路由: 2 - 56  --- 广播推送: none (解散房间)
路由: 2 - 54  --- 广播推送: ByteValueList<FightRoundPlayerScore> (广播玩家对局分数)
路由: 2 - 55  --- 广播推送: none (游戏结束)
==================== 错误码 ====================
 -1008 : 绑定的游戏逻辑服不存在 
 -1007 : 强制玩家下线 
 -1006 : 数据不存在 
 -1005 : class 不存在 
 -1004 : 请先登录 
 -1003 : 心跳超时相关 
 -1002 : 路由错误 
 -1001 : 参数验错误 
 -1000 : 系统其它错误 
 1 : 玩家在房间里 
 3 : 房间不存在 
 4 : 非法操作 
 6 : 开始游戏需要的最小人数不足 
 7 : 请等待其他玩家准备 
 8 : 房间空间不足,人数已满 

[light-game-room]

room 模块相关文档 - room 桌游、房间类 (yuque.com)

#278 桌游类、房间类游戏的扩展模块,简化与规范化房间管理相关的、开始游戏流程相关的、玩法操作相关的相关扩展

light-game-room 房间,是 ioGame 提供的一个轻量小部件 - 可按需选择的模块。

light-game-room + 领域事件 + 内置 Kit = 轻松搞定桌游类游戏

该模块是桌游类、房间类游戏的解决方案。比较适合桌游类、房间类的游戏基础搭建,基于该模型可以做一些如,炉石传说、三国杀、斗地主、麻将 ...等类似的桌游。或者说只要是房间类的游戏,该模型都适用。比如,CS、泡泡堂、飞行棋、坦克大战 ...等。

如果你计划做一些桌游类的游戏,那么推荐你基于该模块做扩展。该模块遵循面向对象的设计原则,没有强耦合,可扩展性强。且帮助开发者屏蔽了很多重复性的工作,并可为项目中的功能模块结构、开发流程等进行清晰的组织定义,减少了后续的项目维护成本。

主要解决的问题与职责

桌游、房间类的游戏在功能职责上可以分为 3 大类,分别是

  1. 房间管理相关的
    1. 管理着所有的房间、查询房间列表、房间的添加、房间的删除、房间与玩家之间的关联、房间查找(通过 roomId 查找、通过 userId 查找)。
  2. 开始游戏流程相关的
    1. 通常桌游、房间类的游戏都有一些固定的流程,如创建房间、玩家进入房间、玩家退出房间、解散房间、玩家准备、开始游戏 ...等。
    2. 开始游戏时,需要做开始前的验证,如房间内的玩家是否符足够 ...等,当一切符合业务时,才是真正的开始游戏。
  3. 玩法操作相关的
    1. 游戏开始后,由于不同游戏之间的具体操作是不相同的。如坦克的射击,炉石的战前选牌、出牌,麻将的吃、碰、杠、过、胡,回合制游戏的普攻、防御、技能 ...等。
    2. 由于玩法操作的不同,所以我们的玩法操作需要是可扩展的,并用于处理具体的玩法操作。同时这种扩展方式更符合单一职责,使得我们后续的扩展与维护成本更低。

以上功能职责(房间管理相关、流程相关、玩法操作相关)属于相对通用的功能。如果每款游戏都重复的做这些工作,除了枯燥之外,还将浪费巨大的人力成本。

而当前模块则能很好的帮助开发者屏蔽这些重复性的工作,并可为项目中的功能模块结构、开发流程等进行清晰的组织定义,减少了后续的项目维护成本。更重要的是有相关文档,将来当你的团队有新进成员时,可以快速的上手。


room 实战简介

room 桌游、房间类实战(yuque.com)

文档中,我们基于该 room 模块做一个实战示例,该示例整体比较简单,多名玩家在房间里猜拳(石头、剪刀、布)得分。实战示例包括了前后端,前端使用 FXGL 引擎,这样开发者在学习时,只需 JDK 环境就可以了,而不需要安装更多的环境。启动游戏后玩家会将加入大厅(类似地图),多名玩家相互可见,并且玩家可以在大厅内移动。


[示例集合整理]

将 SimpleExample(文档中所有功能点的示例)、SpringBootExample(综合示例)、ioGameWeb2Game(web 转游戏 - 示例理解篇)、fxglSimpleGame(移动同步 FXGL + netty)合并成一个示例项目。

github gitee
ioGame 示例集合 ioGame 示例集合

ioGame 21.7,java 高性能游戏服务器框架;netty 高性能游戏服务器框架

11 May 03:46
Compare
Choose a tag to compare

21.7

文档与日志

版本更新汇总

  • [core] #112 protobuf 协议类添加检测,通过 action 构建时的监听器实现
  • [core] #272 业务框架 - 提供 action 构建时的监听回调
  • [core] #274 优化、提速 - 预生成 jprotobuf 协议类的代理,通过 action 构建时的监听器实现
  • [broker] fix #277#280 偶现 BrokerClientType 为空
  • [external] #271 游戏对外服 - 内置与可选 handler - log 相关的打印(触发异常、断开连接时)
  • [room] 简化命名: AbstractPlayer --> Player、AbstractRoom --> Room
  • 其他优化:预先生成游戏对外服统一协议的代理类及内置的协议碎片 (yuque.com)相关代理类,优化 action 参数解析

[external]

#271 游戏对外服 - 内置与可选 handler - log 相关的打印(触发异常、断开连接时)

其他参考 内置与可选的 Handler (yuque.com)

[core]

#272 业务框架 - 提供 action 构建时的监听回调

开发者可以利用 ActionParserListener 接口来观察 action 构建过程,或者做一些额外的扩展。

扩展示例参考

// 简单打印
public final class YourActionParserListener implements ActionParserListener {
    @Override
    public void onActionCommand(ActionParserContext context) {
        ActionCommand actionCommand = context.getActionCommand();
        log.info(actionCommand);
    }
}

void test() {
    BarSkeletonBuilder builder = ...;
    builder.addActionParserListener(new YourActionParserListener());
}

#112 protobuf 协议类添加检测,通过 action 构建时的监听器实现

如果当前使用的编解码器为 ProtoDataCodec 时,当 action 的参数或返回值的类没有添加 ProtobufClass 注解时(通常是忘记添加),给予一些警告提示。

// 该协议类没有添加 ProtobufClass 注解
class Bird {
    public String name;
}

@ActionController(1)
public class MyAction {
    @ActionMethod(1)
    public Bird testObject() {
        return new Bird();
    }
}

警告打印参考

======== 注意,协议类没有添加 ProtobufClass 注解 ========
class com.iohao.game.action.skeleton.core.action.Bird

#274 优化、提速 - 预生成 jprotobuf 协议类的代理,通过 action 构建时的监听器实现

如果当前使用的编解码器为 ProtoDataCodec 时,会在启动时就预先生成好 jprotobuf 协议类对应的代理类(用于 .proto 相关的 编码、解码),而不必等到用时在创建该代理类。从而达到整体优化提速的效果。

在此之前,在没做其他设置的情况下,首次访问 action 时,如果参数使用的 jprotobuf 协议类,那么在解码该参数时,会通过 ProtobufProxy.create 来创建对应的代理类(类似 .proto 相关的 编码、解码)。之后再访问时,才会从缓存中取到对应的代理类。

该优化默认开启,开发者可以不需要使用与配置跟 jprotobuf-precompile-plugin 插件相关的了。

已经预先生成的代理类有

[room]
简化命名: AbstractPlayer --> Player、AbstractRoom --> Room

其他优化

优化 action 参数解析

ioGame 21.6,java 高性能游戏服务器框架;netty 高性能游戏服务器框架

23 Apr 07:41
Compare
Choose a tag to compare

本次更新

属性监听特性

#264 新增属性值变更监听特性

文档 : 属性监听 (yuque.com)

属性可添加监听器,当某些属性值的发生变化时,触发监听器。

使用场景举例

比如玩家的血量低于一定值时,需要触发无敌状态;此时,我们就可以监听玩家的血量,并在该属性上添加一个对应的监听器来观察血量的变化,当达到预期值时就触发对应的业务。

类似的使用场景还有很多,这里就不过多的举例了。属性监听的特点在于属性变化后会触发监听器。

属性监听特点

  • 可为属性添加监听器,用于观察属性值的变化。
  • 属性可以添加多个监听器。
  • 属性的监听器可以移除。

框架已经内置了几个属性实现类,分别是:

  • IntegerProperty
  • LongProperty
  • StringProperty
  • BooleanProperty
  • ObjectProperty

for example - 添加监听器

BooleanProperty

当 BooleanProperty 对象的值发生改变时,触发监听器。

var property = new BooleanProperty();
// 添加一个监听器。
property.addListener((observable, oldValue, newValue) -> {
   log.info("oldValue:{}, newValue:{}", oldValue, newValue);
});

property.get(); // value is false
property.set(true); // 值变更时,将会触发监听器
property.get(); // value is true

IntegerProperty

当 IntegerProperty 对象的值发生改变时,触发监听器。

var property = new IntegerProperty();
// add listener monitor property object
property.addListener((observable, oldValue, newValue) -> {
   log.info("oldValue:{}, newValue:{}", oldValue, newValue);
});

property.get(); // value is 0
property.set(22); // When the value changes,listeners are triggered
property.get(); // value is 22

property.increment(); // value is 23. will trigger listeners

for example - 移除监听器

下面这个示例,我们将 property 初始值设置为 10,随后添加了一个监听器;当监听器观察到新值为 9 时,就从 observable 中移除自己(这个自己指的是监听器本身),而 observable 则是 IntegerProperty。

@Test
public void remove1() {
    IntegerProperty property = new IntegerProperty(10);
    // 添加一个监听器
    property.addListener(new PropertyChangeListener<>() {
        @Override
        public void changed(PropertyValueObservable<? extends Number> observable, Number oldValue, Number newValue) {
            log.info("1 - newValue : {}", newValue);

            if (newValue.intValue() == 9) {
                // 移除当前监听器
                observable.removeListener(this);
            }
        }
    });

    property.decrement(); // value 是 9,并触发监听器
    property.decrement(); // value 是 8,由于监听器已经移除,所以不会触发任何事件。
}

下面的示例中,我们定义了一个监听器类 OnePropertyChangeListener 并实现了 PropertyChangeListener 监听器接口。示例中,我们通过 OnePropertyChangeListener 对象的引用来移除监听器。

@Test
public void remove2() {
    // 监听器
    OnePropertyChangeListener onePropertyChangeListener = new OnePropertyChangeListener();
    
    // 属性
    IntegerProperty property = new IntegerProperty();
    // 添加监听器
    property.addListener(onePropertyChangeListener);

    property.increment(); // value == 1,并触发监听器
    property.removeListener(onePropertyChangeListener); // 移除监听器
    property.increment(); // value == 2,由于监听器已经移除,所以不会触发任何事件。
}

// 自定义的监听器
class OnePropertyChangeListener implements PropertyChangeListener<Number> {
    @Override
    public void changed(PropertyValueObservable<? extends Number> observable, Number oldValue, Number newValue) {
        log.info("oldValue:{}, newValue:{}, observable:{}", oldValue, newValue, observable);
    }
}

属性监听 - 小结

属性监听在使用上是简单的,如果你的业务中有关于属性变化后需要触发某些事件的,可以考虑引用该特性。框架为 int、long、boolean、Object、String 等基础类型提供了对应的属性监听。

属性监听特性支持添加多个监听器,支持移除监听器。

模拟客户端相关

模拟客户端新增与服务器断开连接的方法。

模拟客户端新增是否活跃的状态属性。

ClientUser clientUser = ...;
// 是否活跃,true 表示玩家活跃
clientUser.isActive();
// 关闭模拟客户端连接
clientUser.getClientUserChannel().closeChannel();

获取游戏对外服的数据与扩展相关

文档 获取游戏对外服的数据与扩展 (yuque.com)

RequestCollectExternalMessage 增加 userId 字段。

#265 模拟玩家请求时 - 从游戏对外服中获取在线玩家相关数据

新增 UserHeadMetadataExternalBizRegion,从用户(玩家)所在游戏对外服中获取用户自身的数据,如用户所绑定的游戏逻辑服、元信息 ...等

使用参考

@Slf4j
@RestController
@RequestMapping("other")
public class OtherController {
    static final AtomicLong msgId = GameManagerController.msgId;
    /** 为了方便测试,这里指定一个 userId 来模拟玩家 */
    static final long userId = GameManagerController.userId;

    @GetMapping("/notice")
    public String notice() {
        log.info("other notice");
        // 使用协议碎片特性 https://www.yuque.com/iohao/game/ieimzn
        StringValue data = StringValue.of("other GM web msg " + msgId.incrementAndGet());
        // 模拟请求 : 路由 - 业务数据
        RequestMessage requestMessage = BarMessageKit.createRequestMessage(ExchangeCmd.of(ExchangeCmd.notice), data);

        // 设置需要模拟的玩家
        HeadMetadata headMetadata = requestMessage.getHeadMetadata();
        headMetadata.setUserId(userId);

        // 从游戏对外服中获取一些用户(玩家的)自身的数据,如元信息、所绑定的游戏逻辑服 ...等
        Optional<HeadMetadata> headMetadataOptional = ExternalCommunicationKit.employHeadMetadata(requestMessage);

        if (headMetadataOptional.isPresent()) {
            // 发起模拟请求
            extractedRequestLogic(requestMessage);

            // 打印从游戏对外服获取的元信息
            byte[] attachmentData = headMetadata.getAttachmentData();
            ExchangeAttachment attachment = DataCodecKit.decode(attachmentData, ExchangeAttachment.class);
            return "other notice 玩家的元信息: %s - %s".formatted(attachment, msgId.get());
        } else {
            return "other notice 玩家 %s 不在线,无法获取玩家的元信息 - %s".formatted(userId, msgId.get());
        }
    }

    private void extractedRequestLogic(RequestMessage requestMessage) {
        // 向逻辑服发送请求,该模拟请求具备了玩家的元信息。
        BrokerClient brokerClient = MyKit.brokerClient;
        InvokeModuleContext invokeModuleContext = brokerClient.getInvokeModuleContext();
        invokeModuleContext.invokeModuleVoidMessage(requestMessage);
    }
}

任务工具相关

TaskListener 接口增加异常回调方法 void onException(Throwable e),用于接收异常信息;当 triggerUpdate 或 onUpdate 方法抛出异常时,将会传递到该回调方法中。

@Test
public void testException() throws InterruptedException {
    AtomicBoolean hasEx = new AtomicBoolean(false);
    TaskKit.runOnce(new OnceTaskListener() {
        @Override
        public void onUpdate() {
            // 模拟一个业务异常
            throw new RuntimeException("hello exception");
        }

        @Override
        public void onException(Throwable e) {
            hasEx.set(true);
            // 触发异常后,将来到这里
            log.error(e.getMessage(), e);
        }
    }, 10, TimeUnit.MILLISECONDS);

    TimeUnit.MILLISECONDS.sleep(200);
    Assert.assertTrue(hasEx.get()); // true
}

业务框架相关 - [common-core]

#266 新增 RangeBroadcast 范围内的广播功能,这个范围指的是,可指定某些用户进行广播。

在执行广播前,开发者可以自定义业务逻辑,如

  • 添加一些需要广播的用户
  • 删除一些不需要接收广播的用户
  • 可通过重写 logic、trick 方法来做一些额外扩展

for example
在某些业务场景下,能让语义更清晰

// example - 1
new RangeBroadcast(flowContext)
        // 需要广播的数据
        .setResponseMessage(responseMessage)
        // 添加需要接收广播的用户
        .addUserId(1)
        .addUserId(2)
        .addUserId(List.of(3L, 4L, 5L))
        // 排除一些用户,被排除的用户将不会接收到广播
        .removeUserId(1)
        // 执行广播
        .execute();

// example - 2
new RangeBroadcast(flowContext)
        // 需要广播的数据
        .setResponseMessage(cmdInfo, playerReady)
        // 添加需要接收广播的用户
        .addUserId(1)
        // 执行广播
        .execute();

[light-game-room] 房间模块

移除 AbstractRoom broadcast 系列方法,开发者可使用 RoomBroadcastFlowContext 接口实现旧的兼容。

移除 AbstractRoom createSend 方法,开发者可使用 ofRangeBroadcast 系列来代替。AbstractRoom 新增 RoomBroadcastEnhance,实现房间内的广播增强,该系列在语义上更清晰。

这里使用一个准备就绪的示例来演示,当有玩家准备时,需要通知房间内的其他玩家

final RoomService roomService = ...;

@ActionMethod(RoomCmd.ready)
public void ready(boolean ready, FlowContext flowContext) {
    long userId = flowContext.getUserId();
    // 得到玩家所在的房间
    AbstractRoom room = this.roomService.getRoomByUserId(userId);
    
    // 准备
    PlayerReady playerReady = new PlayerReady();
    playerReady.userId = userId;
    playerReady.ready = ready;
  
    // 通知房间内的所有玩家,有玩家准备或取消准备了
    room.ofRangeBroadcast(flowContext)
            // 响应数据(路由、业务数据)
            .setResponseMessage(flowContext.getCmdInfo(), playerReady)
            .execute();
}

// 准备或取消准备
@ProtobufClass
@FieldDefaults(level = AccessLevel.PUBLIC)
public class PlayerReady {
    /** 当前操作的玩家 userId */
    long userId;
    /** true 表示准备 */
    boolean ready;
}

AbstractRoom 增加 ifPlayerExist、ifPlayerNotExist 方法。

ifPlayerExist 方法

如果玩家在房间内,就执行给定的操作,否则不执行任何操作。

这里演示玩家加入房间的业务

RoomService roomService = ...;
AbstractRoom room = ...;
// 如果玩家不在房间内,就创建一个玩家,并让玩家加入房间
room.ifPlayerNotExist(userId, () -> {
    // 玩家加入房间
    FightPlayerEntity newPlayer = new FightPlayerEntity();
    newPlayer.setId(userId);
    
    this.roomService.addPlayer(room, newPlayer);
});

ifPlayerNotExist 方法

如果玩家不在房间内,就执行给定的操作,否则不执行任何操作。

这里结合 RangeBroadcast,演示当有新玩家加入房间时,通知房间内的其他玩家。

AbstractRoom room = ...;
// 有新玩家加入房间,通知其他玩家
room.ifPlayerExist(userId, (FightPlayerEntity playerEntity) -> {
    FightPlayer fightPlayer = FightMapstruct.ME.convert(playerEntity);
    room.ofRangeBroadcast(flowContext)
            .setResponseMessage(RoomCmd.of(RoomCmd.playerEnterRoomBroadcast), fightPlayer)
            // 排除不需要通知的玩家(当前 userId 是自己)
            .removeUserId(userId)
            .execute();
});

java 游戏服务器框架 ioGame 21.5, netty 游戏服务器框架

16 Apr 02:30
Compare
Choose a tag to compare

主要更新

  1. #255 关于 Proto 生成排除属性问题
/**
 * 动物
 */
@ProtobufClass
@FieldDefaults(level = AccessLevel.PUBLIC)
public class Animal {
    /** id */
    int id;
    /** 动物类型 - 枚举测试 */
    AnimalType animalType;
  	/** 年龄 - 忽略的属性*/
    @Ignore
    String age;
}

生成后的 .proto

// 动物
message Animal {
  // id
  int32 id = 1;
  // 动物类型 - 枚举测试
  AnimalType animalType = 2;
}
  1. 增强 ClassScanner 类

  2. 优化模拟客户端

  3. #258 文档生成,兼容 gradle 编译路径

  4. enhance jprotobuf,临时解决打包后不能在 linux java21 环境运行的问题,see java21,springBoot3.2 打 jar 后使用异常 · Issue #211 · jhunters/jprotobuf (github.com)

  5. 生成 .proto 时,在最后打印文件路径

netty 游戏服务器框架 ioGame 21.4, java 游戏服务器框架

28 Mar 02:43
Compare
Choose a tag to compare

#253

CreateRoomInfo.createUserId int --> long

文档生成时,默认指定 StandardCharsets.UTF_8

javaProjectBuilder.setEncoding(StandardCharsets.UTF_8.name());

玩家下线时,使用自身所关联的线程处理。

SocketUserSessions removeUserSession

ExecutorRegion

1 优化默认创建策略

2 优化 ExecutorRegionKit,SimpleThreadExecutorRegion 默认使用全局单例,减少对象的创建。

java 游戏服务器框架 netty ioGame 21.3

11 Mar 01:17
Compare
Choose a tag to compare

#250

游戏对外服 - 自定义编解码 - WebSocketMicroBootstrapFlow

文档 游戏对外服-自定义编解码 (yuque.com)

重写 WebSocketMicroBootstrapFlow createExternalCodec 方法,用于创建开发者自定义的编解码,其他配置则使用 pipelineCodec 中的默认配置。

DefaultExternalServerBuilder builder = ...;

builder.setting().setMicroBootstrapFlow(new WebSocketMicroBootstrapFlow() {
    @Override
    protected MessageToMessageCodec<BinaryWebSocketFrame, BarMessage> createExternalCodec() {
        // 开发者自定义的编解码实现类。
        return new YourWsExternalCodec();
    }
});

以下展示的是 WebSocketMicroBootstrapFlow pipelineCodec 相关代码

public class WebSocketMicroBootstrapFlow extends SocketMicroBootstrapFlow {
    ... 省略部分代码
    @Override
    public void pipelineCodec(PipelineContext context) {
        // 添加 http 相关 handler
        this.httpHandler(context);

        // 建立连接前的验证 handler
        this.verifyHandler(context);

        // 添加 websocket 相关 handler
        this.websocketHandler(context);

        // websocket 编解码
        var externalCodec = this.createExternalCodec();
        context.addLast("codec", externalCodec);
    }

    @Override
    protected MessageToMessageCodec<BinaryWebSocketFrame, BarMessage> createExternalCodec() {
        // createExternalCodec 相当于一个钩子方法。
        return new WebSocketExternalCodec();
    }
};

#249
将集群启动顺序放到 Broker(游戏网关)之后。

集群增减和逻辑服 Connect 增减使用同一线程处理。

IoGameGlobalConfig brokerClusterLog 集群相关日志不开启。


ioGame 简单介绍

你是否想要开发一个高性能、稳定、易用、自带负载均衡、避免类爆炸设计、可跨进程跨机器通信、集群无中心节点、集群自动化、有状态多进程的分布式的网络编程服务器呢?如果是的话,这里向你推荐一个由 java 语言编写的网络编程框架 ioGame。下面将会从多个方面来对框架做一些简单的介绍。

ioGame 是一个轻量级的网络编程框架,适用于网络游戏服务器、物联网、内部系统及各种需要长连接的场景;

ioGame 有以下特点:

  • 无锁异步化、事件驱动的架构设计
  • 同时支持 TCP、WebSocket、UDP 多种连接方式,并且可扩展
  • 支持 protobuf、json 等不同的通信协议
  • 集群无中心节点、集群自动化、分布式的设计
  • 真轻量级,不依赖任何第三方中间件或数据库就能支持集群、分布式
  • 提供多种通讯方式,且逻辑服之间可以相互跨机器通信
  • 框架为开发者提供了同步、异步、异步回调的方法,用于逻辑服之间的相互访问
  • ioGame 是纯 javaSE 的,使得 ioGame 能与其他框架方便的进行集成、融合;如 spring ...等
  • 学习成本低,开发体验好
  • 支持多服单进程、多服多进程的启动和部署方式
  • 提供游戏文档生成的辅助功能
  • 包体小、启动快、内存占用少
  • 提供优雅的路由访问权限控制
  • 提供了灵活的线程扩展、设置
  • 具备智能的同进程亲和性
  • 具备全链路调用日志跟踪特性
  • 业务框架提供了插件机制,插件是可插拨、可扩展的
  • JSR380验证、断言 + 异常机制 = 更少的维护成本
  • action 支持自动装箱、拆箱基础类型,用于解决协议碎片的问题

ioGame 是一个专为网络编程设计的轻量级框架,它可以帮助你快速地搭建和运行自己的网络服务器。ioGame 适用于网络游戏服务器、物联网、内部系统及各种需要长连接的场景。如各种类型和规模的网络游戏,无论是 H5、手游还是 PC 游戏,无论是简单的聊天室,还是复杂的全球同服、回合制游戏、策略游戏、放置休闲游戏、即时战斗、MMORPG 等,ioGame 都可以满足你的需求。

ioGame 在打包、内存占用、启动速度等方面也是优秀的。打 jar 包后大约 15MB,应用通常会在 0.x 秒内完成启动,内存占用小。详细请看 快速从零编写服务器完整示例

在生态融合方面,ioGame 可以很方便的与 spring 集成(5 行代码);除了 spring 外,还能与任何其他的框架做融合,如:solon ... 等,从而使用其他框架的相关生态。

ioGame 在架构上解决了传统框架所产生的 N*N 问题与传统架构对比)。传统架构在扩展机器时,需要借助很多第三方中间件,如:Redis、MQ、ZooKeeper ...等,才能满足整体架构的运作。通常,只要引入了需要安装的中间件才能做到扩展的,那么你的架构或者说框架,基本上与轻量级无缘了。

在轻量级方面,ioGame 不依赖任何第三方中间件或数据库就能支持集群、分布式,只需要 java 环境就可以运行。这意味着在使用上简单了,在部署上也为企业减少了部署成本、维护难度。使用 ioGame 时,只需一个依赖即可获得整个框架,而无需安装其他服务,如: Nginx、Redis、MQ、Mysql、ZooKeeper、Protobuf 协议编译工具 ...等。

ioGame 具备全链路调用日志跟踪特性,这在分布式下非常的实用。该特性为每个请求分配一个唯一标识,并记录在日志中,通过唯一标识可以快速的在日志中过滤出指定请求的信息。ioGame 提供的全链路调用日志跟踪特性更是强大,支持跨机器、跨进程。简单的说,从玩家的请求进来到结束,无论该请求经过了多少个游戏逻辑服,都能精准记录。

在通讯方式方面,大部分框架只能支持推送(广播)这一类型的通讯方式;ioGame 则提供了多种类型的通讯方式,通过对各种通讯方式的组合使用,可以简单完成以往难以完成的工作,并且这些通讯方式都支持跨进程、跨机器通信,且具备全链路调用日志跟踪。这些通讯方式分别是

  1. 请求响应(单次请求处理)
  2. 广播(推送)
  3. 单个逻辑服间的相互通讯(可跨机器通信、可跨进程通信)
  4. 与同类型多个逻辑服相互通讯(可跨多个机器通信、可跨多个进程通信)
  5. 脉冲通讯(可跨多个机器通信、可跨多个进程通信)
  6. 分布式事件总线(类似 MQ、Redis 发布订阅机制;可跨多个机器通信、可跨多个进程通信)

在编码风格上,ioGame 为开发者提供了类 MVC 的编码风格(无入侵的 Java Bean ),这种设计方式很好的避免了类爆炸。同时,框架为开发者提供了同步、异步、异步回调的方法,用于逻辑服之间的相互访问;这使得开发者所编写的代码会非常的优雅,并且具备全链路调用日志跟踪。

从 ioGame21 开始,框架添加了虚拟线程的相关支持。各逻辑服之间通信阻塞部分使用虚拟线程,这样可以很好的避免阻塞业务线程,并大幅提高了框架的吞吐量。

在线程安全方面,框架为开发者解决了单个玩家的并发问题;即使玩家重新登录后,也会使用相同的线程来消费业务,并推荐使用领域事件来解决同一房间或业务内多个玩家的并发问题。框架在线程的扩展性上提供了友好的支持,开发者可以很容易的编写出无锁并发代码,这得益于 ioGame 独有的线程执行器设计与扩展。换句话说,你不会因为并发问题烦恼。

在无锁并发方面,ioGame 提供了优雅、独特的线程执行器设计。通过该特性,开发者能轻易的编写出无锁高并发的代码。

在连接方式方面,ioGame 允许开发者使用一套业务代码同时支持多种连接方式,无需进行任何修改。ioGame 已经支持了 TCP、WebSocket 和 UDP 连接方式,并且也支持在这几种连接方式之间进行灵活切换。连接方式是可扩展的,并且扩展操作也很简单,这意味着之后如果支持了 KCP,无论你当前项目使用的是 TCP、WebSocket 还是 UDP,都可以切换成 KCP;注意了,即使切换到 KCP 的连接方式,现有的业务代码也无需改变。

在通信协议方面,ioGame 让开发者用一套业务代码,就能轻松切换和扩展不同的通信协议,如 Protobuf、JSON 等。只需一行代码,就可以从 Protobuf 切换到 JSON,无需改变业务方法。

在增减协议方面,ioGame 可以让你在新增或减少协议时,无需重启游戏对外服与 Broker(游戏网关);这样既能避免玩家断线,又能避免因新增、减少协议而重启所有机器的痛点。

在协议碎片方面,action 支持自动装箱、拆箱基础类型特性,用于解决协议碎片的问题。同时该特性除了能使你的业务代码更加清晰以外,还能大幅提高开发者在该环节的生产力。

在集群方面,ioGame 的 Broker (游戏网关)采用无中心节点、自动化的集群设计,所有节点平等且自治,不存在单点故障。集群能够自动管理和弹性扩缩,节点加入或退出时,能够自动保证负载均衡和数据一致性,不影响服务可用性。

在分布式方面,ioGame 的逻辑服使用了分布式设计思想,将服务器分为游戏对外服游戏逻辑服等不同层次,并且每一层都有明确的职责和接口。这样可以提高代码可读性和可维护性,并且方便进行水平扩展

在学习成本方面,ioGame 的学习成本非常低,可以说是零学习成本,即使没有游戏编程经验,也能轻松上手。开发者只需掌握普通的 java 方法或 webMVC 相关知识,就能用框架开发业务。框架不要求开发者改变编码习惯,而是自身适应开发者的需求。

同进程亲和性方面,在同一进程内,不同 Netty 实例之间的通信,是通过内存进行传输的,不需要经过网络传输,数据传输速度极快。同进程亲和性指的是,优先访问同进程内的游戏逻辑服,当同进程内没有能处理请求的游戏逻辑服时,才会去其他进程或机器中查找能处理请求的游戏逻辑服;简单点说,框架对于请求的处理很智能,会优先将请求给同进程内的逻辑服消费。

在开发体验方面,ioGame 非常注重开发者的开发体验;框架提供了 JSR380 验证断言 + 异常机制业务代码定位,action 支持自动装箱、拆箱基础类型,用于解决协议碎片的问题 ...等。诸多丰富的功能,使得开发者的业务代码更加的清晰、简洁;

业务框架提供了插件机制,插件是可插拨、可扩展的。框架内置提供了 DebugInOutaction 调用统计业务线程监控插件各时间段调用统计插件...等插件;不同的插件提供了不同的关注点,比如我们可以使用调用、监控等插件相互配合,可以让我们在开发阶段就知道是否存在性能问题。合理利用好各个插件,可以让我们在开发阶段就能知道问题所在,提前发现问题,提前预防问题。

在分布式开发体验方面,通常在开发分布式应用时是需要启动多个进程的。这会让调试与排查问题变得非常困难,从而降低开发者的效率、增加工作量等,这也是很多框架都解决不了的问题,但 ioGame 做到了!ioGame 支持多服单进程的启动方式,这使得开发者在开发和调试分布式系统时更加简单。

与前端对接联调方面,ioGame 提供了游戏文档生成的辅助功能,可以做到代码即对接文档。简单地说,当业务代码编写完后,框架会自动生成最新的文档。如果没有游戏文档的生成,那么你将要抽出一些时间来编写、维护对接文档的工作,而且当团队人数多了之后,文档就会很乱、不同步、不是最新的、忘记更新等情况就会出现。

在部署方面,ioGame 支持多服单进程的方式部署,也支持多服多进程多机器的方式部署;在部署方式上可以随意的切换而不需要更改代码。日常中我们可以按照单体思维开发,到了生产可以选择使用多进程的方式部署。

在安全方面,所有的游戏逻辑服不需要开放端口,天然地避免了扫描攻击。由于不需要为每个逻辑服分配独立的端口,那么我们在使用诸如云服务器之类的服务时,就不需要担心端口开放权限的问题了。别小看这一个环节,通常这些小细节最浪费开发者的时间。由于我们不需要管理这些 IP:Port,这部分的工作量就自然地消失了

在模拟客户端测试方面,ioGame 提供了压测&模拟客户端请求模块。此模块是用于模拟客户端,简化模拟工作量,只需要编写对应请求与回调。除了可以模拟简单的请求外,通常还可以做一些复杂的请求编排,并支持复杂业务的压测。与单元测试不同的是,该模块可以模拟真实的网络环境,并且在模拟测试的过程中与服务器的交互是可持续的、可互动的,同时也是支持自动化的

架构灵活性方面,ioGame 的架构由三部分组成:1.游戏对外服、2.Broker(游戏网关)、3.游戏逻辑服;三者既可相互独立,又可相互融合。这意味着使用 ioGame 可以适应任何类型的游戏,因为只需通过调整部署方式,就可以满足不同类型的游戏需求。在 ioGame 中进行这些调整工作非常简单,而且不会对现有代码产生不良影响。

开发者基于 ioGame 编写的项目模块,通常是条理清晰的,得益于框架对路由的合理设计,同时也为路由提供了优雅的访问权限控制。当我们整理好这些模块后,对于其他开发者接管项目或后续的维护中,会是一个不错的帮助(模块的整理与建议)。或许现阶段你感受不到这块的威力,随着你深入地使用实践就能体会到这么设计的诸多好处与优势。

开发者基于 ioGame 编写的项目,通常是语法简洁的、高性能的、低延迟的。框架最低要求使用 JDK21,这样即可以让项目享受到分代 ZGC 带来的改进,还能享受语法上的简洁。分代 ZGC 远低于其亚毫秒级暂停时间的目标,可以在不影响游戏速度的情况下,清理掉多余的内存;这样就不会出现卡顿或者崩溃的问题了,相当于在项目中变相的引入了一位 JVM 调优大师。

综上所述,ioGame 是一个非常适合网络游戏开发的框架。可以让你轻松地创建高性能、低延迟、易扩展的游戏服务器,并且节省时间和资源。如果你想要快速地开发出令人惊艳的网络游戏,请不要犹豫,立即选择 ioGame 吧!框架屏蔽了很多复杂且重复性的工作,并可为项目中的功能模块结构、开发流程等进行清晰的组织定义,减少了后续的项目维护成本。

框架在开发、部署、压测&模拟测试 ...等,各个阶段都提供了很好的支持。相信你已经对 ioGame 有了一个初步的了解,虽然还有很多丰富的功能与特性没有介绍到,但你可以通过后续的实践过程中来深入了解。感谢你的阅读,并期待你使用 ioGame 来打造自己的游戏服务器。

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

21 Feb 03:17
Compare
Choose a tag to compare

框架版本更新日志 (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,不依赖其他服务,耦合性低。(不需要安装任何中间件)
  • 事件源和事件监听...
Read more

java 游戏服务器框架 ioGame 17.1.61 action 各时间段调用统计插件

03 Jan 05:47
Compare
Choose a tag to compare

主要更新

#223 一天内 action 各小时的调用统计插件

打印预览

以下打印的是一天内(24小时)的业务消费量,计划以每小时为一个时间阶段,小时内可划分钟阶段。

2023-11-29 action 调用次数  [100] 
	0:00  8 ; - [15~30分钟 3 ] - [30~45分钟 2 ] - [45~59分钟 3 ]
	1:00  9 ; - [0~15分钟 1 ] - [15~30分钟 4 ] - [30~45分钟 1 ] - [45~59分钟 3 ]
	2:00  4 ; - [0~15分钟 1 ] - [15~30分钟 2 ] - [45~59分钟 1 ]
	3:00  2 ; - [0~15分钟 1 ] - [15~30分钟 1 ]
	4:00  1 ; - [0~15分钟 1 ]
	5:00  4 ; - [0~15分钟 1 ] - [15~30分钟 1 ] - [30~45分钟 1 ] - [45~59分钟 1 ]
	6:00  5 ; - [0~15分钟 1 ] - [15~30分钟 1 ] - [30~45分钟 1 ] - [45~59分钟 2 ]
	7:00  4 ; - [15~30分钟 2 ] - [30~45分钟 1 ] - [45~59分钟 1 ]
	8:00  4 ; - [0~15分钟 1 ] - [30~45分钟 3 ]
	9:00  4 ; - [15~30分钟 2 ] - [30~45分钟 2 ]
	10:00  5 ; - [15~30分钟 2 ] - [30~45分钟 1 ] - [45~59分钟 2 ]
	11:00  3 ; - [15~30分钟 2 ] - [45~59分钟 1 ]
	12:00  4 ; - [0~15分钟 2 ] - [30~45分钟 2 ]
	13:00  1 ; - [30~45分钟 1 ]
	14:00  5 ; - [0~15分钟 1 ] - [45~59分钟 4 ]
	15:00  6 ; - [0~15分钟 1 ] - [15~30分钟 2 ] - [45~59分钟 3 ]
	16:00  4 ; - [0~15分钟 1 ] - [15~30分钟 1 ] - [30~45分钟 1 ] - [45~59分钟 1 ]
	17:00  7 ; - [0~15分钟 1 ] - [15~30分钟 3 ] - [30~45分钟 3 ]
	18:00  2 ; - [0~15分钟 1 ] - [15~30分钟 1 ]
	19:00  7 ; - [0~15分钟 1 ] - [15~30分钟 3 ] - [30~45分钟 3 ]
	20:00  5 ; - [15~30分钟 3 ] - [30~45分钟 2 ]
	21:00  3 ; - [15~30分钟 2 ] - [30~45分钟 1 ]
	22:00  1 ; - [45~59分钟 1 ]
	23:00  2 ; - [15~30分钟 1 ] - [45~59分钟 1 ]

使用示例

BarSkeletonBuilder builder = ...;
// 各时间段 action 调用统计插件,将插件添加到业务框架中
var timeRangeInOut = new TimeRangeInOut();
builder.addInOut(timeRangeInOut);

默认的打印预览

默认配置下的打印如下,没有分钟阶段的调用次数统计,只有每小时的阶段的调用次数统计。

2023-11-29 action 调用次数  [10000] 
	0:00  431 ;
	1:00  416 ;
	2:00  421 ;
	3:00  414 ;
	4:00  441 ;
	5:00  423 ;
	6:00  407 ;
	7:00  395 ;
	8:00  410 ;
	9:00  413 ;
	10:00  378 ;
	11:00  411 ;
	12:00  380 ;
	13:00  413 ;
	14:00  417 ;
	15:00  416 ;
	16:00  400 ;
	17:00  430 ;
	18:00  471 ;
	19:00  440 ;
	20:00  405 ;
	21:00  430 ;
	22:00  414 ;
	23:00  424 ;

其他

fix endPoint removeBinding.

netty 集群网络游戏服务器框架 ioGame 17.1.60 action 业务线程监控插件

10 Dec 06:03
Compare
Choose a tag to compare

主要更新

#227 增加调度、定时器相关便捷工具,使用 HashedWheelTimer 来模拟 ScheduledExecutorService 调度

public void test() {
    // 每分钟调用一次
    TaskKit.runIntervalMinute(() -> log.info("tick 1 Minute"), 1);
    // 每 2 分钟调用一次
    TaskKit.runIntervalMinute(() -> log.info("tick 2 Minute"), 2);

    // 每 2 秒调用一次
    TaskKit.runInterval(() -> log.info("tick 2 Seconds"), 2, TimeUnit.SECONDS);
    // 每 30 分钟调用一次
    TaskKit.runInterval(() -> log.info("tick 30 Minute"), 30, TimeUnit.MINUTES);

    //【示例 - 移除任务】每秒调用一次,当 hp 为 0 时就移除当前 TimerListener
    TaskKit.runInterval(new IntervalTaskListener() {
        int hp = 2;

        @Override
        public void onUpdate() {
            hp--;
            log.info("剩余 hp:2-{}", hp);
        }

        @Override
        public boolean isActive() {
            // 当返回 false 则表示不活跃,会从监听列表中移除当前 TimerListener
            return hp != 0;
        }
    }, 1, TimeUnit.SECONDS);

    //【示例 - 跳过执行】每秒调用一次,当 triggerUpdate 返回值为 true,即符合条件时才执行 onUpdate 方法
    TaskKit.runInterval(new IntervalTaskListener() {
        int hp;

        @Override
        public void onUpdate() {
            log.info("current hp:{}", hp);
        }

        @Override
        public boolean triggerUpdate() {
            hp++;
            // 当返回值为 true 时,会执行 onUpdate 方法
            return hp % 2 == 0;
        }
    }, 1, TimeUnit.SECONDS);

    //【示例 - 指定线程执行器】每秒调用一次
    // 如果有耗时的任务,比如涉及一些 io 操作的,建议指定执行器来执行当前回调(onUpdate 方法),以避免阻塞其他任务。
    ExecutorService executorService = TaskKit.getCacheExecutor();

    TaskKit.runInterval(new IntervalTaskListener() {
        @Override
        public void onUpdate() {
            log.info("执行耗时的 IO 任务,开始");

            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            log.info("执行耗时的 IO 任务,结束");
        }

        @Override
        public Executor getExecutor() {
            // 指定执行器来执行当前回调(onUpdate 方法),以避免阻塞其他任务。
            return executorService;
        }
    }, 1, TimeUnit.SECONDS);

    TimeUnit.SECONDS.sleep(20);
}

#225 新增插件 - 业务线程监控

详细文档 https://www.yuque.com/iohao/game/zoqabk4gez3bckis

BarSkeletonBuilder builder = ...;
// 业务线程监控插件,将插件添加到业务框架中
var threadMonitorInOut = new ThreadMonitorInOut();
builder.addInOut(threadMonitorInOut);

打印预览

业务线程[RequestMessage-8-1] 共执行了 1 次业务,平均耗时 1 ms, 剩余 91 个任务未执行
业务线程[RequestMessage-8-2] 共执行了 1 次业务,平均耗时 1 ms, 剩余 0 个任务未执行
业务线程[RequestMessage-8-3] 共执行了 1 次业务,平均耗时 1 ms, 剩余 36 个任务未执行
业务线程[RequestMessage-8-4] 共执行了 1 次业务,平均耗时 1 ms, 剩余 0 个任务未执行
业务线程[RequestMessage-8-5] 共执行了 1 次业务,平均耗时 1 ms, 剩余 88 个任务未执行
业务线程[RequestMessage-8-6] 共执行了 1 次业务,平均耗时 1 ms, 剩余 0 个任务未执行
业务线程[RequestMessage-8-7] 共执行了 7 次业务,平均耗时 1 ms, 剩余 56 个任务未执行
业务线程[RequestMessage-8-8] 共执行了 1 次业务,平均耗时 1 ms, 剩余 0 个任务未执行

其他更新

废废弃 InternalKit,使用 TaskKit 代替

HeadMetadata 新增 cloneAll 方法

增加 ThreadExecutorRegion 线程执行器管理域接口;

  • 新增 UserThreadExecutorRegion 实现,执行器具体数量是不大于 Runtime.getRuntime().availableProcessors() 的 2 次幂。
  • 新增 SimpleThreadExecutorRegion 实现,执行器的数量与 Runtime.getRuntime().availableProcessors() 相同。

默认为 UserThreadExecutorRegion 和 SimpleThreadExecutorRegion 提供了单例实现,即使在同一进程中启动了多个逻辑服,也不会创建过多线程执行器,而是使用同一个。

netty 分布式网络游戏服务器框架 ioGame 17.1.59 action 调用统计插件

20 Nov 02:57
Compare
Choose a tag to compare

TImeKit 新加时间更新策略,开发者可设置时间更新策略。


RandomKit 功能增强,随机得到数组中的一个元素。


移除 SocketUserSessionHandler exceptionCaught 的日志打印。


[#221] 新增 action 调用统计插件 StatisticActionInOut

action 调用统计插件 StatisticActionInOut - 文档

StatActionInOut 是 action 调用统计插件,可以用来统计各 action 调用时的相关数据,如 action 的执行次数、总耗时、平均耗时、最大耗时、触发异常次数...等相关数据

开发者可以通过这些数据来分析出项目中的热点方法、耗时方法,从而做到精准优化


action 调用统计插件的打印预览

StatAction{cmd[1 - 1], 执行[50], 异常[0], 平均耗时[1833], 最大耗时[2945], 总耗时[91691] 
	500 ~ 1000 ms 的请求共 [7] 
	1000 ~ 1500 ms 的请求共 [11] 
	1500 ~ 2000 ms 的请求共 [9] 
	> 2000 ms 的请求共 [23] 

StatAction{cmd[1 - 2], 执行[50], 异常[0], 平均耗时[1782], 最大耗时[2976], 总耗时[89133] 
	500 ~ 1000 ms 的请求共 [10] 
	1000 ~ 1500 ms 的请求共 [7] 
	1500 ~ 2000 ms 的请求共 [12] 
	> 2000 ms 的请求共 [21] 

action 调用统计插件的使用

BarSkeletonBuilder builder = ...;
// action 调用统计插件,将插件添加到业务框架中
var statActionInOut = new StatActionInOut();
builder.addInOut(statActionInOut);

// 设置 StatAction 统计记录更新后的监听处理
// 这个监听不是必需的,这里只是简单的演示一下有变动时就做个打印。与 debugInOut 类似。
statActionInOut.setListener((statAction, time, flowContext) -> {
    // 简单打印统计记录值 StatAction
    System.out.println(statAction);
});

FlowContext 增强

增加 inOutStartTime 和 getInOutTime 方法,用于记录插件的开始执行时间和结束时所消耗的时间。当你扩展了多个插件时,插件中又有时间记录方面需求的,可以使用下面的方式。

// 使用示例
public final class YourInOut implements ActionMethodInOut {
   
    @Override
    public void fuckIn(FlowContext flowContext) {
        // 记录当前时间(重复调用只记录首次时间)
        flowContext.inOutStartTime();
    }

    @Override
    public void fuckOut(FlowContext flowContext) {
        // 消耗时间 = System.currentTimeMillis - inOutStartTime
        long time = flowContext.getInOutTime();
    }
}

其他更新

<netty.version>4.1.101.Final</netty.version>