Pull to refresh

Самый короткий веб-сервер на с++

Reading time 5 min
Views 61K
В предыдущей статье я рассказал, как написать простой сервер для передачи одного файла по протоколам http и https. Прошло немного времени и я решил сделать из этого кода универсальную библиотеку для быстрого создания серверов.

Полный код библиотеки можно посмотреть на гитхабе, а если в двух словах, то я добавил немного «египетских скобок», новомодных лямбда-функций и шаблонов. На сегодняшний день результатом стала кроссплатформенная библиотека для создания асинхронных серверов, состоящая из 5 файлов с общим размером 22.5 килобайт. Версия библиотеки для Линукс состоит из одного файла размером 18 килобайт (517 строк кода).

В этой статье я коротко расскажу, как работает библиотека и покажу, как с ее помощью написать полностью работоспособный веб-сервер для статических сайтов.


Весь код веб-сервера, который я хочу представить, находится в двух файлах.
Первый файл называется serv.cpp и содержит минимальное количество кода:
#include "http_server.h"
using namespace server;

CServer<CHttpClient> s(8085, 1111);

int main() {return 0;}


В первой строке включается файл, где написан весь остальной код веб-сервера. В четвертой строке инициируется низкоуровневая библиотека, о которой я сказал в начале поста.

Как видно, для работы с библиотекой первым делом нужно создать переменную типа CServer, в которую нужно передать имя пользовательского класса и номера портов которые сервер будет слушать.
В приведенном примере библиотека инициируется с классом CHttpClient (о нем речь чуть ниже), с портом 8085 для приема tcp соединений и портом 1111 для ssl.

Работа библиотеки построена на обмене сообщениями с пользовательским классом. На сегодняшний день определены следующие сообщения:
	enum MESSAGE {
		I_READY_EPOLL,
		I_ACCEPTED,
		I_READ,
		I_ALL_WROTE,
		PLEASE_READ,
		PLEASE_WRITE_BUFFER,
		PLEASE_WRITE_FILE,
		PLEASE_STOP
	};


Сообщения, начинающиеся на «I_» посылает библиотека, а сообщения, начинающиеся на «PLEASE_» можно посылать библиотеке.
Для того, чтобы реализовать веб-сервер достаточно описать такой класс:
	class CHttpClient
	{
	public:
		const MESSAGE OnAccepted(shared_ptr<vector<unsigned char>> pvBuffer) 
		{***}
		const MESSAGE OnWrote(shared_ptr<vector<unsigned char>> pvBuffer)
		{***}
		const MESSAGE OnRead(shared_ptr<vector<unsigned char>> pvBuffer)
		{***}
	};
 

Эти три публичные функции обязательны, поскольку их вызывает библиотека для обмена сообщениями и данными.
Обмен данными происходит через «буфер обмена», который является одновременно входным и выходным параметром функций.

Сначала я хотел поэтапно расписать тут создание класса CHttpClient но потом решил, что при желании хабровчане смогут и без меня разобраться в 115 строчках кода с комментариями. Поэтому просто приведу его здесь целиком:
Исходник http_server.h
#include "server.h"

#define ROOT_PATH		"./wwwroot"
#define ERROR_PAGE		"error.html"
#define DEFAULT_PAGE	"index.html"

namespace server
{
	class CHttpClient
	{
		int m_nSendFile;
		off_t m_nFilePos;
		unsigned long long m_nFileSize;

		enum STATES	{
			S_READING_HEADER,
			S_READING_BODY,
			S_WRITING_HEADER,
			S_WRITING_BODY,
			S_ERROR
		};
		STATES m_stateCurrent;
		map<string, string> m_mapHeader;

		void SetState(const STATES state) {m_stateCurrent = state;}
		const bool ParseHeader(const string strHeader)
		{
			m_mapHeader["Method"] = strHeader.substr(0, strHeader.find(" ") > 0 ? strHeader.find(" ") : 0);
			if (m_mapHeader["Method"] != "GET") return false;
			
			const int nPathSize = strHeader.find(" ", m_mapHeader["Method"].length()+1)-m_mapHeader["Method"].length()-1;
			if (nPathSize < 0)	return false;
			m_mapHeader["Path"] = strHeader.substr(m_mapHeader["Method"].length()+1, nPathSize);
			
			return true;
		}
		const MESSAGE OnReadHeader(const string strHeader, shared_ptr<vector<unsigned char>> pvBuffer)
		{
			cout << "Header read\n";
			if (!ParseHeader(strHeader))	m_mapHeader["Path"] = ERROR_PAGE;
			if (m_mapHeader["Path"] == "/") m_mapHeader["Path"] += DEFAULT_PAGE;

			cout << "open file" << ROOT_PATH << m_mapHeader["Path"].c_str() << "\n";
			if ((m_nSendFile = _open((ROOT_PATH+m_mapHeader["Path"]).c_str(), O_RDONLY|O_BINARY)) == -1)
				return PLEASE_STOP;
			
			struct stat stat_buf;
			if (fstat(m_nSendFile, &stat_buf) == -1)
				return PLEASE_STOP;

			m_nFileSize = stat_buf.st_size;

			//Добавляем в начало ответа http заголовок
			std::ostringstream strStream;
			strStream << 
				"HTTP/1.1 200 OK\r\n"
				<< "Content-Length: " << m_nFileSize << "\r\n" <<
				"\r\n";

			//Запоминаем заголовок
			pvBuffer->resize(strStream.str().length());
			memcpy(&pvBuffer->at(0), strStream.str().c_str(), strStream.str().length());
			return PLEASE_WRITE_BUFFER;
		}
		explicit CHttpClient(CHttpClient &client) {}
	public:
		CHttpClient() : m_nSendFile(-1), m_nFilePos(0), m_nFileSize(0), m_stateCurrent(S_READING_HEADER) {}
		~CHttpClient()
		{
			if (m_nSendFile != -1) _close(m_nSendFile);
		}
			
		const MESSAGE OnAccepted(shared_ptr<vector<unsigned char>> pvBuffer) {return PLEASE_READ;}
		const MESSAGE OnWrote(shared_ptr<vector<unsigned char>> pvBuffer)
		{
			switch(m_stateCurrent) {
				case S_WRITING_HEADER:
					if (m_nSendFile == -1)
						return PLEASE_STOP;

					SetState(S_WRITING_BODY);
					pvBuffer->resize(sizeof(int));
					memcpy(&pvBuffer->at(0), &m_nSendFile, pvBuffer->size());
					return PLEASE_WRITE_FILE;
				default:
					return PLEASE_STOP;
			}
		}
		const MESSAGE OnRead(shared_ptr<vector<unsigned char>> pvBuffer)
		{
			switch(m_stateCurrent) {
				case S_READING_HEADER:
				{
					//Ищем конец http заголовка в прочитанных данных
					const std::string strInputString((const char *)&pvBuffer->at(0));
					if (strInputString.find("\r\n\r\n") == strInputString.npos)
						return PLEASE_READ;

					switch(OnReadedHeader(strInputString.substr(0, strInputString.find("\r\n\r\n")+4), pvBuffer)) {
						case PLEASE_READ:
							SetState(S_READING_BODY);
							return PLEASE_READ;
						case PLEASE_WRITE_BUFFER:
							SetState(S_WRITING_HEADER);
							return PLEASE_WRITE_BUFFER;
						default:
							SetState(S_ERROR);
							return PLEASE_STOP;
					}
				}
				default: return PLEASE_STOP;
			}
		}
	};
}



Немного пояснений:
в первой строке включается моя низкоуровневая микробиблиотека,
потом определяются каталог и страницы по умолчанию для сайта,
потом идут функции для управления состоянием класса и парсинга заголовка запроса,
callback-функции возвращают в библиотеку сообщения в зависимости от текущего состояния класса.

Итак, на гитхабе можно найти готовый проект для Visual Studio 2012.
Для Линукса нужны только файлы serv.cpp, server.h, http_server.h и ca-cert.pem. Компилятор gcc 4.5 и выше: «g++ -std=c++0x -L/usr/lib -lssl -lcrypto serv.cpp»

Если ничего в коде не менять, то чтобы проверить работу сервера нужно еще в каталог ./wwwroot поместить хотя бы файл index.html

Проверить сервер в работе можно по адресу:
http://unblok.us:8085/

или
https://unblok.us:1111
Испытание на хаброэффект сервер благополучно завалил, в чем причина буду разбираться.вроде разобрался — исправил.
Tags:
Hubs:
+15
Comments 98
Comments Comments 98

Articles