1. 程式人生 > >用 Linux epoll 實現高效能 HTTP 伺服器

用 Linux epoll 實現高效能 HTTP 伺服器

用 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

程式碼

// 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 = "";
}