用 Linux epoll 实现高性能 HTTP 服务器
为了代码的整洁性,本文章所介绍功能将使用 C++ 实现。实际使用中可转为 C 语言使用。
此项目只能在Linux下使用,windows请绕道。
项目概括
本项目是使用 Linux epoll 实现的一个简单的 HTTP 服务器。仅支持 HTTP 1.0、GET 和 HEAD 方法,对 HTTP 请求报文仅使用正则表达式进行解析。在实际使用中,请使用词法和语法分析来实现请求报文的内容解析。
什么是 epoll
epoll 是 Linux 内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
准备编译环境
本程序需要 gcc,make 相关(make 不是必须)。可通过以下命令安装:
$ sudo apt-get install gcc
$ sudo apt-get install make
项目代码
项目配套代码已上传到 GitHub,项目地址 https://github.com/ZiFung/epoll-http-server/
将项目源码下载下来后,用 terminal 打开,输入
$ make
后输入
$ ./my_http [port]
即可运行
项目成果
本项目经过 GitHub 上的开源项目 wrk 进行压力测试,在测试虚拟机上测得11000并发。
完整代码
项目结构
- epoll_http/
- html/
- index.html
- main.cpp
- HttpServer.hpp
- HttpServer.cpp
- HttpResponse.hpp
- HttpResponse.cpp
- html/
代码
// main.cpp
#include "HttpServer.hpp"
#include <stdlib.h>
#include <string.h>
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
// 端口处理
unsigned short port = 80;
if (argc > 3)
{
cerr << "At most two arguments." << endl;
return -1;
}
if (argc == 2)
port = atoi(argv[1]);
else if (argc == 3)
{
if (strcmp(argv[1], "--port") != 0)
{
cerr << "Unknown command \"" << argv[1] << "\"" << endl;
return -2;
}
port = atoi(argv[2]);
}
// 新建服务器
HttpServer *server = new HttpServer();
if (!server->init_server(port))
{
cerr << "Failed in initializing server!" << endl;
return -3;
}
// 启动服务器
server->start_serving();
delete server;
return 0;
}
// HttpServer.hpp
#ifndef HTTPSERVER_HPP
#define HTTPSERVER_HPP
#include "HttpResponse.hpp"
#include <fstream>
#include <sys/epoll.h>
#define MAX_SOCK_SIZE 1024
#define FD_SIZE 1000
#define EPOLLEVENTS_SIZE 1000
#define BUFFER_SIZE 1024 * 1024
using namespace std;
class HttpServer
{
public:
HttpServer();
~HttpServer();
bool init_server(unsigned short port);
void start_serving();
private:
void add_event(int fd, int state);
void modify_event(int fd, int state);
void delete_event(int fd);
private:
int listen_fd = -1;
int epfd = -1;
struct epoll_event events[EPOLLEVENTS_SIZE];
HttpResponse client_data[MAX_SOCK_SIZE];
};
#endif // HTTPSERVER_HPP
// HttpServer.cpp
#include "HttpServer.hpp"
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
using namespace std;
HttpServer::HttpServer()
{
}
HttpServer::~HttpServer()
{
if (listen_fd > 0)
{
close(listen_fd);
listen_fd = -1;
}
}
bool HttpServer::init_server(unsigned short port)
{
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listen_fd)
{
cerr << "Error: failed to create socket" << endl;
return false;
}
bool opt = false;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(&opt));
sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(port);
saddr.sin_addr.s_addr = htonl(0);
if (::bind(listen_fd, (sockaddr*)&saddr, sizeof(saddr)) != 0)
{
cerr << "Error: failed to bind port (" << port << ")!" << endl;
return false;
}
if (listen(listen_fd, 5) < 0)
{
cerr << "Error: failed to listen!!!" << endl;
return false;
}
epfd = epoll_create(FD_SIZE);
add_event(listen_fd, EPOLLIN);
return true;
}
void HttpServer::start_serving()
{
for (;;)
{
int ret = epoll_wait(epfd, events, EPOLLEVENTS_SIZE, -1);
for (int i = 0; i < ret; i++)
{
if (events[i].data.fd == listen_fd)
{
sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int client = accept(listen_fd, (sockaddr*)&caddr, &len);
if (client <= 0)
{
cerr << "==> Accept client failed!!!" << endl;
continue;
}
add_event(client, EPOLLIN);
}
else
{
int client = events[i].data.fd;
if (events[i].events & EPOLLIN)
{
bool re = false;
if (!re)
{
char buf[BUFFER_SIZE] = { 0 };
int revclen = recv(client, buf, BUFFER_SIZE, 0);
if (revclen <= 0)
{
close(client);
delete_event(client);
client_data[client].request = "";
continue;
}
client_data[client].request += buf;
re = BUFFER_SIZE != revclen;
if (!re)
continue;
}
if (!client_data[client].parse_request())
{
close(client);
delete_event(client);
client_data[client].request = "";
continue;
}
modify_event(client, EPOLLOUT);
}
else if (events[i].events & EPOLLOUT)
{
char buf[BUFFER_SIZE] = { 0 };
int data_read = client_data[client].read_data(buf, BUFFER_SIZE);
int data_sent = send(client, buf, data_read, MSG_NOSIGNAL);
if (data_sent == 0)
continue;
if (data_sent < 0)
{
delete_event(client);
client_data[client].close();
close(client);
continue;
}
client_data[client].forward(data_sent);
if (client_data[client].is_reading_finished())
{
delete_event(client);
client_data[client].close();
close(client);
}
}
}
}
}
close(listen_fd);
}
void HttpServer::add_event(int fd, int state)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = state;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
void HttpServer::modify_event(int fd, int state)
{
struct epoll_event ev;
ev.data.fd = fd;
ev.events = state;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
void HttpServer::delete_event(int fd)
{
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
// HttpSponse.hpp
#ifndef HTTPRESPONSE_HPP
#define HTTPRESPONSE_HPP
#include <string>
#include <fstream>
using namespace std;
class HttpResponse
{
public:
HttpResponse();
~HttpResponse();
string request = "";
bool parse_request();
int read_data(char *buf, int buf_size);
void forward(int offset);
bool is_reading_finished();
void close();
private:
ifstream *in_file = NULL;
string buffer = "";
};
#endif // HTTPRESPONSE_HPP
// HttpResponse.cpp
#include "HttpResponse.hpp"
#include <iostream>
#include <string.h>
using namespace std;
HttpResponse::HttpResponse()
{
}
HttpResponse::~HttpResponse()
{
}
bool HttpResponse::parse_request()
{
string type = "GET";
string path = "/";
if (request[3] == ' ')
{
int i;
for (i = 4; request[i] != '?' && request[i] != ' '; i++);
path = request.substr(4, i - 4);
}
else
type = "HEAD";
buffer += "HTTP/1.1 200 OK\r\n";
unsigned long file_size = 0;
if (type == "GET")
{
string filename = path;
if(path == "/")
filename = "/index.html";
string filepath = "html";
filepath += filename;
in_file = new ifstream(filepath, ios::in | ios::binary);
if(!in_file->is_open())
{
in_file = new ifstream("html/404.html", ios::in | ios::binary);
if(!in_file->is_open())
{
cerr << "Open file html/404.html failed!!!" << endl;
return false;
}
}
// Get the length of the file required.
in_file->seekg(0, ios::end);
file_size = in_file->tellg();
in_file->seekg(0, ios::beg);
buffer += "Content-Length: ";
buffer += to_string(file_size);
buffer += "\r\n";
}
buffer += "\r\n";
request = "";
return true;
}
int HttpResponse::read_data(char *buf, int buf_size)
{
if ((unsigned int)buf_size >= buffer.size() && buffer.size() != 0)
{
strcpy(buf, buffer.c_str());
if (in_file == NULL)
return (int)buffer.size();
in_file->read(buf + buffer.size(), buf_size - (int)buffer.size());
int data_read = in_file->gcount();
in_file->seekg(-1 * data_read, ios::cur);
return (int)buffer.size() + data_read;
}
else if (buffer.size() > 0)
{
string temp = buffer.substr(0, buf_size);
strcpy(buf, temp.c_str());
return buf_size;
}
else
{
if (in_file == NULL)
return -1;
in_file->read(buf, buf_size);
int data_read = in_file->gcount();
in_file->seekg(-1 * data_read, ios::cur);
return data_read;
}
}
void HttpResponse::forward(int offset)
{
if (buffer.size() > 0 && offset > (int)buffer.size())
{
offset -= (int)buffer.size();
buffer = "";
in_file->seekg(offset, ios::cur);
}
else if (buffer.size() > 0)
buffer = buffer.substr(offset - 1, buffer.size() - offset);
else
in_file->seekg(offset, ios::cur);
}
bool HttpResponse::is_reading_finished()
{
if (in_file != NULL)
return in_file->peek() == EOF;
return buffer.size() == 0;
}
void HttpResponse::close()
{
if (in_file != NULL)
in_file->close();
in_file = NULL;
buffer = "";
}