最近对服务器的性能感兴趣,于是开始研究了一阵子loadrunner如何做采用TCP协议交互的服务器的性能测试,对loadrunner不是很熟悉,所以一开始也走了一些弯路,现将学习的过程记录下来,为以后做参考吧。
TCP协议的服务器的性能测试,我想大家都会选择loadrunner的winsocket协议进行测试,我也是采用此种方式。下面将逐一记录如何使用此协议做性能测试。
1.采用DLL文件方式进行测试
由于与服务器连接的客户端的DLL文件我手头有,同时其对应的头文件也有,所以一开始试想的是采用loadrunner调用DLL文件的方式来实现性能测试。因为这种方式简单,不需了解很多loadrunner的winsocket的相关函数,容易上手。下面的代码即是采用DLL文件初步编写的代码:
vuser_init.c
vuser_init() { lrs_startup(257); lr_load_dll("InnoVSSBASClient.dll"); lr_load_dll("ole32.dll"); return 0; }
action.c
#include "lrs.h" #include "def.h" Action() { char* test; long handle; NET_CLIENT_INFO info; int isLogin; NET_CROSS_INFO crossInfo; NET_VEHCILE_PASS_INFO lrPassInfo = {0}; NET_VEHCILE_ALARM_INFO lrAlarmInfo = {0}; handle = InnoVSSBASClient_Init(NULL,NULL); strcpy(info.clientId,guid_gen()); strcpy(info.serverIP,"127.0.0.1"); info.serverPort = 9300; strcpy(info.username,"admin"); strcpy(info.password,"admin"); lr_start_transaction("tran_login_start"); isLogin = InnoVSSBASClient_CreateConn(handle,&info); if(isLogin==1){ lr_end_transaction("tran_login_start", LR_AUTO); lr_output_message(info.clientId); lr_output_message("登陆成功"); //InnoVSSBASClient_SetCallbackFunc(handle,InnoVSSBASClientCallback,1L); lr_start_transaction("tran_addcross_start"); strcpy(crossInfo.crossId,lr_eval_string("{crossId}")); InnoVSSBASClient_AddCrossInfo(handle,&crossInfo); lr_end_transaction("tran_addcross_start", LR_AUTO); }else{ lr_end_transaction("tran_login_start", LR_FAIL); lr_output_message(info.clientId); lr_output_message("登陆失败"); } while(1) { sleep(100); } return 0; }
vuser_init中加载了程序所需要的DLL文件,InnoVSSBASClient.dll为与服务器连接的客户端的DLL文件,而ole32.dll为程序中的字符串函数(比如strcpy等)需要加载的DLL文件。
action中则是性能测试的主体代码。本代码一共对两个操作:登录和添加路口信息做了事务监控。
采用DLL文件的方式针对测试简单的顺序的操作很适用,但是本客户端还有个功能是需要处理服务器实时传输的过车等信息的功能,即在测试服务器端功能的时候,还需要模拟出客户端的回调函数的功能,但是在loadrunner中没有找到定义回调函数的方式,于是不得不放弃这种简单的性能测试的方式。在此想向loadrunner的大牛问一下,如何在loadrunner中第一回调函数呢?
上面的方式不能真实的模拟客户端的情况,于是下面会记录采用loadrunner本身的winsocket函数进行测试。
2.采用loadrunner的winsocket函数做测试
我先上源码,然后逐一讲解:
def.h //本文件是外部文件,在此用于定义自定义函数
char* guid_gen(){ //生成GUID方法 typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[8]; } GUID; GUID m_guid; char buf[50]; char pNameStr[50]; CoCreateGuid(&m_guid); // 定义输出格式 //sprintf (buf, "{%08lX-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", // 大写 //sprintf (buf, "{%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}",// 小写 sprintf (buf, "%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",// 小写%08lx-%04x%04x-%02x%02x%02x%02x-%02x%02x%02x%02x m_guid.Data1, m_guid.Data2, m_guid.Data3, m_guid.Data4[0], m_guid.Data4[1], m_guid.Data4[2], m_guid.Data4[3], m_guid.Data4[4], m_guid.Data4[5], m_guid.Data4[6], m_guid.Data4[7]); //lr_save_string(buf, paramName); //sprintf(pNameStr,"{%s}",paramName); return lr_eval_string(buf); } char* join(char *s1, char *s2) { char *result = (char*)malloc(strlen(s1)+strlen(s2)+1);//+1 for the zero-terminator //in real code you would check for errors in malloc here if (result == NULL) exit (1); strcpy(result, s1); strcat(result, s2); return result; } // 字符串替换函数. // 能替换所有的要替换的字符串,被替换的字符串和替换的字符串不一定一样长. // pInput - 输入字符串. // pOutput - 输出字符串, 要保证足够的空间可以存储替换后的字符串. // pSrc - 要被替换的子字符串, 比如%user% // pDst - 要替换成的字符串, 比如user1 // 注意:以上的字符串均要以'\0'结尾. // void Substitute(char *pInput, char *pOutput, char *pSrc, char *pDst) { char *pi, *po, *p; int nSrcLen, nDstLen, nLen; // 指向输入字符串的游动指针. pi = pInput; // 指向输出字符串的游动指针. po = pOutput; // 计算被替换串和替换串的长度. nSrcLen = strlen(pSrc); nDstLen = strlen(pDst); // 查找pi指向字符串中第一次出现替换串的位置,并返回指针(找不到则返回null). p = (char*)strstr(pi, pSrc); if(p) { // 找到. while(p) { // 计算被替换串前边字符串的长度. nLen = (int)(p - pi); // 复制到输出字符串. memcpy(po, pi, nLen); memcpy(po + nLen, pDst, nDstLen); // 跳过被替换串. pi = p + nSrcLen; // 调整指向输出串的指针位置. po = po + nLen + nDstLen; // 继续查找. p = (char*)strstr(pi, pSrc); } // 复制剩余字符串. strcpy(po, pi); } else { // 没有找到则原样复制. strcpy(po, pi); } } /* @param char* dest 目标串,也就是替换后的新串 * @param const char* src 源字符串,被替换的字符串 * @param const char* oldstr 旧的子串,将被替换的子串 * @param const char* newstr 新的子串 * @param int len 将要被替换的前len个字符*/ char *strreplace(char *dest, char *src, const char *oldstr, const char *newstr, size_t len) { //子串位置指针 char *needle; //临时内存区 char *tmp; //如果串相等,则直接返回 lr_output_message("newStr:%s",newstr); if(strcmp(oldstr, newstr)==0) { return src; } //把源串地址赋给指针dest,即让dest和src都指向src的内存区域 dest = src; //如果找到子串, 并且子串位置在前len个子串范围内, 则进行替换, 否则直接返回 while((needle = (char *) strstr(dest, oldstr)) && (needle -dest <= len)) { //分配新的空间: +1 是为了添加串尾的'\0'结束符 tmp=(char*)malloc(strlen(dest)+(strlen(newstr)-strlen(oldstr))+1); //把src内的前needle-dest个内存空间的数据,拷贝到arr strncpy(tmp, dest, needle-dest); //标识串结束 tmp[needle-dest]='\0'; //连接arr和newstr, 即把newstr附在arr尾部, 从而组成新串(或说字符数组)arr strcat(tmp, newstr); //把src中 从oldstr子串位置后的部分和arr连接在一起,组成新串arr strcat(tmp, needle+strlen(oldstr)); //把用malloc分配的内存,复制给指针retv dest = (char *)strdup(tmp); //释放malloc分配的内存空间 free(tmp); } return dest; }
本文件包含了两种功能的函数:一个是如何生成guid,一个是如何替换字符串中的子串。
action.c
/********************************************************************* * Created by Mercury Interactive Windows Sockets Recorder * * Created on: Tue Dec 30 16:04:06 *********************************************************************/ #include "lrs.h" #include "def.h" Action() { int sendLoginCount=0,sendCrossCount=0; int loginIndex,loginIndex2; char* clientId = guid_gen(); char clientId2[100]; char* clientId3; int clientIdlen; char* loginSrc = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<Parament>\n" " <ClientId>$ClientId</ClientId>\n" " <ServerIP>$IP</ServerIP>\n" " <ServerPort>$Port</ServerPort>\n" " <Username></Username>\n" " <Password></Password>\n" "</Parament>"; char* loginStr; int loginStrLen; char* loginStrLenHex; char loginStrLenStr[5]; char send_loginHeader[100]="\\x12$Len\\x00\\x010"; char* send_loginHeaderf; char send_loginStr[1500]=""; //添加路口相关字符串 char* crossSrc= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<Parament>\n" " <ClientId>$ClientId</ClientId>\n" " <CrossId>$CrossId</CrossId>\n" "</Parament>"; char* send_addCrossHeader = "\\x12$Len\\x00\\x02"; char* crossId = lr_eval_string("<db_crossId>"); char* crossStr; char send_crossStr[1700]; char crossStrLenStr[5]; int crossStrLen; char* send_addCrossHeaderf; int crossAddIndex,crossAddIndex2; strcpy(clientId2,lr_eval_string(clientId)); clientId3 = clientId; //登陆数据 loginStr = strreplace(loginStr,loginSrc,"$ClientId",clientId,strlen(loginSrc)); loginStr = strreplace(loginStr,loginStr,"$IP","127.0.0.1",strlen(loginStr)); loginStr = strreplace(loginStr,loginStr,"$Port","9300",strlen(loginStr)); lr_output_message("loginStr:%s",loginStr); loginStrLen = strlen(loginStr)+1; //lr_output_message("loginStrLen:%d",loginStrLen); //itoa(loginStrLen,loginStrLenStr,16); sprintf(loginStrLenStr, "%X", loginStrLen); //lr_output_message("loginStrLenStr:%s",loginStrLenStr); if(strlen(loginStrLenStr)==2) { char tmpH[5]; strcpy(tmpH,loginStrLenStr); strcpy(loginStrLenStr,"\\x00\\x00\\x00\\x"); strcat(loginStrLenStr,tmpH); }else{ char tmpH[5]; char tmpD[5]; strcpy(tmpH,loginStrLenStr); strcpy(tmpH+1,"\0"); strcpy(tmpD,loginStrLenStr+strlen(loginStrLenStr)-2); strcpy(loginStrLenStr,"\\x00\\x00\\x0"); strcat(loginStrLenStr,tmpH); strcat(loginStrLenStr,"\\x"); strcat(loginStrLenStr,tmpD); //lr_output_message("tmpH:%s",tmpH); //lr_output_message("tmpD:%s",tmpD); } //lr_output_message("loginStrLenStr:%s",loginStrLenStr); send_loginHeaderf = strreplace(send_loginHeaderf,send_loginHeader,"$Len",loginStrLenStr,strlen(send_loginHeader)); //lr_output_message("send_loginHeader:%s",send_loginHeaderf); strcpy(send_loginStr,send_loginHeaderf); //lr_output_message("send_loginStr:%s",send_loginStr); for(loginIndex=0,loginIndex2=strlen(send_loginStr);loginIndex<strlen(loginStr);loginIndex++,loginIndex2++) { char loginHex[5]; sprintf(loginHex,"\\x%.2X",loginStr[loginIndex]); //strcat(send_loginBody,loginHex); strcat(send_loginStr+loginIndex2,loginHex); } strcat(send_loginStr+loginIndex2,"\\x0A"); lr_output_message("send_loginStr:%s",send_loginStr); //创建TCP连接 lr_start_transaction("login"); lrs_create_socket("socket0", "TCP", "LocalHost=0", "RemoteHost=127.0.0.1:1234",LrsLastArg); for(sendLoginCount = 0;sendLoginCount < 10;sendLoginCount++) { //lr_output_message("send_loginStr:%s",send_loginStr); lr_output_message("sendLoginCount:%d",sendLoginCount); lrs_set_send_buffer("socket0",send_loginStr,strlen(send_loginStr)); lrs_send("socket0","buf0",LrsLastArg); lrs_receive("socket0", "buf1", LrsLastArg); { char* login_recv; int login_recvlen; int i; lrs_get_last_received_buffer("socket0",&login_recv,&login_recvlen); if(login_recvlen!=0) { lr_end_transaction("login", LR_AUTO); lr_output_message("%s",login_recv+15); //添加路口 lr_output_message("clientId3:%s",lr_eval_string(clientId2)); crossStr = strreplace(crossStr,crossSrc,"$ClientId",clientId3,strlen(crossSrc)); crossStr = strreplace(crossStr,crossStr,"$CrossId",crossId,strlen(crossStr)); lr_output_message("crossStr:%s",crossStr); crossStrLen = strlen(crossStr)+1; sprintf(crossStrLenStr,"%X",crossStrLen); if(strlen(crossStrLenStr)==2) { char tmpH[5]; strcpy(tmpH,crossStrLenStr); strcpy(crossStrLenStr,"\\x00\\x00\\x00\\x"); strcat(crossStrLenStr,tmpH); }else{ char tmpH[5]; char tmpD[5]; strcpy(tmpH,crossStrLenStr); strcpy(tmpH+1,"\0"); strcpy(tmpD,crossStrLenStr+strlen(crossStrLenStr)-2); strcpy(crossStrLenStr,"\\x00\\x00\\x0"); strcat(crossStrLenStr,tmpH); strcat(crossStrLenStr,"\\x"); strcat(crossStrLenStr,tmpD); //lr_output_message("cross_tmpH:%s",tmpH); //lr_output_message("cross_tmpD:%s",tmpD); } //lr_output_message("crossStrLenStr:%s",crossStrLenStr); send_addCrossHeaderf = strreplace(send_addCrossHeaderf,send_addCrossHeader,"$Len",crossStrLenStr,strlen(send_addCrossHeader)); //lr_output_message("send_addCrossHeaderf:%s",send_addCrossHeaderf); strcpy(send_crossStr,send_addCrossHeaderf); //lr_output_message("send_crossStr:%s",send_crossStr); for(crossAddIndex=0,crossAddIndex2=strlen(send_crossStr);crossAddIndex<strlen(crossStr);crossAddIndex++,crossAddIndex2++) { char crossHex[5]; sprintf(crossHex,"\\x%.2X",crossStr[crossAddIndex]); //strcat(send_loginBody,loginHex); strcat(send_crossStr+crossAddIndex2,crossHex); } strcat(send_crossStr+crossAddIndex2,"\\x0A"); lr_output_message("send_crossStr:%s",send_crossStr); //lr_output_message("send_loginStr:%s",send_loginStr); lr_start_transaction("addCross"); for(sendCrossCount=0;sendCrossCount<10;sendCrossCount++) { lr_output_message("send_crossStr:%s",send_crossStr); lr_output_message("sendCrossCount:%d",sendCrossCount); lrs_set_send_buffer("socket0",send_crossStr,strlen(send_crossStr)); lrs_send("socket0","buf0",LrsLastArg); lrs_receive("socket0","buf1",LrsLastArg); { char* cross_recv; int cross_recvlen; lrs_get_last_received_buffer("socket0",&cross_recv,&cross_recvlen); if(cross_recvlen!=0) { lr_end_transaction("addCross", LR_AUTO); lr_output_message("cross_recv:%s",cross_recv+15); break; }else{ //lr_end_transaction("addCross", LR_FAIL); } } } if(sendCrossCount>10) { lr_end_transaction("addCross", LR_FAIL); } break; }else{ //lr_end_transaction("login", LR_FAIL); } } } if(sendLoginCount>10) { lr_end_transaction("login", LR_FAIL); } while(1) { char* recv; int recvlen; lrs_receive("socket0","buf3",LrsLastArg); lrs_get_last_received_buffer("socket0",&recv,&recvlen); if(recvlen!=0) { lr_output_message("recv:%s",recv+15); }else{ } } lrs_close_socket("socket0"); return 0; }
上面代码的具体业务就不介绍了,在此我介绍一下在该片代码中应该注意的地方:
1)本代码模拟了客户端所发的16进制的数据,在此提一下字符、字符串如何转换成loadrunner中的16进制的字符串。
在loadrunner中16进行的字符采用前面加’\x‘的方式表示,如16进制0xAB,则在loadrunner中需要表示为‘\xAB‘;
整形转为16进制字符串:sprintf(loginStrLenStr, "%X", loginStrLen);
16进制字符串转换为loadrunner中认可的16进制串:
for(loginIndex=0,loginIndex2=strlen(send_loginStr);loginIndex<strlen(loginStr);loginIndex++,loginIndex2++) { char loginHex[5]; sprintf(loginHex,"\\x%.2X",loginStr[loginIndex]); //strcat(send_loginBody,loginHex); strcat(send_loginStr+loginIndex2,loginHex); }
本部分使用的是本方法,即将字符串“abcd"的的每个字符逐一转换成loadrunner认可的16进制串(如:字符”a"转换成"\x61“)
该方法是个笨方法,不知在loadrunner中有没有自带的函数做字符串的16进制转换呢?
2)loadrunner做winsocket测试的基本步骤:
/********************************************************************* * Created by Mercury Interactive Windows Sockets Recorder * * Created on: Mon Dec 29 09:01:03 *********************************************************************/ #include "lrs.h" Action() { int i; char *buffer;//定义字符指针 int numberOfBytes;//定义int型变量保存长度 //这是第一步initializes a socket lrs_create_socket("socket0", "TCP", "LocalHost=0", "RemoteHost=127.0.0.1:1234",LrsLastArg); lr_start_transaction("send"); //这里是第二步,通过建立的socket1将buf1中的数据发送给远端MM-7QL3Z0JYUJN6用户,端口2425 lrs_send("socket0", "buf1", LrsLastArg); //输出缓冲区数据大小 lrs_send("socket0", buffer, LrsLastArg); //从buf2中接收返回的数据 lrs_receive("socket0", "buf2", LrsLastArg); //取得缓冲区数据 lrs_get_buffer_by_name("buf2", &buffer, &numberOfBytes); //输出缓冲区数据大小 lr_output_message("The buffer's size is: %d/n", numberOfBytes); lr_output_message(buffer); lr_end_transaction("send", LR_AUTO); //第三步关闭释放socket连接 lrs_close_socket("socket0"); return 0; }
上面的代码的注释很明确了,不过需要注意一点的是,loadrunner中lrs_send中的缓存的buf需要在data.ws中定义,不能是程序中定义的字符串。
data.ws
;WSRData 2 1 send buf0 recv buf1 101 recv buf2 210 recv buf3 300 -1
3)对winsocket编程的一些函数的解释
①lrs_set_send_buffer("socket0",send_loginStr,strlen(send_loginStr));
lrs_set_send_buffer将程序中定义的字符串放入data.ws第一个定义的send bufx中,如上面的data.ws中定义的为buf0,则是将其方式buf0中,不管调用多少次,都是放入到buf0中。
②lrs_receive("socket0", "buf1", LrsLastArg);
lrs_get_last_received_buffer("socket0",&login_recv,&login_recvlen);
buf1定义的长度与实际接收的长度不一致没关系,loadrunner只会在输出中输出一个警告信息,但是不会影响实际接收的数据。警告信息为: Mismatch in buffer‘s length (expected 101 bytes, 222 bytes actually received, difference in 121 bytes)
该loadrunner测试代码在这里可以下载。