谢邀!
今天㳀谈下测试脚本开发中,报文长度动态变动的问题。
可能小伙伴们刚接触性能测试时不太熟悉,到网上找各个资料,却觉得网上的方法可用,但不太方便,下面我整理一些常用方法,若小伙伴们觉得不太清楚,还麻烦回复我,我及时修订。
条件说明:
- 测试工具:LoadRunner11.0 & Jmeter5.0。
- 协议:TCP。
- 前提条件:报文开头含定长的字符串,表示报文内容的总长度,如:0004abcd,其中“0004”表示后面还有4个字符“abcd”表示报文内容。
- 约定:为描述方便,上述“0004”称为报文长度,“abcd”称为报文内容。
先说说LoadRunner(C语言实现):
1.一个TCP请求时。
思路:利用strlen函数,计算报文内容长度,把计算后的长度拼接报文内容前面。
一点碎碎念:网上常看见利用2个及以上同等长度的buffer或malloc/free函数来实现该思路,但我觉得存在优化的空间。
实现图解:
脚本示例:
#include "lrs.h"
Action()
{
int rc;
int length=0;
char lenOfhead[10];
char buf[4096];
// Step01:与入“报文内容”到Buffer
// 这里应判定报文内容长度是否大于4096,若实际报文不可能超出该长度可不判定
sprintf(buf+8,"%s",lr_eval_string(
"test,this is a message,<vargs>"
));
// Step02:计算“报文内容”长度
length += strlen(buf+8); --+8,在这里是预留的8位定长的“报文长度”字符
// Step03:格式化“报文内容”长度
// 这里需要注意的时sprintf函数会在结束时定入字符串的结束标标,即最后面多一个x00
// 这也是我单独使用一个小buffer存放格式化“报文长度”的原因
sprintf(lenOfhead,"%08d",length);
// 计算“总报文”的长度=“报文长度”+“报文长度”的长度(这里为8位定长字符),用于后面lrs_set_send_buffer函数发送TCP请求时使用
length += 8;
// Step04:完成报文拼接
memcpy(buf,lenOfhead,8*sizeof(char));
lr_output_message("buf:%s",buf);
lr_start_transaction("trans01");
rc=lrs_create_socket("socket1","TCP","RemoteHost=<IP_Port>",LrsLastArg);
if(rc!=0)
{
lr_error_message("An error occurred while opening the socket1,Error code:%d",rc);
return 0;
}
lrs_set_send_buffer("socket1",buf,length);
lrs_send("socket1","buf1",LrsLastArg);
lrs_set_recv_timeout(68,0);
lrs_receive_ex("socket1","buf10","StringTerminator=</service>","Mismatch=MISMATCH_CONTENT",LrsLastArg);
lrs_save_searched_string("socket1",NULL,"RET_CODE","LB=<data name="RET_CODE">","RB=</data>",1,0,-1);
if(strstr(lr_eval_string("<RET_CODE>"),">000000<")!=NULL)
{
lr_end_transaction("trans01", LR_AUTO);
}
else
{
lrs_save_searched_string("socket1",NULL,"RET_MSG","LB=<data name="RET_MSG">","RB=</data>",1,0,-1);
lr_convert_string_encoding(lr_eval_string("<RET_MSG>"),LR_ENC_UTF8,LR_ENC_SYSTEM_LOCALE,"RET_MSG_convet");
lr_error_message("trans01, RET_CODE:%s, RET_MSG_convet:%s",lr_eval_string("<RET_CODE>"),lr_eval_string("<RET_MSG_convet>"));
lr_end_transaction("trans01", LR_FAIL);
}
lrs_close_socket("socket1");
return 0;
}
2.存在多个TCP请求时。
背景:存在多个TCP请求时,上述处理过程堆在一起,可维护性和可读性大大降低。
思路:通过函数封装相同处理逻辑,进而增强可维护性和可读性。
捋一捋,我们需要实现:
- “报文内容”传入函数。
- 若每个TCP请求为一个事务点,函数则需要“事务名称”,为可选项。
- 若每个TCP请求返回,判定是否成功的逻辑不一样,则需传入断言函数,为可选项。
也就是说,我们需要三个参数实现上述过程。
目前尚未遇到第三种情况,偷个懒,暂不实现此需求。若有需求,可请参考C语言的函数指针的章节。
函数实现:
int nongfuSpring(char *transName, char *paper)
{
int rc;
int length=0;
char lenOfhead[10];
char buf[4096];
// Step01:与入“报文内容”到Buffer
sprintf(buf+8, "%s", lr_eval_string(paper));
// Step02:计算“报文内容”长度
length += strlen(buf + 8);
// Step03:格式化“报文内容”长度
sprintf(lenOfhead, "%08d", length);
// 计算“总报文”的长度
length += 8;
// Step4:完成报文拼接
memcpy(buf, lenOfhead, 8*sizeof(char));
lr_output_message("buf:%s", buf);
lr_start_transaction(transName);
rc=lrs_create_socket("socket1","TCP","RemoteHost=<IP_Port>",LrsLastArg);
if(rc!=0)
{
lr_error_message("%s,An error occurred while opening the socket1,Error code:%d",transName,rc);
return 1;
}
lrs_set_send_buffer("socket1",buf,length);
lrs_send("socket1","buf1",LrsLastArg);
lrs_set_recv_timeout(68,0);
lrs_receive_ex("socket1","buf10","StringTerminator=</service>","Mismatch=MISMATCH_CONTENT",LrsLastArg);
lrs_save_searched_string("socket1",NULL,"RET_CODE","LB=<data name="RET_CODE">","RB=</data>",1,0,-1);
if(strstr(lr_eval_string("<RET_CODE>"),">000000<")!=NULL)
{
lr_end_transaction(transName, LR_AUTO);
}
else
{
lrs_save_searched_string("socket1",NULL,"RET_MSG","LB=<data name="RET_MSG">","RB=</data>",1,0,-1);
lr_convert_string_encoding(lr_eval_string("<RET_MSG>"),LR_ENC_UTF8,LR_ENC_SYSTEM_LOCALE,"RET_MSG_convet");
lr_error_message("%s, RET_CODE:%s, RET_MSG_convet:%s, IDCARD_NO:%s, FILE_KEY_PIC:%s%s.jpg",transName,lr_eval_string("<RET_CODE>"),lr_eval_string("<RET_MSG_convet>"),lr_eval_string("<IDCARD_NO>"),lr_eval_string("<RChar>"),lr_eval_string("<RNum>"));
lr_end_transaction(transName, LR_FAIL);
lrs_close_socket("socket1");
return 2;
}
lrs_close_socket("socket1");
return 0;
}
调用示例:
Action()
{
int rc=0;
//第一次调用
rc=nongfuSpring("Reg",
"<?xml version="1.0" encoding="UTF-8"?>"
"<service>"
"<body>"
"<data name="FILE_KEY_PIC">"
"<field length="20" scale="0" type="string"><RChar><RNum>.jpg</field>"
"</data>"
"</body>"
"</service>"
);
//判定第一次调用是否成功
if(rc!=0){
rc=0;
return 0;
}
//第二次调用
rc=nongfuSpring("Compare_1_checkPerson",
"<?xml version="1.0" encoding="UTF-8"?>"
"<service>"
"<body>"
"<data name="FILE_KEY_CAMERA">"
"<field length="20" scale="0" type="string"><RChar><RNum>.jpg</field>"
"</data>"
"</body>"
"</service>"
);
return 0;
}
一点碎碎念:
int nongfuSpring(char *transName, char *paper)
该函数签名使用了可修改地字符指针,但我们只用到它的只读功能,按理函数签名应为:
int nongfuSpring(const char *transName, const char *paper)
但在LoadRunner11.0上编译不通过,目前不知道为什么,若小伙伴们知道为什么麻烦告诉我一声,谢谢。
LoadRunner先到这里,我们再说说Jmeter5.0.
-------------------------这是一个朴素的分隔线-------------------------
Jmeter5.0:
Jmeter TCP Sampler中有一个“Text to send”的文本框,存放待发送的内容,如下图:
我们可以看出,Jmeter上我们无法像LoadRunner那样自行编写代码来处理“动态报文长度”。
虽然Jmeter可通过“前置处理器"写脚本来解决该问题,但我觉得Jmeter为开源工具,或许可让Jmeter自动处理“报文长度”字符串,这样方便我们管理脚本,不用请求或者JMX文件中存在相同的代码。
捋捋,我们的出发点:
- 需要知道在哪里修改Jmeter源码,修改难度大吗?
- 修改前后的性能差异如何 ?
我们来解决上述问题:
1.需要知道在哪里修改Jmeter源码,修改难度大吗?
Step01:在Jmeter官网上下载Jmeter5.0的源码。
Step02:找出TCP Sampler的java实现。
Step03:修改修改。
通过查找:
TCP Sampler的java代码在这几个文件中:
浏览代码,找出他们的依赖关系为:
TCPSamplerc通过Jmeter内置的TCPClient来实现TCP的发送与接收,具体的Java代码在TCPClientImpl.java文件中。
报文的发送与接收,是通Socket的输出输入流来实现的,其中wirte方法为发送报文,如下图:
到这里就比较明确了,我们只需重写write方法就可以,开心 0 . 0
看看对应write方法的源码实现(TCPClientImpl.java文件73行):
@Override
public void write(OutputStream os, String s) throws IOException{
if(log.isDebugEnabled()) {
log.debug("WriteS: {}", showEOL(s));
}
os.write(s.getBytes(CHARSET));
os.flush();
}
上述函数逻辑比较简单,把字符串(String s)按给定字符写入的输出流(OutputStream os)。
要达到我们的目的,我们只需在os.write(s.getBytes(CHARSET)); 方法前,像LoadRunner那样拼接一个“报文长度”即可。
更改后的代码如下:
import org.apache.jmeter.protocol.tcp.sampler.TCPClientImpl;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.io.OutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class LengthPrefixedTCPClientImpl extends TCPClientImpl
{
private static final Logger log = LoggerFactory.getLogger(LengthPrefixedTCPClientImpl.class);
private static final String CHARSET = JMeterUtils.getPropDefault("tcp.charset", Charset.defaultCharset().name());
private final int lengthPrefixLen = JMeterUtils.getPropDefault("tcp.charlength.prefix.length", 8);
private final String prefixChar = JMeterUtils.getPropDefault("tcp.charlength.prefix.char", "0");
public LengthPrefixedTCPClientImpl()
{
super();
}
@Override
public void write(OutputStream os, String s) throws IOException{
// 获取原有待发送的字符串
byte[] sb = s.getBytes(CHARSET);
// 分配一个新的加上报文长度的buffer
ByteBuffer bb = ByteBuffer.allocate(sb.length + this.lengthPrefixLen);
// 获取“报文长度”的长度
String headStr = Integer.toString(sb.length);
int len = headStr.length();
// 像LoadRunner实现那样,分配一个存放“报文长度”的Buffer
byte[] prefixCharByte = this.prefixChar.getBytes(CHARSET);
if(prefixCharByte.length != 1)
{
log.error("prefixCharByte.length not is 1:"+prefixCharByte.length);
return;
}
// 格式化“报文长度”字符串
for(int i=0; i<this.lengthPrefixLen - len; i++)
{
bb.put(prefixCharByte);
}
// 拼接“报文长度”和“报文内容”
bb.put(headStr.getBytes(CHARSET));
bb.put(sb);
if(log.isDebugEnabled()){
log.debug("lengthOfWriteS: {}", this.showEOL(headStr));
log.debug("WriteS: {}", this.showEOL(s));
}
// 发送报文
os.write(bb.array());
os.flush();
}
private String showEOL(final String input) {
StringBuilder sb = new StringBuilder(input.length()*2);
for(int i=0; i < input.length(); i++) {
char ch = input.charAt(i);
if (ch < ' ') {
sb.append('[');
sb.append((int)ch);
sb.append(']');
} else {
sb.append(ch);
}
}
return sb.toString();
}
}
编译代码,把jar包放入Jmeter_home/lib/ext目录。
配置user.properties文件 或 Jmeter TCP Sampler界面配置 TCPClient classname 参数为上述的java类名(建议)。
使用我们新的代码,经测试我们的报文内容可以从“0000004abcd"变为"abcd",可不用再关注“报文长度”相关内容。
2.修改前后的性能对比如何?
已测试,但未做结果收集,待定。
非常保守估计在1000TPS的压力下,响应时间的变化<1ms。
更高的负载因测试机为我的笔记本硬件限制,遂放弃。