Skip to main content

压测&模拟客户端请求

介绍

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

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

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

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


模拟客户端特点

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

如何安装

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

see https://central.sonatype.com/artifact/com.iohao.net/extension-client

pom.xml
<dependency>
<groupId>com.iohao.net</groupId>
<artifactId>extension-client</artifactId>
<version>${ionet.version}</version>
</dependency>

入门级的演示

action 与模拟客户端

  • DemoAction: 是服务器对外提供的 action。
  • DemoClient: 是模拟客户端请求的代码。

图中的模拟请求对应着 action。

An



启动模拟客户端后

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

cmd-subCmd

An



控制台中输入请求命令

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

An



小结

在整体的使用上是简单的,开发者只需要编写对应的请求与回调。 之后在控制台中输入对应的模拟命令编号,就能触发请求了。 入门级的演示到这就结束的,后续是进阶级的内容, 如果是你纯新手或刚接触框架的开发者,不建议继续往下看了

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

概念介绍

模拟客户端由多个自定义的 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());
}

如何查看广播与请求命令

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

An

如何执行请求命令

通过代码

ofRequestCommand(DemoCmd.here).execute();
// or
executeCommand(DemoCmd.here);


通过控制台

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

An

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 来扩展业务,比如可以在动态属性中保存货币、战力值、血条 ...等。

压测

该扩展模块除了能模拟真实的用户操作以外,还能做一些压测相关的工作。 下面,我们用一个示例来演示压测,整体流程如下

  1. 用户登录。
  2. 执行 PressureRegion.ppp 方法,方法会请求 10 次 inc action。
  3. 当计数器不满足条件时,一秒后继续执行 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