loadrunner測試TCP協議伺服器效能
最近對伺服器的效能感興趣,於是開始研究了一陣子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
vuser_init中載入了程式所需要的DLL檔案,InnoVSSBASClient.dll為與伺服器連線的客戶端的DLL檔案,而ole32.dll為程式中的字串函式(比如strcpy等)需要載入的DLL檔案。#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; }
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測試程式碼在這裡可以下載。