先展示一下整个工程的路径结构,如下图所示:
接下来我们就编写一下 server.cc 也就是服务器的主函数,简单的给出几个执行函数映射表,然后启动服务器,代码如下所示:
#include "httpserver.hpp"
const std::string source = "./wwwroot";
std::string RequestStr(const HttpRequest &req)
{
// 将请求报文结构转化为字符串格式
std::stringstream ss;
ss << req._method << " " << req._path << " " << req._version << "\r\n";
for (auto &it : req._queryString)
ss << it.first << ": " << it.second << "\r\n";
for (auto &it : req._header)
ss << it.first << ": " << it.second << "\r\n";
ss << "\r\n" << req._body;
return ss.str();
}
// 进行简单的请求报文的回显(text/plain表示文本内容)
void Hello(const HttpRequest &req, HttpResponse *rsp)
{
rsp->set_content(RequestStr(req), "text/plain");
}
// 进行简单的请求报文的回显
void Login(const HttpRequest &req, HttpResponse *rsp)
{
rsp->set_content(RequestStr(req), "text/plain");
}
// 接收请求的文件内容,然后放到当前资源根目录下
void PutFile(const HttpRequest &req, HttpResponse *rsp)
{
std::string pathname = source + req._path;
Util::write_file(pathname, req._body);
}
// 进行简单的请求报文的回显
void DelFile(const HttpRequest &req, HttpResponse *rsp)
{
rsp->set_content(RequestStr(req), "text/plain");
}
int main()
{
HttpServer server(8080, 10); // 设置端口和超时事件
server.set_nums_of_thread(4);
server.set_static_directory(source);
// 设置执行函数映射表
server.add_get("/hello", Hello);
server.add_post("/login", Login);
server.add_put("/1234.txt", PutFile);
server.add_delete("/1234.txt", DelFile);
// 启动服务器
server.start_httpserver();
return 0;
} makefile 文件如下所示:
CXX = g++
DEBUG = -g
THREAD = -lpthread
HIGH_VERSION = -std=c++17
CXXFLAGS = -c #-Wall
TARGET = server
OBJ = $(patsubst %.cc, %.o, $(wildcard $(TARGET).cc))
HEADERS = httpserver.hpp ../server.hpp
$(TARGET) : $(OBJ)
$(CXX) -o $@ $^ $(HIGH_VERSION) $(THREAD) $(DEBUG)
%.o : %.cc $(HEADERS)
$(CXX) $(CXXFLAGS) -o $@ $< $(DEBUG)
.PHONY:clean
clean:
rm -rf $(TARGET) *.oⅡ. 测试代码
首先先给出 makefile 文件,我们只需要在该 makefile 文件中改一下目标生成文件即可生成不同的目标文件,如下所示:
CXX = g++
TARGET = client1
OBJ = $(patsubst %.cc, %.o, $(wildcard $(TARGET).cc))
CXXFLAGS = -c #-Wall
$(TARGET) : $(OBJ)
$(CXX) -o $@ $^ -std=c++11
%.o : %.cc
$(CXX) $(CXXFLAGS) -o $@ $<
.PHONY:clean
clean:
rm -f $(OBJ) $(TARGET)1、超时连接释放测试一
因为前面服务器的超时时间设置为 10 秒,所以我们让当前连接的处理休眠 15 秒进行测试,预期结果:10s 后连接被释放。
#include "../source/server.hpp"
// 测试长连接的demo
int main()
{
// 创建客户端套接字
Socket client_sock;
client_sock.create_client(8080, "81.71.97.127");
std::string str = "GET /hello HTTP/1.1\r\nConnection: close\r\nContent-Length: 0\r\n\r\n";
while(true)
{
assert(client_sock.Send(str.c_str(), str.size()) != -1);
char buf[1024] = { 0 };
client_sock.Recv(buf, sizeof(buf) - 1);
DLOG("%s", buf);
sleep(15);
}
return 0;
} 可以看到结果确实会超时释放:

2、超时连接释放测试二
给服务器发送一个数据,告诉服务器要发送 1024 字节的数据,但是实际发送的数据不足 1024,查看服务器处理结果,有以下两种情况:
- 如果数据只发送一次,服务器将得不到完整请求,就没有再进行业务处理,客户端也就得不到响应,最终非活跃后超时关闭连接
- 连着给服务器发送了多次小的请求,服务器会将后边的请求当作前边请求的正文进行处理,而后处理的时候有可能就会因为处理错误而关闭连接
预期结果:服务器第一次接收请求不完整,会将后边的请求当作第一次请求的正文进行处理。最终对剩下的数据处理的时候处理出错,关闭连接。
#include "../source/server.hpp"
/*
给服务器发送一个数据,告诉服务器要发送1024字节的数据,但是实际发送的数据不足1024,查看服务器处理结果,有以下两种情况:
1. 如果数据只发送一次,服务器将得不到完整请求,就没有再进行业务处理,客户端也就得不到响应,最终非活跃后超时关闭连接
2. 连着给服务器发送了多次小的请求,服务器会将后边的请求当作前边请求的正文进行处理,而后处理的时候有可能就会因为处理错误而关闭连接
*/
int main()
{
// 创建客户端套接字
Socket client_sock;
client_sock.create_client(8080, "81.71.97.127");
std::string str = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\n\r\nlirendada";
while(true)
{
assert(client_sock.Send(str.c_str(), str.size()) != -1);
//assert(client_sock.Send(str.c_str(), str.size()) != -1);
//assert(client_sock.Send(str.c_str(), str.size()) != -1);
char buf[1024] = { 0 };
client_sock.Recv(buf, sizeof(buf) - 1);
DLOG("%s", buf);
sleep(1);
}
return 0;
}3、超时连接释放测试三
当业务处理超时,查看服务器的处理情况:因为当服务器达到了一个性能瓶颈,如果在一次业务处理中花费了太长的时间(有可能超过了服务器设置的非活跃超时时间),则有可能导致其他的连接也被连累超时,其他的连接有可能会被拖累超时释放!
假设现在 12345 描述符就绪了且非活跃超时时间为 30s,在处理 1 的时候花费了 30s 处理后,导致 2345 描述符因为长时间没有刷新活跃度,此时有两种情况:
- 如果接下来的
2345描述符都是通信连接描述符,如果都就绪了,则没有太大的影响,因为接下来就会进行处理并刷新活跃度 - 如果接下来的
2号描述符是定时器事件描述符,此时定时器触发超时,执行定时任务,就会将345描述符给释放掉- 这时候一旦
345描述符对应的连接被释放,接下来在处理345事件的时候就会导致程序崩溃(内存访问错误)
- 这时候一旦
因此在本次事件处理中,并不能直接对连接进行释放,而应该将释放操作压入到任务池中,等到事件处理完了执行任务池中的任务的时候,再去释放。
不过这种测试是比较偶然的,不一定能测出来,下面我们就创建多个子进程客户端,向服务器发送数据,然后服务器在处理 hello 执行函数中进行 15s 的等待,此时就会超过非活跃连接销毁时间!
#include "../source/server.hpp"
int main()
{
// 创建多进程客户端套接字
signal(SIGCHLD, SIG_IGN);
for(int i = 0; i < 10; ++i)
{
pid_t id = fork();
if(id < 0)
{
ELOG("fork error");
return -1;
}
else if(id == 0)
{
// 子进程进行数据发送
Socket client_sock;
client_sock.create_client(8080, "81.71.97.127");
std::string str = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";
while(true)
{
assert(client_sock.Send(str.c_str(), str.size()) != -1);
char buf[1024] = { 0 };
client_sock.Recv(buf, sizeof(buf) - 1);
DLOG("%s", buf);
sleep(1);
}
exit(0);
}
}
while(true); // 父进程进行死循环
return 0;
}4、数据中多条请求处理测试
一次性给服务器发送多条数据,然后查看服务器的处理结果。预期结果:每一条请求都应该得到正常处理。
#include "../source/server.hpp"
/* 一次性给服务器发送多条数据,然后查看服务器的处理结果 */
/* 每一条请求都应该得到正常处理 */
int main()
{
// 创建客户端套接字
Socket client_sock;
client_sock.create_client(8080, "81.71.97.127");
std::string str = "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";
str += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";
str += "GET /hello HTTP/1.1\r\nConnection: keep-alive\r\n\r\n";
while(true)
{
assert(client_sock.Send(str.c_str(), str.size()) != -1);
char buf[1024] = { 0 };
client_sock.Recv(buf, sizeof(buf) - 1);
DLOG("%s", buf);
sleep(1);
}
return 0;
}5、put请求大文件上传测试
首先我们先用命令行创建一个大文件,如下所示:
[liren@VM-8-7-centos test]$ dd if=/dev/zero of=./test.txt bs=300M count=1
1+0 records in
1+0 records out
314572800 bytes (315 MB) copied, 0.495342 s, 635 MB/s 然后客户端进行发送:
#include "../source/http/httpserver.hpp"
/* 大文件传输测试,给服务器上传一个大文件,服务器将文件保存下来,观察处理结果(上传的文件,应该和服务器保存的文件一致才对) */
int main()
{
// 创建客户端套接字
Socket client_sock;
client_sock.create_client(8080, "127.0.0.1");
std::string str = "put /1234.txt HTTP/1.1\r\nConnection: keep-alive\r\n";
std::string body;
Util::read_file("./test.txt", &body);
str += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";
assert(client_sock.Send(str.c_str(), str.size()) != -1);
assert(client_sock.Send(body.c_str(), body.size()) != -1);
char buf[4096] = { 0 };
client_sock.Recv(buf, sizeof(buf) - 1);
DLOG("%s", buf);
sleep(3);
return 0;
} 可以看到它们的 md5 值都是相同的,说明是同一个文件!
6、性能测试
这里采用 webbench 进行服务器性能测试。webbench 是知名的网站压力测试工具,它是由 Lionbridge 公司(http://www.lionbridge.com)开发的。**`webbench`** 的标准测试可以向我们展示服务器的两项内容: 每秒钟相应请求数 和 每秒钟传输数据量。
webbench测试原理是:创建指定数量的进程,在每个进程中不断创建套接字向服务器发送请求,并通过管道最终将每个进程的结果返回给主进程进行数据统计。所以性能测试的两个重点衡量标准:吞吐量 && QPS(即每秒钟处理的包的数量)。
测试环境:
任何不说明测试环境的测试都是无知的,这里服务器环境:2核2G云服务器,服务器程序采用 1 主 3 从的 reactor 模式。
而 webbench 客户端环境:在同一台云服务器上。(测试的意义不大,因为同主机会造成互相的 cpu 争抢,但是这里目前没办法,毕竟服务器的带宽和资源太低了)
[liren@VM-8-7-centos WebBench]$ ./webbench -c 5000 http://127.0.0.1:8080/
Webbench - Simple Web Benchmark 1.5
Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
Benchmarking: GET http://127.0.0.1:8080/
5000 clients, running 30 sec.
Speed=19040 pages/min, 718584 bytes/sec.
Requests: 9519 susceed, 1 failed.