使用C++制作简单的web服务器(续)攻略
实现目标
本篇攻略主要讲解如何使用C++进行制作简单的Web服务器,其主要实现目标为:
- 实现静态文件的服务器
- 实现HTTP请求的解析和响应
- 支持并发处理请求
- 支持多线程和多进程的方式进行并发处理请求
环境准备
在开始制作Web服务器之前,我们需要先安装一些必要的库和工具:
- C++编译器(可以使用gcc或clang)
- Libevent库:用于事件驱动编程
- OpenSSL库(可选):用于加密和解密
服务器架构
Web服务器的整体架构如下:
- 创建监听套接字
- 绑定监听套接字
- 开始监听
- 接收客户端连接请求
- 接收客户端请求报文
- 解析客户端请求报文
- 处理客户端请求
- 生成服务器响应数据
- 发送响应给客户端
- 关闭连接
代码实现
参数解析
在开始编写Web服务器之前,我们需要先解析全部的传入参数。下面是一个示例命令:
./web_server -h 192.168.31.168 -p 8080 -t 10 -s on -l debug
其中包含的参数解释如下:
-h
:指定服务器IP地址-p
:指定服务器端口号-t
:指定服务器线程池的线程数量-s
:是否启用SSL-l
:指定日志的级别
例如,我们通过如下方式进行参数的解析:
bool Server::parseArgs(int argc, char* argv[])
{
int opt;
while ((opt = getopt(argc, argv, "h:p:t:s:l:")) != -1) {
switch (opt) {
case 'h':
m_host = optarg;
break;
case 'p':
m_port = std::stoi(std::string(optarg));
break;
case 't':
m_threadNum = std::stoi(std::string(optarg));
break;
case 's':
m_ssl = (std::string(optarg) == "on");
break;
case 'l':
m_logLevel = optarg;
break;
default:
return false;
}
}
return true;
}
上述代码中我们使用了getopt
函数对参数进行解析,并使用switch
语句进行请求的处理。
监听套接字
在开始监听客户端连接请求之前,我们需要先创建并设置监听套接字。我们可以使用socket
函数创建和设置套接字。在本次实现中我们采用了AF_INET
协议簇,并使用setsockopt
方法设置套接字选项。
bool Server::initSocket()
{
m_listenFd = socket(AF_INET, SOCK_STREAM, 0);
assert(m_listenFd != -1);
int optval = 1;
if (setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval,
sizeof(int)) < 0) {
close(m_listenFd);
return false;
}
if (setsockopt(m_listenFd, SOL_SOCKET, SO_REUSEPORT, (const void *)&optval,
sizeof(int)) < 0) {
close(m_listenFd);
return false;
}
return true;
}
绑定监听套接字
在设置好监听套接字之后,我们需要将其绑定在指定的IP地址和端口号上:
bool Server::bindSocket()
{
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(m_host.c_str());
serverAddr.sin_port = htons(m_port);
if (bind(m_listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {
close(m_listenFd);
return false;
}
return true;
}
开始监听
之后我们需要将监听套接字转为监听状态,并使用listen
函数开始接受用户的连接请求。在本次实现中我们采用1024作为连接队列的长度。
bool Server::startListen()
{
if (listen(m_listenFd, 1024) == -1) {
close(m_listenFd);
return false;
}
return true;
}
接收客户端连接请求
在服务器开始监听之后,我们需要不断地接受并处理用户的连接请求,并使用accept
函数生成系统分配的套接字描述符,返回可供操作的套接字。在本次实现中我们使用epoll
异步IO来进行监听。
bool Server::acceptConnection()
{
struct sockaddr_in clientAddr;
socklen_t clientAddrLen = sizeof(clientAddr);
int connFd = accept(m_listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
if (connFd == -1) {
return false;
}
if (m_ssl) {
// SSL accept
}
else {
m_eventLoop->add_handler(connFd, poll_event::READ_EVENT,
std::bind(&Server::handleRequest, this, std::placeholders::_1, std::placeholders::_2));
}
return true;
}
客户端请求报文处理
在接受到客户端请求之后,我们需要对其进行解析处理。Web服务器会根据URL路径找到对应的处理函数,然后将处理结果构造为HTTP响应(包括响应头和响应体)依次发送给客户端。在本次实现中我们使用了正则表达式解析请求报文,并使用回调函数进行处理。
void Server::handleRequest(int fd, uint32_t events)
{
uint32_t len = 0;
int ret = ioctl(fd, FIONREAD, &len);
if (ret == -1 || len <= 0) {
removeConn(fd);
return;
}
char buf[MAX_BUFFER];
memset(buf, 0, sizeof(buf));
int nRead = read(fd, buf, len);
if (nRead < 0 && errno != EAGAIN) {
removeConn(fd);
return;
}
std::string buffer = std::string(buf);
std::regex pattern("([^\\s]+)", std::regex_constants::icase);
std::sregex_iterator it(buffer.begin(), buffer.end(), pattern);
std::sregex_iterator end;
std::vector<std::string> strs;
while (it != end) {
strs.push_back(it->str());
++it;
}
if (!strs.size()) {
removeConn(fd);
return;
}
auto method = strs[0];
auto url = strs[1];
auto version = strs[2];
if (url == "/") {
url = "/index.html";
}
auto it1 = UrlHandlers.find(std::string(url));
if (it1 != UrlHandlers.end()) {
it1->second(fd);
} else {
removeConn(fd);
return;
}
}
客户端请求的响应
在处理完成客户端的请求之后,我们需要将处理结果构造为响应结果,并发送给客户端。在实现过程中,我们采用多线程池的方式进行TCP数据的异步读写处理。
bool Server::sendResponse(Response& res)
{
std::stringstream ss;
std::unique_lock<std::mutex> lock(m_mtx);
// 1. 发送响应头
ss << "HTTP/1.1 " << res.getCode() << " OK\r\n";
for (auto it = res.getHeader().begin(); it != res.getHeader().end(); ++it) {
ss << it->first << ": " << it->second << "\r\n";
}
ss << "\r\n";
len_ += ss.str().length();
if (ssl) {
// SSL write
} else {
write(fd_, ss.str().c_str(), ss.str().length());
}
lock.unlock();
// 2. 发送响应体
if (res.body.length()) {
if (ssl) {
// SSL write
} else {
m_threadPool->AddTask(ResponseTask(fd_, res.body.c_str(), res.body.length()));
}
len_ += res.body.length();
}
return true;
}
处理函数的示例
下面是一个示例处理函数,它会返回一个HTML页面。
void Server::handleIndex(int fd)
{
Response res;
res.setCode(ResponseCode::OK);
res.setBody("<html>\r\n<head>Web Server</head>\r\n<body>\r\nThis is Web Server!\r\n</body></html>");
res.addHeader("Content-Length", std::to_string(res.getBody().length()));
res.addHeader("Content-Type", "text/html");
sendResponse(res);
}
支持并发处理请求的方式
在本次实现中,我们采用了多线程编程和多进程编程的方式对客户端请求进行异步处理。如果使用多线程,我们可以通过线程池的方式进行高效处理;如果使用多进程,则可以创建多个子进程进行超高并发处理。下面是一个多进程的实例示例:
bool Server::run()
{
pid_t pid;
for (int i = 0; i < processNum_; ++i) {
if ((pid = fork()) < 0) {
std::cout << "fork failed." << std::endl;
return false;
} else if (pid == 0) {
break;
} else {
m_childs.push_back(pid);
}
}
if (pid == 0) {
loop();
return true;
} else {
while(1) {
wait(nullptr);
if (errno == ECHILD && status == 0) {
std::cout << "All servers has exited." << std::endl;
return true;
}
}
return true;
}
}
完整代码
完整代码可以在我的GitHub仓库中获取:
总结
本次攻略主要讲解了如何使用C++实现一个简单的Web服务器,包括参数解析、套接字设置和监听、客户端请求的处理、响应的发送和多线程处理等方面的实现方法,同时也讲解了一些工具和库的使用方法。这个服务器并没有很长的代码,但是需要理解的知识点还是很多的。通过学习本节知识,你将掌握如何使用C++制作Web服务器的技巧,希望本篇攻略对你有所帮助。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:使用C++制作简单的web服务器(续) - Python技术站