НTTP-сервер своими руками

Стоит задача написать простой HTTP сервер. Сервер должен принимать от клиента запрос. После сего сервер возвращает в ответе тело самого запроса. Работает с несколькими клиентами одновременно.

Для описания cервера нужно совсем мало. Нужно понимать протокол HTTP. То как выглядит запрос мы увидим ниже. А вот ответ имеет вид:


HTTP/1.1 200 OK
Content-Type: text\html
Content-Length: {{длина сообщения}}

{{тело сообщения}}

Таким образом, получив от сервера запрос сервер вернет клиенту (браузеру), например:


HTTP/1.1 200 OK
Content-Type: text\html
Content-Length: {{длина сообщения}}

{{заголовок сообщения}};

Request ({{количество запросов}}):

{{текст запроса}}

С другой стороны, возникает вопрос. А как будет происходить обмен. А обмениваться будем по TCP. Таким образом упростили задачу. Чтобы написать HTTP сервер нужно написать TCP сервер. Для определенности будем писать на С++ (вернее на С с классами и шаблонами, и компилировать g++, но никаких потоков и STL использовать не будем). По задумке сервер должен уметь работать с несколькими клиентами одновременно.
Статья начала получаться очень не маленькая, потому решил разбить ее на две.
Здесь мы сосредоточимся на основной рабочей функции сервера.
Технические детали, которые будут общими для любых TCP-серверов, мы решили вынести отдельно: НTTP-сервер своими руками. Назад к TCP

Основная рабочая функция

И так основная рабочая функция будет иметь вид:

/// На момент вызова этой функции, мы предполагаем,
/// что слущающий сокет уже открыт и сделано все нужное.
bool
Server::run()
{
///
/// Здесь, мы создаем объект разделяемой памяти.
/// Если мы хотим считать запросы. То их число надо где-то хранить.
/// Хранить их в обычно памяти не получится (для каждого процесса она своя),
/// потому, мы используем разделяемую.
/// Shared --- класс для удобного взаимодействия с разделяемой памятью.
/// counter_1 --- и есть счетчик наших запросов.
///
Shared counter_1 = Shared();
if(not counter_1.isValid()){
perror("Cannot create objects in counter_1 memory. Sorry.");
return false;
}
counter_1 = 0;
size_t counter_2 = 0;
for(;;){
if(_accept())
/// Eсли к нам кто-то подключился,
/// открываем (принимаем) информационного сокета.
{
///
/// Ниже создаем новый процесс. Описание fork --- man fork.
///
pid_t pid = fork();
if (-1 == pid){
perror("Can't fork");
return false; /// не создался ;(
}
if (0 == pid){
///
/// Запомним время запроса.
///
time_t accept_time; time(&accept_time);
///
/// Прочитаем ВСЕ (!), что нам послал клиент.
/// В общем случае может быть опасно,
/// ибо послать можно сколь угодно большой массив данных.
///
char* client_string = _sread();
if(!client_string) _exit(0);
counter_1 += 1; /// Shared& operator += (const T &new_t)
counter_2 += 1;
///
/// Как мы помним, мы хотим номер запроса вывести на экраServer::н.
/// Но в общем случае, его обработка может быть долго.
/// Потому запомним его номер.
/// В противном случае, пока мы обрабатываем текущий процесс,
/// на сервер придут новые, и counter_1 --- изменится.
///
size_t c1v = counter_1.getValue();
///
/// Формируем тело ответа. Простой HTML и СSS
/// ssprintf --- наш аналог функции asprintf (man asprintf).
/// Она выделяет память для строки (в отличие от sprintf)
/// и осуществляет в нее форматный вывод.
/// ssprintf возвращает саму строку.
///
char* html = ssprintf(
"%s"
""
"Request (%lu):\n"
"%s"
"\n\n",
"Request", c1v, client_string);
///
/// А вот тут происходит какая-то долгая операция.
/// Например, решение уравнения прочности волны, по методу Галеркина.
///
sleep(10);
///
/// Формируем сам ответ и посылаем его в сокет.
/// Реализация _sprintf будет дана ниже.
///
_sprintf("HTTP/1.1 200 OK\n"
"Content-Type: text\\html\n"
"Content-Length: %d\n"
"\n%s\n",
strlen(html), html);
//:) обязательно всех освобождаем ...
free(html); html = NULL;
///
/// Запомним время ответа.
///
time_t exit_time; time(&exit_time)
///
/// Кроме того, вспомним, что мы так же хотели писать лог сервера.
///
char* exit_time = getTime();
FILE* logfile;
logfile = fopen(_logfilename, "a");
if(NULL == logfile){
fprintf(stderr, "Cannot to open logfile\n");
fflush(stderr);
}
///
/// Запишем время запроса и ответа в красивом виде.
/// ГГ.ММ.ДД ЧЧ:мм:CC
///
char* fat = getFormatedTime(accept_time);
char* fet = getFormatedTime(exit_time);
/// Запишем их в лог.
fprintf(logfile, "%2.2lu [%s] -> [%s]:\n%s\n\n",
c1v, fat, fet, client_string);
//:) опять всех освободим
free(fat); fat = NULL;
free(fet); fet = NULL;
free(client_string); client_string = NULL;
/// Нужно использовать fflush,
/// чтобы сбросить буфер.Укажем
fflush(logfile);
fclose(logfile);
//:) ... и тихо уходим.
_exit(0);
}
/// Мы отработали и закрываем соединение
/// информационного сокета.
_close();
}
}
}

На самом деле, все просто. На логическом уровне, мы откуда-то (вернее из сокета) получаем строку, как-то ее обрабатываем и возвращаем ответ (в сокет). Сложность может заключаться в одновременно работать с несколькими клиентами и в технической реализации этого хозяйсва.

Простые функции _accept(), _close(), _read(), _sprintf являются членами класса Server и описаны НTTP-сервер своими руками. Назад к TCP.

Самой сложно и опасной вещью в нашем сервере, является разделяемая память. Проблема даже не в том, что она должна быть, и совсем не в том, что она не стандартным образом создается и удаляется.


#include

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

Сложность в том, что два параллельных процесса начнут к такой памяти обращаться, и в итоге там может получиться каша.

Попробуем все сказанное учесть в коде ниже. Описание шаблонного класса приводится целиком.


#include
#include
#include

extern "C" {
#include
#include
#include
}

template
class Shared{
public:
Shared(size_t len = 1){
if(0 == len){
_isValid = false;
_t = NULL;
}
else{
_isValid = true;
size_t _size = sizeof(*_t)*len;

/// Произвольное имя объекта разделяемой памяти.
/// В старых ядрах рекомендовали использовать
/// /dev/shm
const char* shm_name = "shm-name";

///
/// Создадим новый объект в памяти.
/// man shm_open
///
int fd = shm_open(shm_name, O_RDWR | O_CREAT, 0777);
if (fd == -1) {
fprintf(stderr, "Open failed : %s\n", strerror(errno));
_isValid = false;
_t = NULL;
}

///
/// Зададим ему размер (man ftruncate)
///
if (ftruncate(fd, _size ) == -1) {
fprintf(stderr, "ftruncate : %s\n", strerror(errno));
_isValid = false;
}

///
/// Аллоцируем память для нашего объекта.
/// man mmap
/// По сути, мы создали некий виртуальный файл.
/// А с помощью ftruncate привели его
/// к нужному размеру и забили нулями.
///
_t = (T*)mmap(0, _size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (_t == MAP_FAILED) {
fprintf(stderr, "mmap failed:%s\n", strerror(errno));
_isValid = false;
_t = NULL;
exit(1);
}
close(fd);
shm_unlink(shm_name);

///
/// Создаем мютекс,
/// для блокирования обращения к памяти
///
pthread_mutex_init(&_mutex, NULL);
}
}

///
/// При вызове деструктора освобождаем память.
/// и удаляем мютекс.
///
~Shared(){
munmap(_t, _size);
pthread_mutex_destroy(&_mutex);
}

/// Показывает, возможно ли обращение
/// к разделяемому объекту.
bool isValid(){
return _isValid;
}

///
/// Ниже представлен набор простых функций доступа.
/// Тут и начинается все интересное.
///
T* getPointer(){
return _t;
}
T getValue() const {
return *_t;
}

T& V(){
return *_t;
}

void setValue(const T& new_t){
/// захватили мютекс,
/// пока он закрыт нами,
/// никто не может в него писать кроме нас
if(0 == pthread_mutex_lock(&_mutex)){
*_t = new_t;
/// изменили значение и освободили мютекс
if(pthread_mutex_unlock(&_mutex)){
perror("pthread_mutex_unlock failed\n");
}
}
else{
perror("pthread_mutex_lock failed\n");
}
}

Shared& operator = (const T &new_t){
setValue(new_t);
return *this;
}

Shared& operator += (const T &new_t){
setValue(getValue() + new_t);
return *this;
}

private:
/// указатель на разделяемый объект типа T
T *_t;
/// размер разделяемого объекта типа T
size_t _size;
/// состояние, можно к указателю t обращаться, или нет.
bool _isValid;
/// наш мютекс
pthread_mutex_t _mutex;
};

Таким образом, используя шаблонны класс Shared мы можем спокойно использовать счетчик объектов в разделяемой памяти.

Полный исходный HTTP-код сервера
https://github.com/w495/Very-Simple-HTTP-Server-VShS-1
Постараюсь еще так же далее с ним играться.
Если, в чем-то наврал, поправьте пожалуйста.

Оценка: 
5
Средняя: 5 (3 оценки)

Комментарии

Да, меня уже пинали за отсутствие правильного отображения и подсветки кода. В течении дня прикручу.

Оценка: 
Средняя: 5 (1 оценка)

Вроде сделал. Сорри, что придется < и > обратно менять. :) И еще надо подумать как дефолтный CSS-стиль поменять, а то серое окно и белая рамка как-то не очень вписываются в общее оформление сайта.

Синтаксис такой:
<code language="язык"></code>, либо <язык></язык>.

Языки пока такие: c, cpp, drupal5, drupal6, java, javascript, php, python, ruby. Остальное будем добавлять по мере необходимости. Фича в стадии тестирования и доступна только некоторым пользователям. Просьба сообщать о найденных ошибках.

Оценка: 
Пока без оценки
w495 аватар

Иттить! Круто однако. А где в принципе можно обсуждать технические вопросы (например меня интересует на чем написан liberatum и с какими либами), и делать [технические] предложения.

Оценка: 
Пока без оценки

Технические предложения по сайту с радостью рассматриваются здесь. Пожаловаться на глюк можно здесь. Другие технические вопросы можно обсудить здесь.

Оценка: 
Пока без оценки

По листингам видно, что надо еще включить подсветку html. Заодно, на будущее, включу bash, css, sql и xml.

Оценка: 
Пока без оценки

Комментировать

Filtered HTML

  • Use [fn]...[/fn] (or <fn>...</fn>) to insert automatically numbered footnotes.
  • Доступны HTML теги: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote> <strike> <code> <h2> <h3> <h4> <h5> <del> <img>
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Строки и параграфы переносятся автоматически.

Plain text

  • HTML-теги не обрабатываются и показываются как обычный текст
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Строки и параграфы переносятся автоматически.