1、实现顺序
2、本地执行某一项目步骤
1. 安装IDEA工具:社区版本地址:
https://www.jetbrains.com/idea/download/#section=mac
2. 创建Maven项目、命名
3. pom.xml文件添加本地、或者导入自己的Maven
每个依赖对应添加版本:
<properties>
<ngrinder.version>3.4.4</ngrinder.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.3.8</spring.version>
<ws.version>1.5.2</ws.version>
<fastjson.version>1.2.76</fastjson.version>
<lombok.version>1.18.20</lombok.version>
<hutool.version>5.7.0</hutool.version>
<socket.io.version>1.0.1</socket.io.version>
</properties>
<dependencies>
<dependency>
<groupId>org.ngrinder</groupId>
<artifactId>ngrinder-groovy</artifactId>
<version>${ngrinder.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>${ws.version}</version>
</dependency>
<dependency>
<groupId>io.socket</groupId>
<artifactId>socket.io-client</artifactId>
<version>${socket.io.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
</dependencies>
4. 通过IDEA解决spring配置文件
https://blog.csdn.net/yanghanxiu/article/details/79366263
- 创建spring-context.xml项目
路径:/Users/XXX/Documents/某一/src/main/resources/spring-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 导入 RPC 客户端 -->
</beans>
5. 创建java目录下的test项目
冒烟:调通http、websocket、socketio
6. 单元测试模型
- 这些方法默认都是public可省略test = new GTest(
7. 创建Groovy Class例子 + 如何添加断言例子
@Test
void test() {
HTTPResponse result = request.POST("http://{host}:20071/api/rest/onLineInfosQuery", params)
grinder.logger.info("返回值: {}", new String(result.getData(), CharsetUtil.UTF_8))
String content = result.getText();
JSONObject obj = JSON.parse(content)
int code = obj.getIntValue("code")
if (code != 200) {
Assert.fail(new String(result.getData(), CharsetUtil.UTF_8))
} else {
Assert.assertTrue("接口pass", result.statusCode == 200)
}
}
8. 界面调通http-post接口
可通过http://{host}:8080/ngrinder/script/页面添加相应的参数生成Groovy脚本,cpoy到文件里。
9. 本地调整长链接例子:测开调通2个音视频业务接口:信令压测的话测试加入房间和推流
参数实例join_room / {"app_package_name":"com.XXX.apple.rtc","app_version":"1.0.8","brand":"iPhone","c_stream_id":"1623914219902","client_ip":"host","connect_state":0,"continent_code":"AP","country_code":"CN","cpu_arc":"arm64","cpu_model":"arm","device_model":"iPhone10,2","host":1,"is_ipv6":0,"isp":"ChinaUnicom","network_type":"wifi","room_id":"appid-L2391421682680422476618","sdk_version":1020196,"show":"all","sync_role":0,"system_version":"13.700000","timestamp":1623914219905,"user_id":"appid-H2391420624443596874139","user_source":"iOS"}
-
音视频长链接
压测就是socketio建立连接后,模拟客户端向服务端发送数据,服务端支持回调,校验服务端返回值。则一次压测执行成功。
步骤:
- 创建类实现父类接口,回调接口中申明这些回调状态
class JoinroomPress implements SocketIOCallback{
}
package com.project.performance.callback
interface SocketIOCallback {
/**
* 正确连接
*/
void connect(Object ...objects)
/**
* 连接断开
*/
void disconnect(Object ...objects)
/**
* 连接超时
*/
void connectTimeout(Object ...objects);
/**
* 连接超时
*/
void connectError(Object ...objects);
/**
* 回调事件监听
*/
void onEventMessage(Object ...objects);
}
- 主要是在建立长链接的同时对状态进行判断
重点:
client = IO.socket(url,options)
// 客户端建立长链接,几个状态判断
client.on(Socket.EVENT_CONNECT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接成功~",JSON.toJSONString(objects))
JoinroomPress.this.connect(objects)
}
})
client.on(Socket.EVENT_DISCONNECT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接失败~",JSON.toJSONString(objects))
JoinroomPress.this.disconnect(objects)
}
})
client.on(Socket.EVENT_CONNECT_TIMEOUT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接超时~",JSON.toJSONString(objects))
JoinroomPress.this.connectTimeout(objects)
}
})
// 添加业务的校验
client.on(EVENT_RECEIVE_JOIN_SUCCESS,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("接受服务端 joinSuccess 事件返回消息: {}", JSON.toJSONString(objects))
JoinroomPress.this.onEventMessage(objects)
}
} )
- 建立信令joinroom模版的例子
import cn.hutool.core.date.chinese.SolarTerms
import cn.hutool.core.util.RandomUtil
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.project.performance.callback.SocketIOCallback
import groovy.util.logging.Slf4j
import io.socket.client.IO
import io.socket.client.Socket
import io.socket.emitter.Emitter
import net.grinder.script.GTest
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import static net.grinder.script.Grinder.grinder
@Slf4j
@RunWith(GrinderRunner)
class JoinroomPress implements SocketIOCallback {
private static GTest test
private Socket client
private static final String url
private static IO.Options options
private JSONObject param
private static final String appId
private static final String roomId
private static final String EVENT_RECEIVE_JOIN_SUCCESS
private static final String EVENT_SEND_JOIN_SUCCESS
static {
url = "http://{host}:8883/octopus"
appId = "appid"
roomId = appId + "-" + RandomUtil.randomString(25)
EVENT_RECEIVE_JOIN_SUCCESS = "joinSuccess"
EVENT_SEND_JOIN_SUCCESS = "join_room"
}
@BeforeClass
static void beforeClass(){
test = new GTest(1, "调试joinroom信令")
options = new IO.Options()
String[] transports = new String[1]
transports[0] = "websocket"
options.transports = transports
grinder.logger.info("执行beforeClass~")
}
private Date date
@BeforeThread
void beforeThread(){
test.record(this, "test")
grinder.statistics.delayReports = true
param = new JSONObject()
client = IO.socket(url,options)
// 客户端建立长链接,几个状态判断
client.on(Socket.EVENT_CONNECT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接成功~",JSON.toJSONString(objects))
JoinroomPress.this.connect(objects)
}
})
client.on(Socket.EVENT_DISCONNECT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接失败~",JSON.toJSONString(objects))
JoinroomPress.this.disconnect(objects)
}
})
client.on(Socket.EVENT_CONNECT_TIMEOUT,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("建立连接超时~",JSON.toJSONString(objects))
JoinroomPress.this.connectTimeout(objects)
}
})
// 添加业务的校验
client.on(EVENT_RECEIVE_JOIN_SUCCESS,new Emitter.Listener(){
@Override
void call(Object... objects) {
grinder.logger.info("接受服务端 joinSuccess 事件返回消息: {}", JSON.toJSONString(objects))
JoinroomPress.this.onEventMessage(objects)
}
} )
client.connect()
// 传参数
param.put("c_stream_id", "appid")
param.put("app_id", appId)
param.put("connect_state", 0)
param.put("room_id", roomId)
param.put("user_id", appId + "-" + RandomUtil.randomString(33))
param.put("host", 1)
param.put("role", "participant")
param.put("show", "all")
param.put("user_source", "desktop")
param.put("sdk_version", 20002)
param.put("sync_role", 0)
param.put("app_package_name", "")
date = new Date()
}
@Before
void before(){
param.put("user_id", "appid-" + RandomUtil.randomString(33))
date.setTime(System.currentTimeMillis())
param.put("timestamp", date)
// socket是直接连接,音视频业务导致:就是在规定时间500ms,在500ms内如果建立连接就退出,如果没有就继续执行
long start = System.currentTimeMillis()
while (System.currentTimeMillis() - start < options.timeout){
if(client.connected()){
break
}
}
grinder.logger.info("执行before~")
}
@Test
void test(){
Assert.assertTrue("连接断开", client.connected())
param.put("timestamp", date)
grinder.logger.info("发送数据: {}", param.toJSONString())
client.emit(EVENT_SEND_JOIN_SUCCESS, param)
TimeUnit.SECONDS.sleep(1)
grinder.logger.info("执行test~")
}
@After
void after(){
if(client.disconnect()){
client.connect()
}
grinder.logger.info("执行after~")
}
@Override
void connect(Object... objects) {
}
@Override
void disconnect(Object... objects) {
client.connect()
Assert.fail("连接断开【" + JSON.toJSONString(objects) + "】")
}
@Override
void connectTimeout(Object... objects) {
Assert.fail(JSON.toJSONString(objects))
}
@Override
void connectError(Object... objects) {
Assert.fail(JSON.toJSONString(objects))
}
@Override
void onEventMessage(Object... objects) {
JSONObject obj = JSON.parseObject(JSON.toJSONString(objects[0]))
int code = obj.getIntValue("code")
Assert.assertTrue(JSON.toJSONString(objects), code == 200)
}
}
- IM TCP压测(IM的chat通信实现)
就是使用WS建立建立连接,调用客户端的方法,给服务端发送多种消息类型,服务端给callback回调(开发待添加),校验是否成功。
-
步骤:
-
导入jar压缩包依赖
-
pom.xml引入新增依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.65.Final</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.17.3</version>
</dependency>
- 建立连接 + 调用发送方法
@Test
void testImChat(){
String message = messagePre + RandomUtil.randomString(10)
boolean result = client.chat(targetUserId, messagePre + RandomUtil.randomString(10))
Assert.assertTrue("【" + userId + "】发送数据【" + message + "】到【" + targetUserId + "】失败", result)
}
- 完整代码逻辑
package com.project.performance.test.cmim
import cn.hutool.core.util.RandomUtil
import com.linkv.im.core.IMEventCallBack
import com.linkv.im.core.IMOnMessageCallback
import com.linkv.im.core.LinkVIMClient
import groovy.util.logging.Slf4j
import net.grinder.script.GTest
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.AfterThread
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit
import static net.grinder.script.Grinder.grinder
@Slf4j
@RunWith(GrinderRunner)
class IMChatTest implements IMOnMessageCallback, IMEventCallBack {
private static GTest test
private static String host
private static int port
private static String appId
private static String messagePre
@BeforeProcess
static void beforeProcess() {
test = new GTest(1, "压测 IM-Chat 私聊")
host = "{host}"
port = 10001
appId = "cccc_tob"
messagePre = "Press Test -- "
}
private LinkVIMClient client
private String userId
private String deviceId
private String targetUserId
@BeforeThread
void beforeThread() {
test.record(this, "testImChat")
grinder.statistics.delayReports = true
client = new LinkVIMClient(appId, host, port)
userId = "self" + RandomUtil.randomString(5)
deviceId = RandomUtil.randomString(10)
client.connection(this, this)
targetUserId = "1234"
}
@AfterThread
void afterThread(){
client = null
}
private boolean isLogin = false
@Before
void beforeTest(){
while (true){
if(isLogin){
break
}
TimeUnit.MILLISECONDS.sleep(10)
}
}
@Test
void testImChat(){
String message = messagePre + RandomUtil.randomString(10)
boolean result = client.chat(targetUserId, messagePre + RandomUtil.randomString(10))
Assert.assertTrue("【" + userId + "】发送数据【" + message + "】到【" + targetUserId + "】失败", result)
}
@After
void afterTest(){
if(!isLogin){
client.connection(this, this)
}
}
@Override
void callBack(String event, int status) {
if (event.equals("connetion") && status == 0) {
client.login(userId, deviceId)
} else if(event.equals("loginSendMessage") && status == 0){
isLogin = true
} else {
isLogin = false
client.connection(this, this)
}
}
@Override
void onEvent(String messageType, Long messageId, String content) {
if(messageType == "ack" && messageId != 0){
log.info("获取发送消息回执: {}", content)
Assert.assertTrue(content, content.contains(userId))
}
}
}
- 将调通的代码粘贴到前端脚本,执行使用即可。
3、问题解决记录
-
Maven需要加载,执行位置,爆红直接都加载即可
-
每调试一条case报错需要添加依赖
-
Maven依赖的版本号确定
查询地址:https://mvnrepository.com/
-
因为前端添加了Maven依赖,每次执行压测都是将依赖包打包传到代理服务器上去进行压测,所以有等待压测的状态。
-
本地也可使用python,对应Jpthon
-
因为音视频服务端的socket.io是2以下版本,不支持,才使用的Java语言,对应的Groovy。
-
了解压测report
1. 成功、失败统计
2. 均值
3. 峰值
4. 每秒执行的条数
- 将本机配置成一台Agent方法
1. 将ngrinder-agent-3.4.4-{host}-mzhuo.tar文件下载到本地
2. 解压之后:tar -zxvf ngrinder-agent-3.4.4-{host}-mzhuo.tar
3. 进入该目录:cd ngrinder-agent/
4. 本地启动进程:./run_agent.sh
5. 则在设置配置的页面,选择的Agent变为max:3
6. 执行关闭进程则恢复:./stop_agent.sh