Server模块
单元测试
在Server模块里包含了几个单独的模块,分别是Buffer模块,Epoll模块,TimerWheel模块和Socket模块,这些模块的功能需要进行单元测试,而剩余的模块都需要整合基础模块,所以这些剩余的模块需要介入联调,也就是集成测试。
Buffer模块
Buffer主要的功能是模拟一个缓冲区,可以调用外部的接口对缓冲区进行写入和读出数据的操作,针对这个模块,通过白盒测试,我进行语句覆盖,条件覆盖,组合条件覆盖等方法,设计了如下的单元测试用例:

buffer模块测试代码位置 :buffer模块测试代码位置
Socket模块
socket模块是实现套接字函数的封装,用于构建服务器端和客户端,针对这个模块,通过场景法,我设置了以下的单元测试用例:

socket模块服务端测试代码位置 :服务端代码位置
socket模块客户端测试代码位置 :客户端代码位置
timerwheel模块
timerwheel模块在项目里主要承担管理超时连接的功能,可以添加销毁连接,刷新连接,添加连接的任务,主要是通过场景法,以下是我设计的测试用例:

TimerWheel模块测试代码位置 :TimerWheel模块测试代码位置
Channel模块
Channel模块用于对一个文件描述符进行事件管理,主要功能有设置读/写事件监控,设置回调函数,判断是否监控了读/写,获得就绪事件,删除所有事件监控,执行回调函数,以下是我设计的测试用例:

channel模块测试代码位置 :channel模块测试代码位置
集成测试
Epoll模块
Epoll模块主要是封装了epoll模型的操作,这也是此高并发服务器多路复用技术所在,这个模块主要实现了创建一个epoll文件描述符,往epoll里添加事件,删除事件,处理事件的功能,以下是我设计的测试用例:

Epoll模块测试代码位置 :Epoll模块测试代码位置
Eventloop模块
Eventloop模块绑定了一个线程,如果任务都在同一个线程中执行,就可以避免线程安全的问题,临界资源就不必加锁,减少了加锁的消耗,Eventloop集成了Epoll模块,Channel模块和TimerWheel模块,主要功能有添加/删除由channel管理的文件描述符,添加需要执行的任务和执行任务,以下是我设计的测试用例:

Eventloop模块测试代码位置 :Eventloop模块测试代码位置
LoopThread模块
由于Eventloop在创建自身对象的时候对应线程是不确定的,所以需要指定一个对象,在一个明确的线程里创建这个对象,那么这个对象里的所有函数都会在这个线程里执行,LoopThread模块主要实现的功能是封装一个线程,设置线程池大小,创建线程池,返回下一个线程,以下是我设计的测试用例:

LoopThread模块测试代码设计 :LoopThread模块测试代码设计
connection模块
connection模块用于管理一个通信描述符,主要功能是给通信描述符设置对应的阶段回调函数,设置完之后如果触发对应的读事件/写事件/任意事件/错误事件/关闭事件,就会在这些事件里调用外部设置的阶段回调函数,比如说触发了读事件之后,处理完缓冲区里的数据,就要对数据进行处理,就会调用对应的阶段回调函数,还有设置此通信描述符的非活跃连接销毁功能,以下是我设计的测试用例:

Connection模块测试代码设计 :Connection模块测试代码设计
TCPServer模块
TCPServer是对前面各个模块的集合,其中实现了设置阶段回调函数,添加定时任务,删除定时任务,启动非活跃连接销毁的功能,以下是我设计的测试用例:

TCPServer模块测试代码设计 :TCPServer模块测试代码设计
HTTP协议模块
单元测试
Util模块
Util模块主要实现的是在HTTP中的一些零碎的功能,有往文件里写入数据,读取文件数据,URL编码和解码,状态码和文件后缀mime的获取,资源有效性判断,文件类型判断,字符串分割的功能,以下是我设计的测试用例:

Util模块测试代码设计 :Util模块测试代码设计
Request模块测试
request模块主要是用于解析http请求的,其中包含了请求行,请求头和请求体,分别对这三个部分实现了不同的函数,以下是我设计的测试用例:

Request模块测试代码设计 :Request模块测试代码设计
response模块
response模块是用来管理http响应报文的,里面严格规定了http响应的格式,包含设置响应头和响应体相关的功能,下面是我设计的测试用例:

Response模块测试代码设计 :Response模块测试代码设计
集成测试
Context模块
context模块主要是用于对request分解,包含对请求行,请求头,请求体的分解,这里就可以使用正交表法,以下是我设计的测试用例:

Context模块测试代码设计 :Context模块测试代码设计
系统测试
各类请求方法的测试
GET方法
制作一个简单的回显服务器,当进行GET方法的请求的时候,会获得如下的界面

这里是我们从浏览器获得的请求

这里是服务器回复的报文

POST方法
当进行POST方法的测试的时候,浏览器可以看到如下界面,可以看到最后一行是我们提交的用户名和密码。

这是我们从浏览器获得的请求

这是服务器回复的报文

PUT方法
DELETE方法
长连接连续请求的测试
测试源码
cpp
int main()
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int cnt=10;
int sockfd=sock.RetSock();
while(cnt--)
{
ssize_t ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n",0);
if(ret<0)
{
break;
}
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
//INFO_LOG("%s",buffer.c_str());
break;
}
//std::cout<<buffer<<std::endl;
sleep(1);
}
while(1)
{
sleep(1);
}
return 0;
}
测试结果
客户端在前十秒钟,每隔一秒向服务器发送一次数据,服务器的超时连接管理机制会将此客户端的连接刷新,每秒钟刷新一次。
预期结果:客户端在前十秒钟,每一秒发送一次信息,并接收从服务器发送的信息,在启动程序二十秒的时候,客户端与服务器的通信连接被释放。
实验结果:客户端在前十秒钟,每一秒发送一次信息,并接收从服务器发送的信息,在启动程序二十秒的时候,客户端与服务器的通信连接被释放,符合预期,测试通过。
超时连接测试
测试源码
cpp
#include"../socket.hpp"
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int sockfd=sock.RetSock();
ssize_t ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n",0);
if(ret<0)
{
return 1;
}
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
return 1;
}
std::cout<<buffer<<std::endl;
sleep(20);
return 0;
}
测试结果
客户端在最开始的时候向服务器发送一次数据,服务器的超时连接管理机制会将此客户端的连接刷新,并给客户端回复一次数据。
预期结果:客户端在刚开始运行的时候发送一次信息,并接收从服务器发送的信息,在启动程序十秒的时候,客户端与服务器的通信连接被释放。
实验结果:客户端在刚开始运行的时候发送一次信息,并接收从服务器发送的信息,在启动程序十秒的时候,客户端与服务器的通信连接被释放,符合预期,测试通过。
错误请求测试
测试源码
cpp
#include"../socket.hpp"
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int sockfd=sock.RetSock();
ssize_t ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 100\r\n\r\nhellovientiane",0);
if(ret<0)
{
return 1;
}
std::cout<<"-----------------------------------"<<std::endl;
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
return 1;
}
std::cout<<buffer<<std::endl;
sleep(20);
return 0;
}

客户端在与服务器建立连接以后,不断向服务器发送错误的请求报文,请求的请求体长度和请求头中的Content-Length长度不一致,就会导致后续报文解析出错。
预期结果:第一次解析的报头和请求行是正确的,服务器也并没有发现问题,会向客户端发送一次200 OK的应答,后续解析报文出错,就向客户端发送状态码为400的应答,表示这个请求是有问题的,并返回错误界面。
实验结果:第一次解析的报头和请求行是正确的,服务器也并没有发现问题,会向客户端发送一次200 OK的应答,后续解析报文出错,就向客户端发送状态码为400的应答,表示这个请求是有问题的,并返回错误界面,符合预期,测试通过。
业务处理超时测试
测试源码
cpp
#include"../socket.hpp"
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>
int main()
{
for(int i=0;i<10;i++)
{
pid_t id=fork();
if(id<0)
{
std::cout<<"fork error"<<std::endl;
return -1;
}
else if(id==0)
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int sockfd=sock.RetSock();
while(1)
{
ssize_t ret=sock.Send(sockfd,"GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n",0);
if(ret<0)
{
return 1;
}
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
return 1;
}
std::cout<<buffer<<std::endl;
sleep(1);
}
sock.Close();
exit(0);
}
}
while(1)
{
sleep(1);
}
return 0;
}
服务器到达了一个性能瓶颈,在一次业务处理中花费了太长时间,超过了服务器本身设置的非活跃超时连接的时间,假设有五个描述符就绪了,分别为1,2,3,4,5,在处理1号描述符的时候超时了,导致2,3,4,5长时间没刷新活跃度,导致超时,如果接下来的2号描述符是通信描述符则没有太大问题,因为任务队列里接下来就执行2,3,4,5的时间,而不会直接释放2,3,4,5,如果2号描述符是定时器描述符,就会触发超时连接释放,如果此时把3,4,5直接释放掉,那么就会导致接下来在处理3,4,5的时候内存访问错误,这个测试主要是查看程序是否将销毁任务压入任务队列,而不是直接执行。
预期结果:这十个连接会不断执行,最后ctrl+c结束程序后,也不会触发内存访问错误,因为都是在执行完所有任务才会执行释放任务的。
实验结果:这十个连接会不断执行,最后ctrl+c结束程序后,也不会触发内存访问错误,符合预期,测试成功。
同时处理多条请求测试
测试源码
cpp
#include"../socket.hpp"
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int sockfd=sock.RetSock();
std::string str="GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
str+="GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
str+="GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
str+="GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
str+="GET /Get HTTP/1.1\r\nConnection: keep-alive\r\nContent-Length: 0\r\n\r\n";
while(1)
{
ssize_t ret=sock.Send(sockfd,str,0);
if(ret<0)
{
return 1;
}
std::cout<<"-----------------------------------"<<std::endl;
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
return 1;
}
std::cout<<buffer<<std::endl;
sleep(1);
}
sleep(20);
return 0;
}
测试结果
客户端一次性发送多条请求,如果处理出问题就会导致粘包问题,格式出错。
预期结果:客户端可以收到正确的响应报文,并不会出现格式错误导致的错误响应。
实验结果:客户端收到了正确的响应报文,符合预期,实验成功

大文件传输测试
测试源码
cpp
#include"../socket.hpp"
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include"../http/Util.hpp"
int main()
{
Socket sock;
sock.CreateClient("127.0.0.1",8888);
int sockfd=sock.RetSock();
std::string str="PUT /Put HTTP/1.1\r\nConnection: keep-alive\r\n";
std::string body;
Util::ReadFile("./maxdata.txt", body);
str+="Content-Length: "+std::to_string(body.size())+"\r\n\r\n";
int cnt=1;
while(cnt--)
{
ssize_t ret=sock.Send(sockfd,str,0);
ret=sock.Send(sockfd,body,0);
if(ret<0)
{
return 1;
}
std::cout<<"-----------------------------------"<<std::endl;
char buffer[1024];
ret=sock.Recv(sockfd,buffer,sizeof(buffer));
if(ret<0)
{
perror("recv: ");
return 1;
}
std::cout<<buffer<<std::endl;
sleep(1);
sock.Close();
}
sleep(20);
return 0;
}
测试结果
大文件传输往往会导致性能问题,刚开始我是采用1G的大文件进行测试,但受限于设备环境配置,最后使用的是100M的大文件进行测试。
预期结果:两个大文件的哈希值是一样的
实验结果:两个大文件的哈希值是一样的,符合预期,实验成功

性能测试
受限于设备环境配置,测试时使用的服务器环境是2核2G轻量级应用服务器Ubuntu-22.04,服务器程序采用一主零从Reactor模式,客户端与服务端共用一台轻量级应用服务器,测试工具为Webbench,以4000并发量向服务器发起请求,结果近似为1300QPS,吞吐量近似为每秒钟48万字节

用于创建线程也需要开销,在这台机器上创建多个线程就会导致webbench崩溃,受限于性能原因,这边只测试一个主线程,零个从属线程的情况。