Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页: 9ilk
(๑•́ ₃ •̀๑) 文章专栏: 项目
本篇博客是对仿muduo的高性能服务器这个项目的延续,主要对项目进行一些测试,如性能测试、长连接测试、超时连接测试、错误请求测试、连续同时多条请求测试、大文件传输测试、业务处理超时测试等。
长链接测试
主要目的是测试服务端是否能维护长链接,测试方案是创建一个客户端持续给服务器发送数据,直到超时时间,查看是否会释放连接,不释放才是正常的,这里我们设置超时时间是10s。
服务端代码:
cpp
#include"http.hpp"
#define WWWROOT "./wwwroot/"
//回显
string RequestStr(const HttpRequest& req)
{
stringstream ss;
ss << req._method << " " << req._path << " " << req._version << "\r\n";
for(auto& it : req._params) //查询字符串
{
ss << it.first << "=" << it.second << "\r\n";
}
for(auto& it:req._headers)
{
ss << it.first << ":" << it.second << "\r\n";
}
ss << "\r\n";
ss << req._body;
return ss.str();
}
void Hello(const HttpRequest& req,HttpResponse* rsp)
{
DBG_LOG("come hello");
rsp->SetContent(RequestStr(req),"text/plain");
sleep(15);
}
void Login(const HttpRequest& req,HttpResponse* rsp)
{
DBG_LOG("come login");
rsp->SetContent(RequestStr(req),"text/plain");
}
void PutFile(const HttpRequest& req,HttpResponse* rsp)
{
DBG_LOG("come PutFile");
// rsp->SetContent(RequestStr(req),"text/plain");
string pathname = WWWROOT + req._path;
cout << "pathname: " << pathname << endl;
cout << "body:" << req._body << endl;
Util::WriteFile(pathname,req._body);
}
void DeleteFile(const HttpRequest& req,HttpResponse* rsp)
{
DBG_LOG("come DeleteFile");
rsp->SetContent(RequestStr(req),"text/plain");
}
int main()
{
HttpServer server(8085);
server.SetThreadCount(3);
server.SetBaseDir(WWWROOT);//设置静态资源,告诉服务器有静态资源请求到来,需要到哪里去寻找资源文件
server.Get("/hello",Hello);
server.Post("/login",Login);
server.Put("/1234.txt",PutFile);
server.Delete("/1234.txt",DeleteFile);
server.EnableInactiveRelease(10);
server.Listen();
return 0;
}
客户端代码:
cpp
#include"../source/server.hpp"
//长连接测试
//创建一个客户端持续给服务器发送数据,直到超时时间看看是否正常,这里我们设置连接超时时间为10s
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
while(1)
{
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
sleep(3);
}
cli_sock.Close();
}
现象:

超时连接测试
主要测试方案是:创建一个客户端给服务器发送一次数据之后就不再发送了,查看服务器是否会正常的释放超时连接。
客户端代码:
cpp
#include"../source/server.hpp"
//超时连接测试
// 创建一个客户端,给服务器发送一次数据后,不动了,查看服多器是否会正常的超时关闭连接
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
while(1)
{
sleep(15);
}
cli_sock.Close();
}
现象:

错误请求测试
主要测试方案是:告诉服务器要发送1024字节的数据,但是实际发送的数据不足1024字节,查看服务器的处理结果。如果数据只发送一次,服务器将会得不到完整的请求,就不会进行业务处理,客户端就得不到响应,应该看到的现象是连接超时关闭。如果连着给服务器发送多次小的请求,服务器会将后边的请求当做前边请求的正文进行处理,而后边处理的时候就有可能会因为处理错误而关闭连接。
测试1:只发送一次数据
cpp
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:100\r\n\r\nhello";
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
while(1)
{
sleep(1);
}
cli_sock.Close();
}
现象:

测试2:发送多次小的请求,会因为把后面请求当前正文而处理错误而关闭连接
cpp
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:100\r\n\r\nhello";
// assert(cli_sock.Send(str.c_str(),str.size())!=-1);
// char buf[1024] = {0};
// assert(cli_sock.Recv(buf,1023));
// DBG_LOG("[%s]",buf);
while(1)
{
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
sleep(3);
}
cli_sock.Close();
}
现象:

连续发送多次请求测试
主要测试方案是发送一次数据时,数据内包含了多条HTTP请求,看服务器是否处理正确。
客户端:
cpp
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
str += "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
str += "GET /hell0 HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
while(1)
{
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
sleep(12);
}
cli_sock.Close();
}
现象:

大文件传输测试
先生成一个大文件:

注册Put请求处理回调:将请求的正文内容写入新文件1234.txt:
cpp
void PutFile(const HttpRequest& req,HttpResponse* rsp)
{
// DBG_LOG("come PutFile");
// rsp->SetContent(RequestStr(req),"text/plain");
string pathname = WWWROOT + req._path;
Util::WriteFile(pathname,req._body);
}
客户端将生成的hello.txt内容作为正文传输给服务器生成新文件1234.txt
cpp
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string req = "PUT /1234.txt HTTP/1.1\r\nConnection:keep-alive\r\n";
string body;
Util::ReadFile("./hello.txt",&body);
req += "Content-Length:"+ to_string(body.size()) + "\r\n\r\n"
assert(cli_sock.Send(req.c_str(),req.size())!=-1);
assert(cli_sock.Send(body.c_str(),body.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
sleep(3);
cli_sock.Close();
}
现象:1234.txt上传到服务器

我们可以使用md5sum的值比较我们传输的文件和源文件的内容是否相同:

业务处理超时测试
当服务器达到性能瓶颈,在一次业务处理中花费了太长时间,超过了服务器设置的非活跃连接销毁时间,这会导致其他的 连接也被连累超时,因为他们的事件处理无法得到及时处理,也就无法刷新活跃度,导致连累其他连接被超时释放假设现在1、2、3、4、5描述符事件就绪,在处理1号描述符事件时花费了15s才完成,而非活跃连接的超时时间是10s,这会导致超时,其他描述符长时间没有刷新活跃度而超时:
- 如果接下来的2345描述符都是通信连接描述符,如果都就绪了,则并不影响,因为接下来就会进行处理并刷新活跃度。
- 如果接下来的2号描述符是定时器事件描述符,定时器触发超时,执行定时任务(定时任务就是执行Release接口释放连接),就会将345号描述符给释放调用,这时候 一旦345描述符对应的连接被释放,接下来在处理345事件事就会导致程序崩溃(内存访问错误),因此在这时,本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候再去释放,应该等本次所有就绪事件连接的事件处理完毕再去释放。(毕竟我们的EventLoop是先对就绪事件处理调用HandlerEvent,再调用RunAllTasks执行任务队列中任务)
Connection模块Release()应该将ReleaseInLoop()压入线程池
cpp
void Release()
{
_loop->QueueInLoop(std::bind(&Conncetion::ReleaseInLoop,this));
}
之前在调用HandlerEvent时,我们担心因为有些事件会释放连接造成内存非法访问而把任意事件回调放在它们各自的回调之前执行,但是由于我们上面将释放连接操作压入任务池不会直接释放,所以我们不用担心这个问题了:
cpp
void HandlerEvent() //事件处理:一旦链接事件触发就调用该函数,自己触发了什么事件如何处理由自己决定
{
if(_revnts & EPOLLIN || _revnts & EPOLLRDHUP || _revnts & EPOLLPRI) //断开链接认为要求上层读取数据,关闭链接后没有数据发送了
{
if(_read_callback) _read_callback();
}
//有可能链接释放的事件,一次只处理一个因此使用else if
if(_revnts & EPOLLOUT)
{
if(_write_callback) _write_callback();
}
else if(_revnts & EPOLLERR)
{
if(_error_callback) _error_callback();
}
else if(_revnts & EPOLLHUP)
{
if(_close_callback) _close_callback();
}
if(_any_callback) _any_callback();
}
经过上面的调整,但服务器处理一个客户端的请求业务处理时间过长的话,处理过程中不能直接对连接进行释放,而应该是事件都处理完才对连接进行释放:
cpp
#include"../source/server.hpp"
//业务超时测试
int main()
{
signal(SIGCHLD,SIG_IGN);
for(int i = 0 ; i < 10 ;i++)
{
pid_t pid = fork();
if(pid < 0)
{
DBG_LOG("Fork Error");
return -1;
}
else if(pid == 0)
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8085,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
string str = "GET /hello HTTP/1.1\r\nConnection:keep-alive\r\nContent-Length:0\r\n\r\n";
while(1)
{
assert(cli_sock.Send(str.c_str(),str.size())!=-1);
char buf[1024] = {0};
assert(cli_sock.Recv(buf,1023));
DBG_LOG("[%s]",buf);
}
cli_sock.Close();
exit(0);
}
}
while(1)
{
sleep(1);
}
}
现象:


性能压力测试
本次性能测试采用的是hey,hey是一款使用Golang开发的HTTP压力测试工具,它的主要功能是模拟大量的并发请求,以测试服务器的性能和和稳定性。
常见参数:
- -n 请求次数。默认为 200。
- -c 并发工作线程数。请确保总请求数不小于并发数量。默认为 50。
- -q 每个工作线程的查询每秒限制(QPS)。默认不设限制。
- -z 应用发送请求的持续时间。达到指定时间后,应用将停止并退出。如果指定了该选项,则忽略-n选项。示例:-z 10s -z 3m。
本次测试环境:服务器和客户端都是4核4G带宽4M的虚拟机,按照并发量1000、5000、10000 、50000的维度测试:(并发量极限在几万)
