Skip to content

Releases: iohao/ioGame

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>

netty 网络游戏服务器框架 ioGame 17.1.58 模拟客户端请求新 api

02 Nov 03:39
Compare
Choose a tag to compare

优化 FlowContext createRequestMessage

#194

可能在 springboot 集成 light-domain-event 时,启动报 java.lang.ClassNotFoundException

#198

关于改造现有或老客户端项目到 ioGame 遇到的问题

HeadMetadata 增加 customData 属性;用于自定义数据,专为开发者预留的一个字段,开发者可以利用该字段来传递自定义数据。该字段由开发者自己定义,框架不会对数据做任何处理,也不会做任何检查,开发者可以利用该字段来传递任何数据,包括自定义对象。


模拟客户端

因发展需要,模拟客户端使用新 api ,与 SDK 风格做统一。从 17.1.58 版本开始,推荐开发者使用新版本的 api。已经将老的 api 做了过期标记,将在 ioGame21 版本中完全移除这些过期的 api。整体变化不大,内容如下

请求 api

变更说明

新版本反序列化消息放在 result 时解析。

setDescription 使用 setTitle 代替。

setInputRequestData 使用 setRequestData 代替。

对于 List 类型的使用更友好

请求 - list 响应

########## 请求 - 旧版本 ##########
// 创建一个模拟命令 - 【125-3】读取某个玩家的私有消息
ofCommandUserId(ChatCmd.readPrivateMessage).callback(ByteValueList.class, result -> {
    List<ChatMessage> list = result.toList(ChatMessage.class);
    if (CollKit.isEmpty(list)) {
        return;
    }

    log.info("玩家【{}】读取私聊消息数量 : {}", userId, list.size());
    System.out.println("------------------------------");
    list.stream().map(ClientChatKit::toString).forEach(System.out::println);
    System.out.println("------------------------------");

}).setDescription("读取某个玩家的私有消息");

########## 请求 - 新版本 ##########
// 创建一个模拟命令 - 【125-3】读取某个玩家的私有消息
ofCommandUserId(ChatCmd.readPrivateMessage)
        .setTitle("读取某个玩家的私有消息")
        .callback(result -> {
            List<ChatMessage> list = result.listValue(ChatMessage.class);
            if (CollKit.isEmpty(list)) {
                return;
            }

            log.info("玩家【{}】读取私聊消息数量 : {}", userId, list.size());
            System.out.println("------------------------------");
            list.stream().map(ClientChatKit::toString).forEach(System.out::println);
            System.out.println("------------------------------");
        });
########## 请求 - 旧版本 ##########
// 创建一个模拟命令 - 【125-2】未读消息的发送者列表
ofCommand(ChatCmd.listUnreadUserId).callback(LongValueList.class, result -> {
    LongValueList longValueList = result.getValue();
    log.info("未读消息的发送者列表 : {}", longValueList.values);
}).setDescription("未读消息的发送者列表");

########## 请求 - 新版本 ##########
// 创建一个模拟命令 - 【125-2】未读消息的发送者列表
ofCommand(ChatCmd.listUnreadUserId)
        .setTitle("未读消息的发送者列表")
        .callback(result -> {
            List<Long> values = result.listLong();
            log.info("未读消息的发送者列表 : {}", values);
        });

请求

########## 请求 - 旧版本 ##########
// 创建一个模拟命令 - 【125-1】玩家与玩家的私聊
ofCommand(ChatCmd.c_2_c)
        // 动态请求内容 - 私聊,聊天内容
        .setInputRequestData(() -> {
            ... 省略部分

            return chatSendMessage;
        })
        // 命令描述
        .setDescription("玩家与玩家的私聊");

########## 请求 - 新版本 ##########
// 创建一个模拟命令 - 【125-1】玩家与玩家的私聊
ofCommand(ChatCmd.c_2_c)
        .setTitle("玩家与玩家的私聊")
        // 动态请求内容 - 私聊,聊天内容
        .setRequestData(() -> {
            ... 省略部分

            return chatSendMessage;
        });

title、请求参数、响应

########## 请求 - 旧版本 ##########
HelloReq helloReq = new HelloReq();
helloReq.name = "abc12";

ofCommand(DemoCmd.here).callback(HelloReq.class, result -> {
    HelloReq value = result.getValue();
    log.info("value : {}", value);
}).setDescription("here").setRequestData(helloReq);

########## 请求 - 新版本 ##########
ofCommand(DemoCmd.here)
// 标题
.setTitle("here")
// 请求参数
.setRequestData(() -> {
    HelloReq helloReq = new HelloReq();
    helloReq.name = "abc12";
    return helloReq;
})
// 响应
.callback(result -> {
    HelloReq value = result.getValue(HelloReq.class);
    log.info("value : {}", value);
});

########## 请求 - 新版本 - 简写 ##########
ofCommand(DemoCmd.here).setTitle("here").setRequestData(() -> {
    HelloReq helloReq = new HelloReq();
    helloReq.name = "abc12";
    return helloReq;
}).callback(result -> {
    HelloReq value = result.getValue(HelloReq.class);
    log.info("value : {}", value);
});

广播 api

变更说明

listenBroadcast 使用 ofListen 代替。

新版本反序列化消息放在 result.getValue 时解析。

########## 广播 - 旧版本 ##########
// 广播监听回调 - 监听【125-11】玩家私聊消息通知
listenBroadcast(ChatNotifyMessage.class, result -> {
    ChatNotifyMessage chatNotifyMessage = result.getValue();
    // 聊天消息发送方的 userId
    long senderId = chatNotifyMessage.senderId;
    log.info("玩家[{}]给我的私聊通知", senderId);
}, ChatCmd.notifyPrivate, "玩家私聊消息通知");

########## 广播 - 新版本 ##########
// 广播监听回调 - 监听【125-11】玩家私聊消息通知
ofListen(result -> {
    ChatNotifyMessage chatNotifyMessage = result.getValue(ChatNotifyMessage.class);
    // 聊天消息发送方的 userId
    long senderId = chatNotifyMessage.senderId;
    log.info("玩家[{}]给我的私聊通知", senderId);
}, ChatCmd.notifyPrivate, "玩家私聊消息通知");

其他更新

<netty.version>4.1.100.Final</netty.version>
<lombok.version>1.18.30</lombok.version>

Full Changelog: 17.1.55...17.1.58

netty 网络游戏服务器框架 ioGame 17.1.55 action 参数使用体验增强

06 Sep 05:54
Compare
Choose a tag to compare

主要更新

[#186] 增强 ProtoDataCodec
当 data 为null 时,使用空数组来解析序列化;可以确保 action 参数不会为 null,使得开发者拥有更好的编码体验。

将 ActionCommandTryHandler 逻辑合并到 ActionCommandHandler

DevConfig.me 标记为过期,并将 DevConfig 改为静态类;

把 MethodParsers.me 方法标记为过期,并将 MethodParsers 改为静态类;


### 其他更新

<netty.version>4.1.97.Final</netty.version>

<lombok.version>1.18.28</lombok.version>

java 游戏服务器框架 ioGame 17.1.54 nginx 代理 webSocket 获取真实ip

18 Aug 02:24
Compare
Choose a tag to compare

[#174] fix action 交给容器管理时,实例化两次的问题

获取游戏对外服的数据与扩展,获取ResponseCollectExternalMessage 新增 optionalAnySuccess 方法,方便得到成功的 optional

    public String getUserIp() {

        ResponseCollectExternalMessage message = ...

        return message
                .optionalAnySuccess()
                // 得到返回值
                .map(ResponseCollectExternalItemMessage::getData)
                // 将为 String
                .map(Objects::toString)
                // 如果没获取到给个空串,调用方就不需要做 null 判断了。
                .orElse("");
    }

压测&模拟客户端请求模块,新增模块名标识

public class BagInputCommandRegion extends AbstractInputCommandRegion {
    @Override
    public void initInputCommand() {
        this.inputCommandCreate.cmd = BagCmd.cmd;
        this.inputCommandCreate.cmdName = "背包模块";
    }
}

新游戏对外服新增 HttpRealIpHandler,用于获取玩家真实 ip 支持

游戏对外服 webSocket 使用 nginx 代理,也能获取真实的玩家 ip

public class MyExternalServer {
    ... ...省略部分代码
    public ExternalServer createExternalServer(int externalPort) {
        ... ...省略部分代码
        // 游戏对外服 - 构建器
        DefaultExternalServerBuilder builder = ...

        builder.setting().setMicroBootstrapFlow(new WebSocketMicroBootstrapFlow() {
            @Override
            protected void httpHandler(PipelineContext context) {
                super.httpHandler(context);
                /*
                 * HttpRealIpHandler 是框架内置的一个 handler。
                 * 添加上后,即使是通过 nginx 转发,也可以得到玩家真实的 ip
                 */
                context.addLast("HttpRealIpHandler", new HttpRealIpHandler());
            }
        });

        // 构建游戏对外服 https://www.yuque.com/iohao/game/ea6geg
        return builder.build();
    }
}

java 游戏服务器框架 ioGame 17.1.52 webSocket token 鉴权、校验支持

07 Aug 07:03
Compare
Choose a tag to compare

[#172] 新增 webSocket token 鉴权、校验支持

有时,我们需要在 WebSocket 建立连接前做 token 相关鉴权、校验的业务。ioGame 支持此类业务的扩展,我们可以在游戏对外服部分做相关扩展;

简单的说,如果校验没通过,我们就不建立 ws 连接了,在 http 阶段就结束所有流程,可以有效的减少恶意长连接。

相关文档与使用示例 https://www.yuque.com/iohao/game/tb1126szmgfu6u55

日志相关调整
移除 light-log 模块,统一使用 lombok slf4j 相关注解

压测&模拟客户端增强

新增 SplitParam,方便模拟测试时,解析控制台输入参数的获取

    private void useRequest() {
        InputRequestData inputRequestData = () -> {
            ScannerKit.log(() -> log.info("输入需要使用的背包物品,格式 [背包物品id-数量]"));
            String inputType = ScannerKit.nextLine("1-1");

            SplitParam param = new SplitParam(inputType);
            // 得到下标 0 的值
            String id = param.getString(0);
            // 得到下标 1 的值,如果值不存在,则使用默认的 1 代替
            int quantity = param.getInt(1, 1);

            ... ... 省略部分代码
        };

        ofCommand(BagCmd.use).callback(BoolValue.class, result -> {
            var value = result.getValue();
            log.info("value : {}", value);
        }).setDescription("使用背包物品").setInputRequestData(inputRequestData);
    }

Netty 分布式网络游戏服务器框架 ioGame 17.1.50、java MMO 游戏开发的首选

02 Aug 02:09
Compare
Choose a tag to compare

1 DebugInout 在自定义 FlowContext 时的打印优化。

2 cmdInfo 新增 of 系列方法,用于代替 getCmdInfo 系列方法

3 异常机制接口 MsgExceptionInfo,新增方法

// 断言为 true, 就抛出异常,可自定义消息

void assertTrueThrows(boolean v1, String msg)

// 断言值 value 不能为 null, 否则就抛出异常,可自定义消息

void assertNonNull(Object value, String msg)

4 将旧版的游戏对外服标记为过时的

请使用新游戏对外服

5 模拟客户端新增成功回调触发

模拟命令域 InputCommandRegion 新增 loginSuccessCallback 成功回调方法,当模拟玩家登录成功后会调用此方法

public class MapInputCommandRegion extends AbstractInputCommandRegion {
    ... ... 省略部分代码

    @Override
    public void loginSuccessCallback() {
        // 进入地图,根据地图 id
        EnterMapReq enterMapReq = new EnterMapReq();
        enterMapReq.mapId = 1;
        ofRequestCommand(MapCmd.enterMap).request(enterMapReq);
    }
}

6 ClientUser 新增 callbackInputCommandRegion 方法

一般在登录模拟请求的回调中主动的调用,开发者可在登录成功后,调用此方法。使其触发所有的 InputCommandRegion.loginSuccessCallback 方法;

@Slf4j
public class LoginInputCommandRegion extends AbstractInputCommandRegion {
    ... ... 省略部分代码
    @Override
    public void initInputCommand() {
        InputRequestData inputRequestData = () -> {
            LoginVerify loginVerify = new LoginVerify();
            loginVerify.jwt = clientUser.getJwt();
            return loginVerify;
        };
        
        ofCommand(LoginCmd.loginVerify).callback(UserInfo.class, result -> {
            UserInfo userInfo = result.getValue();
            log.info("登录成功 : {}", userInfo);
            clientUser.setUserId(userInfo.id);
            clientUser.setNickname(userInfo.nickname);
            // ------------ 关键代码 ------------
            clientUser.callbackInputCommandRegion();
        }).setDescription("登录").setInputRequestData(inputRequestData);
    }
}

7 压测&模拟客户端请求

新增重复上一次命令的支持

演示

假设在模拟客户端控制台查中,有如下命令

  • 5-1 : 添加玩家经验值

image

增加经验值

每次输入 5-1,会给人物增加 10 点经验值
image

如果我们想多次增加经验值,每次在控制台 5-1 太麻烦了;这里有个小技巧,在控制台输入 【+】,可以重复上次输入的命令。

image

分步式网络编程框架 ioGame 17.1.48 文档生成增强

26 Jul 01:49
Compare
Choose a tag to compare

主要更新

文档生成增强,增加 action 参数注释说明.
文档生成增强,返回值注释说明.
fix 在 pom 中引入本地 jar 时,文档解析的错误。

分步式网络编程框架 ioGame 17.1.46 增加压测&模拟客户端请求模块

18 Jul 02:54
Compare
Choose a tag to compare

主要更新

[160] 轻量小部件 - 压测&模拟客户端请求模块

文档:https://www.yuque.com/iohao/game/tc83ud

介绍

此模块是用于模拟客户端,简化模拟工作量,只需要编写对应请求与回调。

使用该模块后,当我们与前端同学联调某个功能时,不需要跟前端哥们说:在点一下、在点一下、在点一下了。这种“在点一下”的交流联调方式将成为过去式。

除了可以模拟简单的请求外,通常还可以做一些复杂的请求编排,并支持复杂业务的压测。模拟测试的过程是可互动的,但也支持测试自动化。

与单元测试不同的是,该模块可以模拟真实的网络环境,并且在模拟测试的过程中与服务器交互是可持续的、可互动的

可互动模式是用于调试测试某些功能。在互动的过程中,开发者可以在控制台中指定执行某个模拟请求命令,并且支持在控制台中输入一些动态的请求参数,从而让我们轻松的测试不同的业务逻辑走向。

关于可互动部分,需要将后续文档阅读完后才能知道其具体意思。

特点

  • 使用简单
  • 压测支持
  • 可以模拟客户端请求
  • 可以模拟真实的网络环境
  • 可以编排复杂的业务请求
  • 同样的模拟测试用例,支持在多种连接方式下工作(tcp、udp、websocket)
  • 可持续的与服务器交互,模拟测试的过程是可互动的,但也支持测试自动化

入门级演示

整体文档有点多,但在实际的使用上是比较简单的。

图一

图左边是我们提供的 action,图右边则是我们编写的模拟请求。

0cd9b0d4aa682c3dddc2a372e76ac0f7

图二

控制台中是可交互的部分,可以查看提供了哪些模拟的客户端请求。

通过在控制台中输入 【cmd-subCmd】 来触发对应的请求。

51148d07dde132224a04780b51e2209c

图三

触发请求后,当服务器有响应数据时,会进入到对应模拟请求的回调中。

6674c6a4ab6f7a72bb8d5594ad4e0e00

小结

比如需要做一个简单的:私聊系统、聊天频道、好友系统时,这个模拟客户端就派上用场了。

模拟客户端模块还提供了一个 ClientUser(玩家)对象,一个模拟客户端对应一个 ClientUser。ClientUser 是客户端的用户(玩家)对象,开发者可以通过动态属性 options 来扩展业务,比如可以在动态属性中保存货币、战力值、血条 ...等。也可以通过继承的方式来扩展。

其他更新

文档生成增强,增加 action 参数注释说明、返回值注释说明。

ioGame 网络编程框架 17.1.45 同进程亲和性

07 Jul 03:35
Compare
Choose a tag to compare

主要更新

[#159] 同进程同时支持多种连接方式方式的技巧

public class MyApplication {
    ... ... 省略部分代码
    static int externalCorePort = 10100;

    public static void main(String[] args) {
        // 游戏对外服列表
        List<ExternalServer> externalServerList = listExternalServer();
        new NettyRunOne()
                .setExternalServerList(externalServerList)
                .startup();
    }

    static List<ExternalServer> listExternalServer() {
        return List.of(
                // 连接方式;WEBSOCKET
                createExternalServer(ExternalJoinEnum.WEBSOCKET)
                // 连接方式;TCP
                , createExternalServer(ExternalJoinEnum.TCP)
                // 连接方式;UDP
                , createExternalServer(ExternalJoinEnum.UDP)
        );
    }

    static ExternalServer createExternalServer(ExternalJoinEnum join) {
        int port = externalCorePort;
        port = join.cocPort(port);
        DefaultExternalServerBuilder builder = DefaultExternalServer
                .newBuilder(port)
                // 连接方式
                .externalJoinEnum(join)
                // 与 Broker (游戏网关)的连接地址
                .brokerAddress(new BrokerAddress("127.0.0.1", IoGameGlobalConfig.brokerPort));

        return builder.build();
    }
}

[#157] fix 默认心跳钩子问题

[#122] 同进程亲和性

相关文档:https://www.yuque.com/iohao/game/unp26u

简介

同进程内不同 Netty 实例通信时,是通过内存进行传输的,不需要经过网络传输,数据传输速度极快。也就是说,如果我们将游戏对外服、Broker(游戏网关)、游戏逻辑服部署在同一个进程中(也就是单体应用),那么各服务器之间是在内存中通信的。甚至可以简单的理解为在同一 JVM 中的 a 方法调用了 b 方法,b 方法调用了 c 方法。

同进程亲和性是 ioGame 的特性之一,可以让同一进程内的 Netty 实例拥有相互访问优先权。说人话就是,如果你在同一进程内启动了游戏对外服、Broker(游戏网关)、游戏逻辑服,当有请求需要处理时:

  • 即使你启动了多个 Broker(游戏网关),游戏对外服会优先将请求交给同进程内的 Broker(游戏网关)来处理。
  • 即使你启动了多个相同的游戏逻辑服,Broker(游戏网关)会优先将请求交给同进程的游戏逻辑服来处理。
  • 同样的,游戏逻辑服处理完请求后,会优先将响应交给同进程内的 Broker(游戏网关)。

部署简图

img

图中有这么几个部分,分别是:

  • 玩家,这些玩家可能连接到【1111】或【2222】进程中。
  • 【进程 3333】启动了多个公共的游戏逻辑服。
  • 【进程 1111】启动了游戏对外服、Broker(游戏网关)、游戏逻辑服[A-1、B-1]。
  • 【进程 2222】启动了游戏对外服、Broker(游戏网关)、游戏逻辑服[A-2、B-2]。

TIP:同一进程内,不同 Netty 实例之间的通信,是通过内存进行传输的,不需要经过网络传输,数据传输速度极快。

同进程亲和性指的是,优先访问同进程内的游戏逻辑服,当同进程内没有能处理请求的游戏逻辑服时,才会去其他进程或机器中查找能处理请求的游戏逻辑服;

处理流程

下面用一个场景来说明,游戏逻辑服对应的 action 数据如下:

游戏逻辑服-A 提供了路由:1-1。

游戏逻辑服-C 提供了路由:2-1。

从图中我们知道了,相同类型的【游戏逻辑服-A】启动了两台,分别在【进程 1111】和【进程 2222】中。

现在我们有一个玩家与【进程 1111】的游戏对外服建立了连接,并发起 1-1 请求;请求会被【游戏逻辑服-A-1】来处理,因为【游戏逻辑服-A-1】与玩家所在的【游戏对外服】是同一个进程内的,所以该请求会优先被【游戏逻辑服-A-1】消费;

通过同进程亲和性我们可以看出,虽然启动了两台相同类型的【游戏逻辑服-A】,但玩家的请求只会由【进程 1111】内的【游戏逻辑服-A-1】来消费,因为玩家连接的是【进程 1111】的游戏对外服。

当玩家发起 2-1 请求时,在【进程 1111】内找不到对应的游戏逻辑服来处理这个请求时,框架会将请求交给【游戏逻辑服-C-1】来处理。

使用场景

开发者可以利用同进程亲和性的特点,按照上图中的部署方式,可以让各服务器之间通过进程来做微隔离。此外,又能提供一些游戏逻辑服来处理公共业务;如一些跨服活动、跨服战...等,各种跨服业务

使用该部署方式,可做参考的游戏类型如下:

  • 滚服类型的游戏
  • RTS(Real Time Strategy)实时战略游戏,例如星际争霸、红色警戒。
  • MOBA(Multiplayer Online Battle Arena)多人在线竞技场游戏,例如英雄联盟、DOTA2。
  • ARPG(Action Role-playing Game)动作角色扮演游戏,例如暗黑破坏神、无尽之剑。
  • FPS(First-person Shooter)一人称射击游戏,例如使命召唤、绝地求生。
  • TPS(Third-person Shooter)第三人称射击游戏,例如疾速前进、战地。
  • 待补充...

小结

同进程亲和性是优先访问同进程内的游戏逻辑服,并不是说不能访问其他进程的游戏逻辑服。

特点

  • 同进程亲和性换句话说就是,除了可以优先让同进程内的逻辑服相互访问外,还能访问其他进程的逻辑服。
  • 各逻辑服在同进程内的通信,是通过内存进行传输的,不需要经过网络传输,数据传输速度极快。
  • 合理利用同进程亲和性,可以让各服务器之间通过进程来做微隔离。

最后

ioGame 的架构由三部分组成:1.游戏对外服、2.Broker(游戏网关)、3.游戏逻辑服;三者既可相互独立,又可相互融合。所以,使用 ioGame 几乎可以满足任意的部署方式,可以根据你的需求来适应不同类型的游戏,并且在 ioGame 中做这些工作是简单的。

这里为什么要特意介绍一下三者之间的组合关系呢?目的是想告诉开发者,ioGame 的架构是灵活的。同进程亲和性在以下组合会生效。

为了更简单的说明三者之间的灵活性,现在把三者用字母代替,A.游戏对外服、B.Broker(游戏网关)、C.游戏逻辑服;我们可以得出如下组合

ABC :三者在一个进程中,他们之间使用内存通信;(无需传输

AB + C :【游戏对外服和游戏网关】在一个进程中,他们之间使用内存通信;(传输一次

A + BC :【游戏网关和游戏逻辑服】在一个进程中,他们之间使用内存通信;(传输一次

通过上面的组合,我们可以看出,ioGame 架构是支持类似传统架构那样只做一次跳转,甚至可以做到零跳转,这完全取决于你们的部署方式。

上面三种组合方式,都具备同进程亲和性。同进程亲和性生效的要点只有一个,至少保证有两部分在一个进程内。

Netty 网络游戏服务器框架 ioGame 17.1.44

14 Jun 02:05
Compare
Choose a tag to compare

主要更新

[#138] 提供协议碎片的工具类,方便协议碎片在广播时的使用

代码中演示了协议碎片相关的使用,通过工具类,可以让一些基础类型在使用上更简便。
对应的包装类中,都提供了静态 of 方法;

框架支持的包装类可到 框架支持的自动装箱、拆箱基础类型 查询。

... ... 省略部分代码
private static void test() {
  // 给客户端广播一个 int 值 : 1
  var bizData = WrapperKit.of(1);

  // 广播上下文
  CmdInfo cmdInfo = CmdInfo.getCmdInfo(DemoBroadcastCmd.cmd, DemoBroadcastCmd.broadcastMsg);
  BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();
  broadcastContext.broadcast(cmdInfo, bizData);

  // 给客户端广播一个 bool 值 : true
  var bizDataBoolean = WrapperKit.of(true);
  broadcastContext.broadcast(cmdInfo, bizDataBoolean);

  // 对象列表演示
  DemoBroadcastMessage broadcastMessage = new DemoBroadcastMessage();
  broadcastMessage.msg = "broadcast hello," + counter.longValue();
  List<DemoBroadcastMessage> list = new ArrayList<>();
  list.add(broadcastMessage);
  var bizDataList = WrapperKit.ofListByteValue(list);
  broadcastContext.broadcast(cmdInfo, bizDataList);

  // int 列表
  var bizDataIntList = IntValueList.of(List.of(1, 3, 5, 7));
  broadcastContext.broadcast(cmdInfo, bizDataIntList);

  ... ... 省略部分代码
  其他类同,不全部介绍了。
}

[#133] 向指定对外服上的用户广播数据

示例参考代码

... ...省略部分代码
private static void extracted(String externalId) {
    var bizData = new DemoBroadcastMessage();
    broadcastMessage.msg = "broadcast hello!" ;

    // 广播消息的路由
    CmdInfo cmdInfo = ...;
    ResponseMessage responseMessage = BarMessageKit.createResponseMessage(cmdInfo, bizData);
    
    // 指定游戏对外服广播
    HeadMetadata headMetadata = responseMessage.getHeadMetadata();
    int sourceClientId = MurmurHash3.hash32(externalId);
    headMetadata.setSourceClientId(sourceClientId);

    // 广播上下文
    BroadcastContext broadcastContext = BrokerClientHelper.getBroadcastContext();
    broadcastContext.broadcast(responseMessage);
}

容错设置

IoGameGlobalConfig.brokerSniperToggleAK47 = boolean;

Broker(游戏网关)转发消息容错配置
      游戏逻辑服与游戏对外服通信时,如果没有明确指定要通信游戏对外服,游戏网关则会将消息转发到所有的游戏对外服上。
      如果指定了游戏对外服的,游戏网关则会将消息转发到该游戏对外服上,而不会将消息转发到所有的对外服上。
 
      当为 true 时,开启容错机制
          表示开发者在发送消息时,如果指定了游戏对外服的,
          但【游戏网关】中没有找到所指定的【游戏对外服】,则会将消息转发到所有的游戏对外服上,
          这么做的目的是,即使开发者填错了指定的游戏对外服,也能保证消息可以送达到游戏对外服。
 
      当为 false 时,关闭容错机制
          表示在【游戏网关】中找不到指定的【游戏对外服】时,则不管了。
 
      支持的通讯方式场景
          广播、推送 
          获取游戏对外服的数据与扩展 
  
另一种叙述版本
      作用:
          在游戏逻辑服发送广播时,支持指定游戏对外服来广播;
          如果你能事先知道所要广播的游戏对外服,那么在广播时通过指定游戏对外服,可以避免一些无效的转发。
 
          为了更好的理解的这个配置的作用,这里将作一些比喻:
          1. 将广播时指定的游戏对外服,看作是目标
          2. 将发送广播的游戏逻辑服,看作是命令
          3. 而 Broker(游戏网关)职责是对消息做转发,可看成是一名射击员;射击员手上有两把枪,分别是狙击枪和 AK47。
 
          狙击枪的作用是单点目标,而 AK47 的作用则是扫射多个目标(就是所有的游戏对外服)。
 
      场景一:
          当设置为 true 时,表示射击员可以将手中的狙击切换为 AK47,什么意思呢?
          意思就是如果在【游戏网关】中没有找到所指定的【游戏对外服】,则将广播数据发送给【所有的游戏对外服】。(换 AK 来扫射)
          这么做的目的是,即使开发者填错了指定的游戏对外服,也能保证消息可以送达到游戏对外服。
 
      场景二:
          当设置为 false 时,表示找不到指定的【游戏对外服】时,则不管了。

[#131] 获取指定对外服上数据的接口

参考使用示例,通过 RequestCollectExternalMessage 请求对象,可以指定游戏对外服id;

@UtilityClass
public class ExternalCommunicationKit {
    /**
     * 设置元信息到游戏对外服
     * 
<pre>
     *     之后所有 action 的 FlowContext 中会携带上这个元信息对象,
     *     不建议在元信息保存过多的信息,因为会每次传递。
     * </pre>
*
     * @param attachment  元信息
     * @param flowContext flowContext
     */
    public void setAttachment(Attachment attachment, FlowContext flowContext) {
        // 不做 null 判断,只做个 userId 的检测
        long userId = attachment.getUserId();

        if (userId <= 0) {
            throw new RuntimeException("userId <= 0");
        }

        // 得到游戏对外服 id
        RequestMessage request = flowContext.getRequest();
        HeadMetadata headMetadata = request.getHeadMetadata();
        int sourceClientId = headMetadata.getSourceClientId();

        var requestCollectExternalMessage = new RequestCollectExternalMessage()
                // 根据业务码,调用游戏对外服与业务码对应的业务实现类 (AttachmentDataExternalBizRegion)
                .setBizCode(ExternalBizCodeCont.attachment)
                // 元信息
                .setData(attachment)
                // 指定游戏对外服
                .setSourceClientId(sourceClientId);

        BrokerClientHelper
                // 【游戏逻辑服】与【游戏对外服】通讯上下文
                .getInvokeExternalModuleContext()
                .invokeExternalModuleCollectMessage(requestCollectExternalMessage);
    }
}

容错设置

IoGameGlobalConfig.brokerSniperToggleAK47 = boolean;