目录
1 搭建一个基础的HTTP服务器
2 长连接测试
3 测试错误报文的处理
4 测试业务处理耗时超过超时时间的处理
5 测试同时收到多条正常请求
6 大文件传输测试
7 压力测试
1 搭建一个基础的HTTP服务器
在这个部分,我们需要搭建一个最简单的HTTP服务器,其中最主要的就是设置几个功能性请求的方法,我们简单一点,就为每一个方法设置一个正则匹配式,一个处理方法。
不过具体的业务逻辑我们都设置成一样的,没必要搞太复杂,我们的业务逻辑就是返回客户端发回来的HTTP请求。
首先,我们的服务器需要设置网页根目录,以及根目录下需要有一个index.html 文件,也就是我们我们用一个网上找的代码:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>网站首页一</title><style>* {margin: 0;padding: 0;}html {height: 100%;}body {height: 100%;}.container {height: 100%;background-image: linear-gradient(to right, #fbc2eb, #a6c1ee);}.login-wrapper {background-color: #fff;width: 358px;height: 588px;border-radius: 15px;padding: 0 50px;position: relative;left: 50%;top: 50%;transform: translate(-50%, -50%);}.header {font-size: 38px;font-weight: bold;text-align: center;line-height: 200px;}.input-item {display: block;width: 100%;margin-bottom: 20px;border: 0;padding: 10px;border-bottom: 1px solid rgb(128, 125, 125);font-size: 15px;outline: none;}.input-item::placeholder {text-transform: uppercase;}.btn {text-align: center;padding: 10px;width: 100%;margin-top: 40px;background-image: linear-gradient(to right, #a6c1ee, #fbc2eb);color: #fff;}.msg {text-align: center;line-height: 88px;}a {text-decoration-line: none;color: #abc1ee;}</style>
</head>
<body>
<div class="container"><form action = "/login" method="get"><div class="header">登录页面</div><div class="form-wrapper"><input type="text" name="username" placeholder="用户名" class="input-item"><input type="password" name="password" placeholder="密码" class="input-item"><input type="submit" value="提交" name="submit"></div><div class="msg">没有账号?<a href="http://baidu.com">注册账号</a></div></form>
</div>
</body>
</html>
虽然我也不是很能看懂,但是无所谓,能用就行。
#define BASEDIR "./wwwroot" //网页根目录
当前目录结构https://i-blog.csdnimg.cn/direct/b6b4aa3fe6a443f6bf58eaeb01ec75bc.png" width="1200" />
然后就是设置几个方法:
//将req转换为一个string对象,充当返回的响应的正文
std::string RequestToStr(const HttpRequest& req)
{std::ostringstream os;os<<req._method<<" "<<req._path<<" "<<req._version<<"\r\n"; //参数另外放//参数for(auto& p :req._params){os<<p.first<<"="<<p.second<<"\r\n";}//头部字段for(auto&p:req._headers){os<<p.first<<": "<<p.second<<"\r\n";}os<<"\r\n";os<<req._body;return os.str();
}void HandlerGet(const HttpRequest& req , HttpResponse& resp)
{//将请求设置为响应的正文std::string s = RequestToStr(req);s += "\r\nGET\r\n"; //加一点标志resp._body = s;resp.AddHeader("Content-Type","text/plain");
}void HandlerPost(const HttpRequest& req , HttpResponse& resp)
{//将请求设置为响应的正文std::string s = RequestToStr(req);s += "\r\nPOST\r\n"; //加一点标志resp._body = s;resp.AddHeader("Content-Type","text/plain");
}
void HandlerHead(const HttpRequest& req , HttpResponse& resp)
{//将请求设置为响应的正文std::string s = RequestToStr(req);s += "\r\nHEAD\r\n"; //加一点标志resp._body = s;resp.AddHeader("Content-Type","text/plain");
}
void HandlerPut(const HttpRequest& req , HttpResponse& resp)
{//将请求设置为响应的正文std::string s = RequestToStr(req);s += "\r\nPUT\r\n"; //加一点标志resp._body = s;resp.AddHeader("Content-Type","text/plain");
}
void HandlerDelete(const HttpRequest& req , HttpResponse& resp)
{//将请求设置为响应的正文std::string s = RequestToStr(req);s += "\r\nDELETE\r\n"; //加一点标志resp._body = s;resp.AddHeader("Content-Type","text/plain");
}
这几个方法其实都是一样的,就是将请求作为相应正文,然后再最后添加了一点标记。
然后就是main函数的设置
int main()
{HttpServer server(8080);server.EnableInactiveRelease(10); //十秒超时释放server.SetBasePath(BASEDIR);server.SetThreadCount(2);server.Get(std::regex("/get"),HandlerGet);server.Post(std::regex("/post"),HandlerPost);server.Put(std::regex("/put"),HandlerPut);server.Delete(std::regex("/delete"),HandlerDelete);server.Head(std::regex("/head"),HandlerHead);server.Start();return 0;
}
这时候我们可以使用我们的浏览器充当客户端来看一下访问网站首页:
https://i-blog.csdnimg.cn/direct/23eb431732bc42b6b2440504fdb6e9af.png" width="1200" />
然后再访问一个不存在的资源
https://i-blog.csdnimg.cn/direct/5bfbace5d1e7429ebab19b493fea800f.png" width="1200" />
然后就是功能性请求,功能性请求我们是用浏览器就不好查看了,因为我们的业务逻辑中返回的是请求的报文本身,那么我们可以使用 postman 这个工具来进行测试。
首先是 GET 方法请求get路径
=https://i-blog.csdnimg.cn/direct/257ab6ba91764caaa9ae154b94df9c86.png" width="1200" />
我们能看到收到的响应的正文就是我们发出去的报文本身
再测试一下其他的方法:
post:
https://i-blog.csdnimg.cn/direct/70a47234032a44298eb95293b2e2d708.png" width="1200" />
delete:
https://i-blog.csdnimg.cn/direct/497d149a5d634617a89c4a960fbce26f.png" width="1200" />
put:
https://i-blog.csdnimg.cn/direct/4e3100e94d3c444f9f6f160a6724b099.png" width="1200" />
head方法我们就不好测试了,返回的响应格式会有一些问题,因为HEAD请求的响应实际上是不能包含正文的,或许我们可以改一下上面的HandlerHead方法,我们让HEAD方法中什么也不做,不过这时候实验现象就不明显了。
https://i-blog.csdnimg.cn/direct/0ea0c3a66e58469499e8320d8d3045ce.png" width="1200" />
不管怎么说,我们的测试表明了服务器的处理逻辑是没有问题的,能够执行用户设置的功能方法。
2 长连接测试
长连接测试很简单,为了方便观察现象,我们自己写一个客户端,自己手写HTTP请求。
// 长连接测试 1:创建一个客户端,连续多次发送HTTP请求(携带长连接信息),
//我们观察服务器是否会在每一次请求处理完之后关闭连接再重新建立
int main()
{Socket cli;cli.CreatClientSocket("127.0.0.1",8080);int n = 3; //发送三次请求,每一次间隔三秒,观察中间是否会关闭std::string req = "GET /get HTTP/1.1\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"; //简单的HTTP请求while(n--) {int ret = cli.Send(req.c_str(),req.size());if(ret < 0) ERROR_LOG("Send Error");char buf[1024] = {0};ret = cli.Read(buf,1023); //阻塞读取if(ret < 0) ERROR_LOG("Send Error");std::cout<<buf<<std::endl;sleep(3);}while(1) sleep(1);//观察最后是否正常超时关闭return 0;
}
https://i-blog.csdnimg.cn/direct/ae08fc5bb69547a799d40b792b18d045.png" width="1200" />
从我们的测试结果上来看,长连接测试是没有问题的。
那么我们再来测试一下短连接,我们只发送一次请求报文,请求中设置短连接属性,我们观察服务器能否在处理完之后直接关闭该连接。
// 短连接测试 1:创建一个客户端,发送一次请求(短连接请求)之后不挂断,观察服务器是否在处理完该请求之后立马关闭连接int main()
{Socket clt;clt.CreatClientSocket("127.0.0.1",8080);std::string req = "GET /get HTTP/1.1\r\nContent-Length: 0\r\nC\r\n"; //简单的HTTP请求int ret = clt.Send(req.c_str(),req.size());if(ret < 0) ERROR_LOG("Send Error");char buf[1024] = {0};ret = clt.Read(buf,1023);if(ret < 0) ERROR_LOG("Send Error");std::cout<<buf<<std::endl;while(1) sleep(1); //客户端不进行挂断return 0;
}
https://i-blog.csdnimg.cn/direct/555e199a72b84a4b97a28a9cde7b0b29.png" width="1200" />
没有问题
3 测试错误报文的处理
我们的服务器可能会收到错误的客户端请求,比如一个请求中Conetent-Length 字段为300 ,但是他的正文长度却没有300,这时候就会触发错误请求的关闭机制,也就是 RECV_ERR 状态引起的关闭。
而错误报文分为两种情况,一种是短连接情况下,也就是只发送一次请求,如果请求正文不够,那么其实后续会触发超时关闭,我们的超时关闭是没有问题的,这一点不需要再次测试了。
那么我们就来测试一下长连接的情况下,如果出现错误报文,那么按照我们的服务器的处理逻辑,第一个错误报文会将后续的报文或者他的一部分当作第一个报文的正文,最后会导致解析第二个报文或者后面的报文时出错。
// 错误请求测试 1: 发送错误的请求,看他是否会影响后续的正常请求报文
int main()
{Socket cli;cli.CreatClientSocket("127.0.0.1",8080);int n = 5; //发送三次请求,每一次间隔三秒,观察中间是否会关闭std::string errorreq = "GET /get HTTP/1.1\r\nContent-Length: 100\r\nConnection: keep-alive\r\n\r\n";//错误报文std::string req = "GET /get HTTP/1.1\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n"; //简单的HTTP请求int ret = cli.Send(errorreq.c_str(),errorreq.size());if(ret < 0) {ERROR_LOG("Send Error");return 0;}while(n--) {ret = cli.Send(req.c_str(),req.size());if(ret < 0) {ERROR_LOG("Send Error");return 0;}sleep(3);}while(1) sleep(1);//观察最后是否正常超时关闭return 0;
}
这时候我们就会发现服务器崩溃了,收到了SIGSEGV 也就是11号信号,内存访问出错。
https://i-blog.csdnimg.cn/direct/93fe5e497fe94291b379304e7028b8de.png" width="1200" />
那么具体出问题在哪呢?
我们可以查看一下程序崩溃时的快照,或者说核心转储文件,看一下崩溃时的调用堆栈
https://i-blog.csdnimg.cn/direct/bbaacf2a6ca94430b596bb960b8920b4.png" width="1052" />
从上往下,直到看到我们自己写的接口,崩溃的地方就是第4层堆栈,也就是我们的WriteResponse接口,但是这里怎么会出现内存访问出错呢?我们可以看一下他的代码。
void WriteResponse(const PtrConnection& conn , HttpRequest& req , HttpResponse& resp){// 1 先把响应的头部字段完善了if(req.Close()) resp.AddHeader("Connection","close");else resp.AddHeader("Connection","keep-alive");if(resp._body.size()&&!resp.HasHeader("Content-Length")) resp.AddHeader("Content-Length",std::to_string(resp._body.size()));if(resp._body.size() && !resp.HasHeader("Content-Type")) resp.AddHeader("Content-Type","application/octet-stream");//重定向信息if(resp._redirect_flag) resp.AddHeader("Location",resp._redirect_url);// 2 组织响应std::ostringstream out;//响应行 HTTP/1.0 404 NotFound\r\nout<<req._version<<" "<<std::to_string(resp._statu)<<" "<<Util::StatuDesc(resp._statu)<<"\r\n";//头部字段for(auto& p : resp._headers){out<<p.first<<": "<<p.second<<"\r\n";} //空行out<<"\r\n";//正文out<<resp._body;// 3 发送conn->Send(out.str().c_str(),out.str().size());}
在这段代码中,除了Send,我们并没有进行内存的危险访问,其他的地方我们都是在调用库里面的以及我们实现的一些不会出现内存错误的接口。
那么我们可以把问题定位在Send接口,https://i-blog.csdnimg.cn/direct/08a3789554e94826bfbdfffb18274c3f.png" width="1200" />
而Send接口其实就是调用了Buffer的WriteAndPush和SendInLoop接口,WriteAndPush接口在这里只是一次简单的调用,我们先不看她,先看最有可能出问题的SendInLoop
https://i-blog.csdnimg.cn/direct/83f9641d1f8b4524ac10f002c3e43e89.png" width="1171" />
但是其实最终SendInLoop也就是调用了一次简单的WriteAndPush,那么WriteAndPush到底为什么会出现内存访问错误呢?
https://i-blog.csdnimg.cn/direct/e021aaef89f1424c9fa13a262875885a.png" width="1167" />
https://i-blog.csdnimg.cn/direct/0ea21c148cbc4d1db1e856e4b87c547d.png" width="1200" />
WriteAndPush内部其实又只有Write有内存访问操作
https://i-blog.csdnimg.cn/direct/34fd4c0b3ed8421fae20a0be7e1e1ff8.png" width="1200" />
Write中有一个EnsureWriteSize,这里可能会涉及到扩容,我们可以看一下是不是他出现问题,
https://i-blog.csdnimg.cn/direct/c771b74edcf944179087441adc647b20.png" width="1200" />
在扩容之后我们打印一次扩容之后的容量:
https://i-blog.csdnimg.cn/direct/9e188b439ce848c8bd695a3824dbf8d6.png" width="1147" />
https://i-blog.csdnimg.cn/direct/2642235e5da34aefa1bb175ef9e8fc3f.png" width="1076" />
这时候我们会发现,我们的buf不断地在扩容,最终系统可用资源不足了,导致程序崩溃。
那么为什么我们的_buf会不断扩容呢?
这时候我们需要回到OnMessage的错误处理中:
void OnMessage(const PtrConnection& conn,Buffer* buf) //获取新数据回调{while(buf->ReadSize() > 0) //从逻辑上来说 while(1) 也是一样的{// 1 获取上下文// Any* context = conn->GetContext();// HttpContext* pctx = context->GetData<HttpContext>();HttpContext* pctx = conn->GetContext()->GetData<HttpContext>();// 2 解析缓冲区数据pctx->RecvHttpRequest(buf);HttpRequest& req = pctx->GetRequest();HttpResponse resp;//判断解析是否出错if(pctx->RespStatu() >= 400) //请求解析出错,此时的_recv_statu 也一定是RECV_ERR{HandlerError(req,resp,pctx->RespStatu()); //调用错误处理方法WriteResponse(conn,req,resp); //返回响应conn->ShutDown(); //发生错误就关闭连接return;}if(pctx->RecvStatu() != RECV_OVER) //还没收到一个完整请求return;//走到这里说明req是一个完整的请求// 3 数据处理,路由DEBUG_LOG("收到一个完整报文,fd:%d url:%s %s",conn->Fd(),req._method.c_str(),req._path.c_str());Route(req,resp); //进行方法路由,判断是不是静态资源请求。// 4 返回给客户端WriteResponse(conn,req,resp);// 5 处理完之后重置上下文pctx->Reset();// 6 判断长短连接if(resp.Close()) //如果是短连接就直接关闭{conn->ShutDown();return;}//如果是长连接就需要搞成循环,读取下一个报文}}
我们在解析出错之后,直接调用了ShutDown,其实这里的问题就很明显了,因为我们的ShutDown中如果输入缓冲区还有数据会先去调用读方法,而读方法就又会来调用OnMessage接口,但是其实每一次走到判断接收状态码的时候,也就是上面的 if(pctx->RespStatu()>=400) 这一个判断时,其实都会判断为真,因为我们上一次解析出现错误之后并没有重置上下文,而是将错误的状态码和错误的接收状态继续保留在上下文,那么最终就会导致我们不断地调用WriteResponse,将错误界面的html信息不断地写入到我们的发送缓冲区中,而虽然写入发送缓冲区中之后虽然启动了写事件监控,但是其实我们的EventLoop线程根本就走不到下一轮的时间监控,而是一直在这一轮的这个OnMessage和ShutDown中死循环调用,即使最后缓冲区没数据了,我们的处理函数直接返回,那么也会走进这个错误处理,向缓冲区中不断写数据,最终就导致资源不够了。
那么我们正常的逻辑应该是: 出现错误请求之后,这个连接要关闭,同时,缓冲区中剩下的数据我们也不再处理了,因为他的请求都是错误的了,我们没必要再进行处理,同时,在调用ShutDown之前,我们需要将上下文重置。
//判断解析是否出错if(pctx->RespStatu() >= 400) //请求解析出错,此时的_recv_statu 也一定是RECV_ERR{HandlerError(req,resp,pctx->RespStatu()); //调用错误处理方法WriteResponse(conn,req,resp); //返回响应buf->MoveReadOffset(buf->ReadSize());pctx->Reset();conn->ShutDown(); //发生错误就关闭连接return;}
这样就不会出现循环调用HandlerError和WriteResponse这几个函数的情况了。
那么重新编译之后在来测试一下:
https://i-blog.csdnimg.cn/direct/55b7ef413e1848de8bf9a3656bdfc26d.png" width="1200" />
这时候我们就发现,服务器在处理第一个错误报文之后,读取第二个请求报文的时候读取出错,那么他就直接关闭连接了。
4 测试业务处理耗时超过超时时间的处理
有时候我们的业务处理耗时很长,甚至会超过我们的超时时间。
而我们的EventLoop的处理流程是先处理就绪事件,再去执行我们的任务队列中的任务,而超时释放任务的执行,也就是秒针的移动(RunTick)就是放在任务队列中的。
也就是说,可能会出现一种情况,就是我们的某些文件描述符就绪了,而后再执行我们的就绪事件的处理时,由于处理时间过长,如果我们的timerfd的时间就绪在其他文件描述符的处理之前,那么就会导致其他就绪的文件描述符还没来得及处理,就先去执行定时任务去了,这时候是否会出现问题?
其实不会出现问题,因为我们的定时任务是Connection::Release,而Release的操作是将ReleaseInLoop放入EventLoop中的任务队列中,他会在执行完所有的就绪事件之后再去执行任务队列的任务,所以其实即使超时时间到了,超时释放任务放入到任务队列中了,可是由于会先执行就绪的时间,而执行就绪事件的时候会刷新活跃度,所以最终只是会导致定时任务对象的计数器减一,而不会导致对象的销毁而调用释放连接的操作。
但是这里也提醒了我们两个问题:
1 我们的就绪处理阶段,其实连接一定是不会被释放的,那么我们的Channel模块中的就绪事件的处理其实不管怎么样,在这个函数作用域内,我们的连接是不会被释放的,因为连接释放的操作一定是在事件处理之后的,那么我们其实任意时间的回调也就是刷新活跃度的操作,可以放在执行完所有的就绪事件之后。
void HandlerEvents(){//读事件需要处理if(_revents & (EPOLLIN | EPOLLPRI | EPOLLRDHUP)) {if(_read_cb) _read_cb(); }//剩下三个事件只需要处理其中一种if(_revents & EPOLLOUT){if(_write_cb) _write_cb();}else if(_revents & EPOLLHUP){if(_close_cb) _close_cb();}else if(_revents & EPOLLERR){// if(_event_cb) _event_cb();// NORMAL_LOG("错误事件:%d",_fd);// _revents=0;if(_close_cb) _close_cb();}else _revents = 0;if(_event_cb) _event_cb();return;}
同时,在我们上面的分析中,我们会发现,定时器的timerfd的读事件是每秒钟触发一次,但是我们真正去处理读事件的时候,却可能并不止发生了一次超时,因为可能EventLoop在执行timerfd的读回调之前,有几个耗时很长的就绪事件的回调,那么其实并不是每一次timerfd触发事件之后,只是超时了一次,或者说过了一秒,也有可能已经超时了很多次了,具体的次数还是取决于我们读到的数据的大小,那么我们之前写的timerfd的读事件回调其实是有一点问题的。
原版:
void OnTime(){TimerRead();RunTick();}void TimerRead(){uint64_t val = 0;int ret = read(_timerfd,&val,sizeof val);if(ret<0){if(errno == EAGAIN ||errno == EWOULDBLOCK ||errno == EINTR) return;ERROR_LOG("timerfd read failed");abort();}return;} //移动秒针void RunTick(){_timer_idx ++;_timer_idx %= MAXTIME; // DEBUG_LOG("tick:%d",_timer_idx);_wheel[_timer_idx].clear();}
正确的逻辑是超时了多少次,就需要调用多少次RunTick。那么修正之后的代码:
void OnTime(){int times = TimerRead();while(times--)RunTick();}int TimerRead(){uint64_t val = 0;int ret = read(_timerfd,&val,sizeof val);if(ret<0){if(errno == EAGAIN ||errno == EWOULDBLOCK ||errno == EINTR) return 0;ERROR_LOG("timerfd read failed");abort();}return val;} //移动秒针void RunTick(){_timer_idx ++;_timer_idx %= MAXTIME; // DEBUG_LOG("tick:%d",_timer_idx);_wheel[_timer_idx].clear();}
那么这时候我们就可以来测试一下我们的业务处理很耗时的情况了,其实我们看到的现象不会很明显,因为我们无法确定未来就绪事件的处理的顺序,所以这个测试的结果我们就关注他是否正常超时释放就行了。
这时候我们至少需要为一个EvenbtLoop分配两个连接我们才能更好的看到效果。
//超时释放测试2: 业务处理耗时很长时,能否正常超时释放
int main()
{signal(SIGCHLD,SIG_IGN); //忽略该信号,子进程结束时自动被操作系统回收,不需要我们手动回收//创建四个客户端进程,服务器有两个从属EventLoop,那么每一个EventLoop都会分配到两个连接for(int i =0;i<4;++i){pid_t id = fork();if(id < 0) return 0;if(id ==0 )//子进程{Socket clt;clt.CreatClientSocket("127.0.0.1",8080);std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"; //不要让服务器直接关闭连接,我们需要观察超时连接的释放int ret = clt.Send(req.c_str(),req.size());if(ret < 0 ) ERROR_LOG("Send Error");char buf[1024] = {0};ret = clt.Read(buf,1023);int time = 10;while(1) sleep(5);exit(-1);}}while(1) sleep(1);return 0;
}
然后我们让GET方法中 /get 的处理时间变长,简单一点就是睡眠15 秒钟(我们设置的超时时间是10s)。
//超时释放测试2: 业务处理耗时很长时,能否正常超时释放
int main()
{signal(SIGCHLD,SIG_IGN); //忽略该信号,子进程结束时自动被操作系统回收,不需要我们手动回收//创建四个客户端进程,服务器有两个从属EventLoop,那么每一个EventLoop都会分配到两个连接for(int i =0;i<4;++i){pid_t id = fork();if(id < 0) return 0;if(id ==0 )//子进程{Socket clt;clt.CreatClientSocket("127.0.0.1",8088);std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"; //不要让服务器直接关闭连接,我们需要观察超时连接的释放int ret = clt.Send(req.c_str(),req.size());if(ret < 0 ) ERROR_LOG("Send Error");char buf[1024] = {0};ret = clt.Read(buf,1023);int time = 10;while(1) sleep(10);exit(-1);}}while(1) sleep(1);return 0;
}
https://i-blog.csdnimg.cn/direct/c44ad8bcb5574e99bd5d4d3076235cf9.png" width="1200" />
我们可以分析一下这个过程:
首先是建立连接和分配EventLoop线程阶段
https://i-blog.csdnimg.cn/direct/3be64bd01cbf448f8e56aedce1a2100a.png" width="1200" />
关于连接的分配其实我们从代码逻辑就能知道结果,因为我们分配EventLoop的时候是采用RR轮转的方式,保证负载均衡。 同时,从后面的数据的处理的线程也能看出来哪个连接归哪个线程管理。
然后就是这两个线程的第一个就绪事件的处理,他们是同时或者说并发处理的,我们可以理解为同时推进,多核cpu。https://i-blog.csdnimg.cn/direct/fe2bf30d74cc4269ae19781015483f7f.png" width="1200" />
但是从上面的结果中我们也能发现一个问题,就是每一个线程其实都只处理了第一个到来的连接的就绪事件,而第二个连接在这一轮事件处理中仅仅是添加了定时任务,还没有开始通信,这就会导致一个问题,就是下一轮事件处理时,如果先处理的是timerfd的读事件,那么我们的tick就会直接向后走 15 秒。 而第二个描述符添加定时任务时,使用的是旧时 tick + 10 来确定定时任务的位置。
那么就会导致出现上面的问题,也就是实际上第二个到来的文件描述符还没有虽然也有读事件继续,同时也从 epoll 模型中将读事件读取到了 eventloop 中的 actives 中,该文件描述符的读事件是就绪的,但是如果先执行timerfd的读事件,那么此时 tick 会向后走 15 格,这时候而第二个连接的定时任务的超时时间是 tick + 10 ,这时候就会直接触发第二个连接的超时任务,也就会把连接释放,但是我们的就绪事件数据中还有该文件描述符的读事件待处理,虽然我们的PtrConnection还有计数存在,不会出现野指针的访问,但是在定时任务触发后会把sock关闭,所以后续读取数据肯定是要出错的。
这样的超时逻辑其实是有一点不合理的,但是也能接受,因为我们现实中不可能出现 15 秒的io处理时间,如果出现,那么就说明我们的服务器的负载过大,已经无法正常工作了,那么这时候释放一些连接其实是一种自救的策略。所以我们并不认为这个定时逻辑是错误的,只是认为这是一种定时的策略。
如果不想这样做,而是想在优化定时机制,那么实际上我们可以参考 Muduo 库,Muduo库是把所有的定时任务或者说只设置时间轮在主Reactor线程中,而工作线程或者从属Reactor线程是负责IO处理和业务处理。而由于主线程只负责获取新连接,而获取新连接的是很短很短的,一般来说,同时到来大量的新连接,他的处理时间也不会超过 1s ,所以不会影响tick的走动。 那么可能有人会说,他不会出现线程安全吗?
并不会,因为如果在主线程设置定时任务,我们的定时任务还是Release,他会去执行 ReleaseInLoop,这时候由于是在主线程,他会把任务放到连接所绑定的任务队列中执行,所以对连接的所有的操作还是在连接所绑定的EventLoop线程中执行的。
5 测试同时收到多条正常请求
这里主要测试服务器在同一时间收到了多条请求,能否正常解析处理,这一点其实没什么好讲的,直接上测试代码。
int main()
{signal(SIGCHLD,SIG_IGN); //忽略该信号,子进程结束时自动被操作系统回收,不需要我们手动回收//创建四个客户端进程,服务器有两个从属EventLoop,那么每一个EventLoop都会分配到两个连接Socket clt;clt.CreatClientSocket("127.0.0.1",8088);std::string req = "GET /get HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";req += req;req += req;//同时发送四条请求clt.Send(req.c_str(),req.size());while(1){char buf[1024] = {0};int ret = clt.Read(buf,1023);if(ret == -1) break;if(ret == 0) continue;DEBUG_LOG("%s",buf);};sleep(100);return 0;
}
https://i-blog.csdnimg.cn/direct/39223df312ba46278a085a6e4c5df67a.png" width="1200" />
测试下来是没问题的。
6 大文件传输测试
在这一部分我们需要测试给服务器上传一个大文件,服务器将文件保存下来,然后我们需要观察上传的文件和保存给服务器的文件是否相同。
这一点其实不好测试,最主要的还是我们的测试文件其实无法过大,因为我们的测试条件有限,服务器和客户端都在一个云服务器上运行,同时需要将文件的内容放在内存中,这就要求我们的文件内容不能过大。
我们怎么查看可用内存大小呢?
使用 free -h 命令
https://i-blog.csdnimg.cn/direct/bb67198c64cd4620bf8c235e94743a89.png" width="1200" />
我们可以看到 available 栏,我们的可用内存大小只有 644 M ,那么我们为了稳妥期间,就使用一个200M的文件进行测试
那么如何生成一个200M的文件呢?我们可以使用下面的这个命令
dd if=/dev/zero of=./test.txt bs=200M count=1
dd 就是拷贝,if就是输入,也就是被拷贝的文件,而我们要拷贝的文件就是 /dev/zero ,这是一个设备文件,或者说是一个输入设备文件,它可以无限提供 0 。 of 就是我们的输出目标。我们在当前目录下生成一个test.txt , 而bs就是blocksize ,指的是输入输出的块的大小,而count就是块的数量。
https://i-blog.csdnimg.cn/direct/d2d3742391294c1cabfdf7f80f96c927.png" width="1104" />
生成好文件之后,我们可以先写好client 的测试代码
https://i-blog.csdnimg.cn/direct/04695a0d738b47919eaff0da875d475a.png" width="1200" />
我们进行数据摘要发现两个文件确实是一样的,所以也算是验证成功了,当然,200M的文件在互联网中真的不算大,是很小很小的一个文件了,但是我们条件有限,也只能测试到这个程度。
7 压力测试
压力测试其实也就是我们的并发量测试,还是一样的,由于我们的云服务器的配置有限,所以压力测试的结果不一定准确。
压力测试我们会有两个测试指标:
1 并发量:可以同时处理多少个客户端请求而不会出现连接失败
2 QPS(QPM):每秒钟(每分钟)处理的包的数量
而测试服务器的性能是不能抛开测试环境的。
先说明,我们的测试环境如下:
服务器的环境:2核2G,带宽4Mbps 的腾讯云CentOS QMB8云服务器
而客户端的测试环境是在服务器的同一台主机,所以我们的测试结果中其实没有受到带宽的影响,都是在本地环回测试。
我们使用webbench来进行测试,服务器创建 5 个从属 Reactor 线程。
建立5000个客户端,测试时间为30s,
https://i-blog.csdnimg.cn/direct/0a3c1bfec5e948fab2c01893e2365b88.png" width="1200" />
5000可以,我们在来测试一下6000个客户端:
https://i-blog.csdnimg.cn/direct/d4530b6d641745a6ba31481986012d91.png" width="1159" />
再来测试一下7000个客户端:
https://i-blog.csdnimg.cn/direct/bf92dfe4ce544a81bde2e332b76942f3.png" width="1142" />
测试8000个的时候,系统资源不够了,不允许我们创建这么多个子进程。因为webbench的底层就是创建子进程,在子进程中连接服务器来测试的。
然后我们再换成创建 8 个线程来测试:
https://i-blog.csdnimg.cn/direct/7a8a3fe33b9040ac8d432384f3bf90e5.png" width="1141" />
我们进行一下服务器8个线程,5000客户端100s的负载测试:
https://i-blog.csdnimg.cn/direct/48c7681c60124daab9bffd362f745cf9.png" width="1169" />
我们的webbench给出的是QPM,也就是每分钟的处理的包的数量,性质都是一样的。
那么我们服务器的测试结果就是:前提条件是我们的服务器和客户端所处的环境,我们测试的软件,以及我们服务器创建 8 个新线程处理请求。
支持并发量7000,测试30s,QPM为 12 万
支持并发量5000,测试100s,QPM为 15 万