压测&模拟客户端请求
介绍
这部分的内容是扮演架构简图中的玩家部分,也就是用于模拟玩家的。
当前模块是用于模拟客户端,简化模拟工作量,只需要编写对应请求与回调。 使用该模块后,当我们与前端同学联调某个功能时,不需要跟前端哥们说:在点一下、在点一下、在点一下了, 这种在点一下的交流联调方式将成为过去式。
除了可以模拟简单的请求外,通常还可以做一些复杂的请求编排,并支持复杂业务的压测。 模拟测试的过程是可互动的,但也支持测试自动化。 与单元测试不同的是,该模块可以模拟真实的网络环境,并且在模拟测试的过程中与服务器交互是可持续的、可互动的。
可互动模式是用于调试测试某些功能,在互动的过程中,开发者可以在控制台中指定执行某个模拟请求命令, 并且支持在控制台中输入一些动态的请求参数,从而让我们轻松的测试不同的业务逻辑走向。
模拟客户端特点
- 使用简单
- 压测支持
- 可以模拟客户端请求
- 可以模拟真实的网络环境
- 可以编排复杂的业务请求
- 同样的模拟测试用例,支持在多种连接方式下工作(tcp、udp、websocket)
- 可持续的与服务器交互,模拟测试的过程是可互动的。同时也支持测试自动化
如何安装
由于模拟客户端是单独的、按需选择的功能模块,使用时需要在 pom.xml 中引入
see https://central.sonatype.com/artifact/com.iohao.net/extension-client
<dependency>
<groupId>com.iohao.net</groupId>
<artifactId>extension-client</artifactId>
<version>${ionet.version}</version>
</dependency>
入门级的演示
action 与模拟客户端
- DemoAction: 是服务器对外提供的 action。
- DemoClient: 是模拟客户端请求的代码。
图中的模拟请求对应着 action。

启动模拟客户端后
如下图所示,控制台中是可交互的部分。 我们可以查模拟请求列表,还可以在控制台中输入命令来触发对应的请求。
cmd-subCmd

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

小结
在整体的使用上是简单的,开发者只需要编写对应的请求与回调。 之后在控制台中输入对应的模拟命令编号,就能触发请求了。 入门级的演示到这就结束的,后续是进阶级的内容, 如果是你纯新手或刚接触框架的开发者,不建议继续往下看了。
这个示例使用的是快速从零编写服务器完整示例中的代码来演示的,需要做简单体验的,可以下载源码尝试。
概念介绍
模拟客户端由多个自定义的 InputCommandRegion 组成,通常一个 InputCommandRegion 对应一个 action 类。
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:设置主路由
this.setCmd(DemoCmd.cmd, "TestDemo");
// ---------------- 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 triggerBroadcast = 3;
AtomicInteger broadcastIndex = new AtomicInteger(20);
CmdInfo listenData = CmdInfo.of(cmd, broadcastIndex.getAndIncrement());
CmdInfo listenList = CmdInfo.of(cmd, broadcastIndex.getAndIncrement());
}
如何查看广播与请求命令
- 在控制台中输入
.,可以查看所有的请求命令。 - 在控制台中输入
..,可以查看所有的广播命令。 - 在控制台中输入
...,可以查看所有的请求和广播命令。

如何执行请求命令
通过代码
ofRequestCommand(DemoCmd.here).execute();
// or
executeCommand(DemoCmd.here);
通过控制台
在控制台中,输入路由 1-0

ClientRunOne 启动客户端
ClientRunOne 是客户端启动类。
- code 10,添加自定义 InputCommandRegion。
- code 11,启动模拟客户端。
public class DemoClient {
static void main() {
// US or CHINA
Locale.setDefault(Locale.US);
// closeLog. cn: 关闭模拟请求相关日志
ClientUserConfigs.closeLog();
// Startup Client. cn: 启动模拟客户端
new ClientRunOne()
.setInputCommandRegions(List.of(new DemoRegion()))
.startup();
}
static class DemoRegion extends AbstractInputCommandRegion {
@Override
public void initInputCommand() {
...
}
}
}
更多配置
- code 4,i18n 设置。
- code 5,关闭控制台输入。
- code 6,关闭模拟请求相关日志。
- code 8 ~ 13,InputCommandRegion 按需加载。 之前我们提到过,一个 Action 类对应一个 InputCommandRegion。 因为随着系统的迭代,action 的数量可能会有几百、几千个。 这种管理方式可以让我们按需加载对应模块的测试数据,避免过多的干扰项。
- code 17,开启心跳,每 10 秒发送一次。
- code 18,设置连接方式。
- code 19,设置连接 ip。
- code 20,设置连接端口
public class DemoClient {
static void start(long userId) {
// US or CHINA
Locale.setDefault(Locale.US);
ClientUserConfigs.closeScanner = true;
ClientUserConfigs.closeLog();
List<InputCommandRegion> inputCommandRegions = List.of(
new HallRegion()
// , new RoomRegion()
// , new Jsr380Region()
// , new BroadcastRegion()
);
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 表示用户,一个模拟客户端对应一个。 开发者可以通过动态属性 options 来扩展业务,比如可以在动态属性中保存货币、战力值、血条 ...等。
压测
该扩展模块除了能模拟真实的用户操作以外,还能做一些压测相关的工作。 下面,我们用一个示例来演示压测,整体流程如下
- 用户登录。
- 执行
PressureRegion.ppp方法,方法会请求 10 次 inc action。 - 当计数器不满足条件时,一秒后继续执行
ppp方法。
- code 2,pressureUserSize 表示需要模拟压测的用户数量。
- code 3,pressureSecondsRequest 表示每个用户每秒的请求次数。
- code 4,pressureRound 表示需要测试多少轮。
- code 11,创建模拟用户。
- code 14~17,设置并启动。
public class PressureClient {
static int pressureUserSize = 80;
static int pressureSecondsRequest = 10;
static int pressureRound = 10;
static void main() throws InterruptedException {
for (int i = 1; i <= pressureUserSize; i++) {
long userId = i;
TaskKit.executeVirtual(() -> {
ClientUser clientUser = new DefaultClientUser();
clientUser.setJwt(String.valueOf(userId));
new ClientRunOne()
.setClientUser(clientUser)
.setInputCommandRegions(List.of(new PressureRegion()))
.startup();
});
}
TimeUnit.SECONDS.sleep(1);
}
}
PressureRegion
- code 4,连接成功后执行登录请求。
- code 24,添加登录后执行的任务
ppp,该方法是我们需要压测的业务代码。 - code 30: 启动定时任务,每秒执行一次
onUpdate方法,该方法每次向服务器发送 N 次请求。 - code 42: 停止定时任务的测试条件。
static class PressureRegion extends AbstractInputCommandRegion {
@Override
public void connectionComplete() {
this.executeCommand(login);
}
@Override
public void initInputCommand() {
this.setCmd(PressureCmd.cmd, "Pressure");
ofCommand(login, "login").setRequestData(() -> {
ClientUser clientUser = getClientUser();
return clientUser.getJwt();
}).callback(result -> {
UserMessage message = result.getValue(UserMessage.class);
log.info("loginSuccess: {}", message);
this.setUserId(message.id);
});
ofCommand(inc).setTitle("inc").callback(_ -> {
});
// Tasks to be executed after all users have completed login
PressureKit.addAfterLoginTask(this::ppp);
}
AtomicInteger count = new AtomicInteger();
private void ppp() {
TaskKit.runInterval(new IntervalTaskListener() {
@Override
public void onUpdate() {
// Request inc action.
for (int i = 0; i < pressureSecondsRequest; i++) {
executeCommand(PressureCmd.inc);
}
}
@Override
public boolean isActive() {
// Stop test condition
return count.incrementAndGet() <= pressureRound;
}
}, 1, TimeUnit.SECONDS);
}
}
压测小结
我们在压测这一小节中,演示了 N 个玩家向服务器发送多个请求,虽然示例很简单。 开发者可以根据自身的业务来定制相关的压测业务。 因为我们可以在模拟请求中编排任何请求逻辑,所以无论多么复杂的业务都是可以做到的。
小结
该模块为我们提供了
- 模拟请求,监听广播。
- ScannerKit 控制台输入辅助。
- 压测。
整体的概念及使用是非常的简单,一个 action 类对应一个 InputCommandRegion 测试类,这样就可以做到按需加载测试类。
Example Source Code
see https://github.com/iohao/ionet-examples
path : ionet-cookbook-code
- OneApplication
- OneClient
- PressureClient