Skip to main content

压测&模拟客户端请求

介绍

这部分的内容是扮演架构简图中的玩家部分,也就是用于模拟玩家的。

当前模块是用于模拟客户端,简化模拟工作量,只需要编写对应请求与回调。 使用该模块后,当我们与前端同学联调某个功能时,不需要跟前端哥们说:在点一下、在点一下、在点一下了, 这种在点一下的交流联调方式将成为过去式。

除了可以模拟简单的请求外,通常还可以做一些复杂的请求编排,并支持复杂业务的压测。 模拟测试的过程是可互动的,但也支持测试自动化。 与单元测试不同的是,该模块可以模拟真实的网络环境,并且在模拟测试的过程中与服务器交互是可持续的、可互动的。

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


模拟客户端特点

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

如何安装

由于模拟客户端是单独的、按需选择的功能模块,使用时需要在 pom.xml 中引入

see https://central.sonatype.com/artifact/com.iohao.game/light-client

pom.xml
<dependency>
<groupId>com.iohao.game</groupId>
<artifactId>light-client</artifactId>
<version>${ioGame.version}</version>
</dependency>

入门级的演示

action 与模拟客户端

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

An



启动模拟客户端后

启动模拟客户端后,如下图所示。 控制台中是可交互的部分,可以查看提供了哪些模拟的客户端请求。 我们可以在控制台中输入 cmd-subCmd 来触发对应的请求。

An



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

我们是可以在控制台中多次调用模拟命令的,如果你有一个业务是需要多次测试的,那么这种方式是友好的。

An



小结

在整体的使用上是简单的,开发者只需要编写对应的请求与回调。 之后在控制台中输入对应的模拟命令编号,就能触发请求了。

入门级的演示到这就结束的,后续是进阶级的内容, 如果是你纯新手或刚接触框架的开发者,不建议继续往下看了

这个示例使用的是快速从零编写服务器完整示例中的代码来演示的,需要做简单体验的,可以下载源码尝试。

概念介绍

tip

模拟客户端由多个自定义的 InputCommandRegion 组成。

通常一个 InputCommandRegion 对应一个 action 类。

InputCommandRegion

InputCommandRegion 用于配置模拟客户端请求和监听服务器广播。 自定义 InputCommandRegion 需要继承 AbstractInputCommandRegion

添加广播与请求命令

代码说明

  • code 4,当我们启动模拟客户端时,会触发 initInputCommand 方法,开发者可以在些方法内编写模拟请求相关的代码。
  • code 6,设置主路由,与 action 类使用的主路由对应。
  • code 9 ~ 16,添加一个请求模拟命令。
    • ofCommand,设置一个子路由,用于访问具体的 action。
    • setTitle,给请求命令设置一个标题,方便在控制台查看。
    • setRequestData,该对象会被 action 接收,作为 action 的请求参数。
    • callback,接收 action 的返回值。
  • code 21 ~ 24,添加一个广播监听,用于接收服务器的广播数据。
    • ofListen,设置一个子路由,监听广播路由,并设置一个标题,方便在控制台查看。
    • code 22,当接收到服务器广播的数据,我们可以通过 result 对象来获取。
public class DemoRegion extends AbstractInputCommandRegion {
...
@Override
public void initInputCommand() {
// Setting cmd. cn:设置主路由
inputCommandCreate.cmd = DemoCmd.cmd;

// ---------------- request 1-0 ----------------
ofCommand(DemoCmd.here).setTitle("here").setRequestData(() -> {
HelloMessage helloMessage = new HelloMessage();
helloMessage.name = "1";
return helloMessage;
}).callback(result -> {
HelloMessage value = result.getValue(HelloMessage.class);
log.info("{}", value);
});

...

// ---------------- Broadcast Listener. cn: 广播监听 ----------------
ofListen(result -> {
List<HelloMessage> helloMessageList = result.listValue(HelloMessage.class);
log.info("{}", helloMessageList);
}, DemoCmd.listenList, "listenList");
}
}


public interface DemoCmd {
int cmd = 1;
int here = 0;
int jackson = 1;
int list = 2;
int listenValue = 4;
int listenList = 5;
}

如何查看广播与请求命令

  • 在控制台中输入 .,可以查看所有的请求命令。
  • 在控制台中输入 ..,可以查看所有的广播命令。
  • 在控制台中输入 ...,可以查看所有的请求和广播命令。

An

如何执行请求命令

通过代码

ofRequestCommand(DemoCmd.here).execute();


通过控制台

在控制台中,输入路由 1-0

An

ClientRunOne 启动客户端

ClientRunOne 是客户端启动类。

  • code 4,添加自定义 InputCommandRegion。
  • code 5,启动模拟客户端。
public class DemoClient {
public static void main(String[] args) {
new ClientRunOne()
.setInputCommandRegions(List.of(new DemoRegion()))
.startup();
}
}

更多配置

  • code 3,关闭模拟请求相关日志
  • code 5 ~ 10,InputCommandRegion 按需加载。 之前我们提到过,一个 Action 类对应一个 InputCommandRegion。 因为随着系统的迭代,action 的数量可能会有几百、几千个。 这种管理方式可以让我们按需加载对应模块的测试数据,避免过多的干扰项。
  • code 14,开启心跳,每 10 秒发送一次。
  • code 15,设置连接方式。
  • code 16,设置连接 ip。
  • code 17,设置连接端口
public class DemoClient {
static void start(long userId) {
ClientUserConfigs.closeLog();

List<InputCommandRegion> inputCommandRegions = List.of(
new LoginInputCommandRegion()
// , new FriendInputCommandRegion()
// , new ChatInputCommandRegion()
// , new MailInputCommandRegion()
);

new ClientRunOne()
.setInputCommandRegions(inputCommandRegions)
.idle(10)
.setJoinEnum(ExternalJoinEnum.TCP)
.setConnectAddress("127.0.0.1")
.setConnectPort(ExternalGlobalConfig.externalPort)
.startup();
}
}

ScannerKit 控制台输入辅助类

ScannerKit 是控制台输入辅助类,可用于提示及接收控制台的输入,这在一些需要动态输入数据的测试场景下特别有用。

  • code 2,在控制台提示测试者需要输入的内容。
  • code 3,接收控制台的输入
ofCommand(DemoCmd.name).setRequestData(() -> {
ScannerKit.log(() -> log.info("Please enter your name."));
String name = ScannerKit.nextLine("Michael Jackson");

HelloMessage helloMessage = new HelloMessage();
helloMessage.name = name;
return helloMessage;
})

ClientUser

ClientUser 是玩家对象,一个模拟客户端对应一个 ClientUser。

开发者可以通过动态属性 options 来扩展业务,比如可以在动态属性中保存货币、战力值、血条 ...等,也可以通过继承的方式来扩展。

压测

模拟客户端模块除了可以模拟真实的玩家操作以外,还能做一些压测相关的工作。 下面示例中,我们在 PressureInputCommandRegion 中配置了一个模拟请求 inc action。


这里使用一个示例来演示压测,我们启动了 N 个玩家,整体流程如下

  1. 玩家登录
  2. 执行 PressureInputCommandRegion.ppp 方法,方法会请求 100 次 inc action。
  3. 当计数器不满足条件时,一秒后继续执行 ppp 方法。

示例中我们设置 80 个玩家,那么服务器 inc 被请求的次数 = 80 * 10 * 100 。


代码说明

  • code 6,需要模拟压测的玩家数量
  • code 16,玩家对象
  • code 20,一秒后触发登录请求
public class PressureClient {
public static void main(String[] args) throws InterruptedException {
ClientUserConfigs.closeScanner = true;
ClientUserConfigs.closeLog();

int userSize = 80;
for (int i = 1; i <= userSize; i++) {
long userId = i;
TaskKit.execute(() -> start(userId));
}

TimeUnit.SECONDS.sleep(1);
}

static void start(long userId) {
ClientUser clientUser = new DefaultClientUser();
String jwt = String.valueOf(userId);
clientUser.setJwt(jwt);

TaskKit.runOnceSecond(() -> {
CmdInfo cmdInfo = LoginCmd.of(LoginCmd.login);

clientUser.getClientUserInputCommands()
.ofRequestCommand(cmdInfo)
.requestCommand.execute();
});

List<InputCommandRegion> inputCommandRegions = List.of(
new LoginInputCommandRegion()
, new PressureInputCommandRegion()
);

new ClientRunOne()
.setClientUser(clientUser)
.setInputCommandRegions(inputCommandRegions)
.startup();
}
}

  • code 1,压测 InputCommandRegion 类。
  • code 9,需要压测的业务代码。将任务添加到队列中,当玩家全部登录完成后就开始执行任务。
  • code 12 ~ 27,ppp 方法内启动了一个定时任务。 定时任务执行 10 次,每次向服务器发送 100 次请求。
class PressureInputCommandRegion extends AbstractInputCommandRegion {
LongAdder count = new LongAdder();

@Override
public void initInputCommand() {
this.inputCommandCreate.cmd = LoginCmd.cmd;
ofCommand(LoginCmd.inc).setTitle("inc");

ClientUsers.execute(this::ppp);
}

private void ppp() {
TaskKit.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
count.increment();
if (count.longValue() > 10) {
return;
}

for (int i = 0; i < 100; i++) {
ofRequestCommand(LoginCmd.inc).execute();
}

TaskKit.newTimeout(this, 1, TimeUnit.SECONDS);
}
}, 1, TimeUnit.SECONDS);
}
}

在 PressureClient 类中,使用了 ClientUsers 类来辅助我们做压测。 ClientUsers 提供了一个 void execute(Runnable task) 方法, 将任务添加到队列中,当全部玩家登录完成后才会开始执行任务。

如果你的压测业务是需要等待所有玩家登录成功后才执行业务代码的,可以考虑使用这个方法。


压测小结

我们在压测这一小节中,演示了 N 个玩家向服务器发送多个请求,虽然示例很简单。 开发者可以根据自身的业务来定制相关的压测业务。 因为我们可以在模拟请求中编排任何请求逻辑,所以无论多么复杂的业务都是可以做到的。

注意事项

编解码器

框架默认的编解码器使用的是 jprotobuf ,如果服务器使用的是 json,模拟客户端需要与服务器保持同样的编解码器。

see pb、json 数据协议扩展

public class ClientApplication {
public static void main(String[] args) {
IoGameGlobalSetting.setDataCodec(new JsonDataCodec());

// ... new ClientRunOne()
}
}

Example Source Code

see https://github.com/iohao/ioGameExamples

path : SimpleExample/example/component

  • ComponentApplication,游戏服务器启动类。
  • MyClient1 模拟客户端启动类,模拟玩家1
  • MyClient2 模拟客户端启动类,模拟玩家2
  • PressureClient 压测模拟客户端启动类。

聊天测试

将 MyClient1 和 MyClient2 启动好后,为了方便描述,后续将使用以下简称

  • MyClient1 简称为玩家1
  • MyClient2 简称为玩家2

玩家1玩家2 发送一条私聊消息。 通过控制台的信息,我们得知 125-1 是玩家与玩家的私聊命令编号。



测试步骤

  1. 控制台中输入 125-1。
  2. 控制台要求我们输入聊天消息接收方的 userId,我们输入 2 后按回车。
  3. 控制台要求我们输入聊天内容,随意输入内容后按回车。

An



现在,我们来看 MyClient2 的控制台中分别都有哪些内容

  1. 触发广播监听回调,【125-11 : 玩家私聊消息通知】
  2. 在广播监听的回调中,打印了玩家1给我的私聊通知。
  3. 玩家2 向服务器发送请求 【125-3 : 读取某个玩家的私有消息】
  4. 玩家2 的请求回调【125-3 : 读取某个玩家的私有消息】
  5. 玩家2 读取私聊消息数量 : 1。打印了玩家1玩家2发送的聊天内容。

An