用 Linux epoll 實現高效能 HTTP 伺服器
阿新 • • 發佈:2018-12-19
用 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 = "";
}