НTTP-сервер своими руками. Назад к TCP

Стоит задача написать простой HTTP сервер. Сервер должен принимать от клиента запрос. После сего сервер возвращает в ответе тело самого запроса. Работает с несколькими клиентами одновременно. В этой части мы расскажем о низкоуровневой части сервера, которая более относится к TCP.

Ранее мы сказали, что написание HTTP сервера, сводится к задаче написания TCP сервера. Например, можно воспользоваться готовой реализацией с http://habrahabr.ru/blogs/programming/70796/
Здесь мы приведем свою, правда не сильно отличающуюся от сотен других.

Хочу сразу предупредить читателей, что я не являюсь гуру системного программирования, и при возникновении трудностей лучше обращаться к книге У. Р. Стивенса <<Разработка сетевых приложений>>, a еще лучше к man.

Инициализация сервера

Далее, чтобы наш сервер хотя бы запустился, нам придется провести инициализацию.

Server::Server(int portno, const char* logfilename)
{
  _logfilename = logfilename;
 
  /// Создадим описатели сокета.
  /// AF_INET --- говорит, что используем ipv4
  /// 	 Если хотим работать с локальными ресурсами,
  /// 	 то надо указать AF_UNIX или AF_LOCAL
  /// 	 Если хотим ipv6 --- AF_INET6
  /// SOCK_STREAM --- сокет постоянного соединения.
  /// 	 как понимаю, если используем TCP, то указывать 
  /// 	 надо именно его.
  /// IPPROTO_TCP (= 0) --- тип протокола.
  /// 	 Вообще, если мы просто поставим 0, то это будет 
  /// 	 означать, что мы используем протокол по-умолчанию
  /// 	 для данного сокета.  bit.ly/fsfkCq
 
  _socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 
  /// Создадим структуру
  struct sockaddr_in serv_addr;
 
  /// Обнулим структуру
  memset((char *) &serv_addr, 0, sizeof(serv_addr));
 
  /// Укажем тип домена AF_INET.
  serv_addr.sin_family = AF_INET; 
 
  /// Укажем, что любой адрес входящий.
  serv_addr.sin_addr.s_addr = INADDR_ANY;
 
  /// Укажем номер порта.
  serv_addr.sin_port = htons(portno);
 
  /// Сопоставляем адрес к сокетом.[не дописано].
  /// Внутри содержит 
  ///   setsockopt и bind.
 
  _bind(serv_addr);
 
  /// Начинаем слушать.  CLIENT_MAX_NUMBER --- константа класса Server
  /// Это максимальная длинна очереди ожидающих запросов на соединение.
  _listen(CLIENT_MAX_NUMBER);
 
  /// Далее мы знаем, что будет использоваться fork ().
  /// Если процесс созданный с помощью fork уже отработал,
  ///   то он ждет завершения своего родителя (зомби-процесс).
  ///   Ресурсы системы он освободит, однако, 
  ///   в таблице процессов будет присутствовать.
  ///   Таблица процессов имеет ограниченный размер.
  ///   И может когда-то кончиться. 
  ///   Это особенно актуально, с учетом, что мы пишем 
  ///   HTTP-сервер.  Функция ниже заставляет родителя 
  ///   не ждать своих отработавших потомков.
  ///   Система  сама удаляет все ресурсы.
 
  _zombie_handling();
}

Функции _socket, _bind, _listen --- являются обертками стандартных функций. Это члены нашего класса Server. Предварительно лучше воспользоваться man socket, man bind, man listen.

Код функий приведен ниже:

void
Server::_socket(int family, int type, int protocol)
{
  _listen_socket = socket(family, type, protocol);
  if(0 > _listen_socket){
    error("socket error");
  }
}

int _listen_socket --- член класса Server. Это наш слушающий сокет.

void
Server::_bind(struct sockaddr_in &serv_addr)
{
  int on = 1;
  int n;
  n = setsockopt(_listen_socket, SOL_SOCKET, SO_REUSEADDR,
    (char *)&on, sizeof(on));
  if (0 > n){
    error("setsockopt error");
  }
  n = bind(_listen_socket, (struct sockaddr *) &serv_addr,
    sizeof(serv_addr));
  if (0 > n)
    error("bind error");
}

Перед вызовом bind, не плохо задать настройки, нашему слушающему сокету.
Флаг SO_REUSEADDR отвечает повторное использование локальных адресов для функции bind(). Даже если порт занят в режиме ожидания (TIME_WAIT state), то он все равно будет ассоциирован с этом сокетом. Флаг SOL_SOCKET --- определение уровня сокета. Как я понимаю, иные флаги для сетевых соединений не используются (так сложилось исторически).
(char *)&on, sizeof(on) --- фиктивные параметры указателя на флаг и размера флага.

Server::_listen(int size)
{
  listen(_listen_socket, size);
}

Это даже не интересно.

void Server::_zombie_handling()
{
    struct sigaction sa;
    sigaction(SIGCHLD, NULL, &sa);
    sa.sa_handler = SIG_IGN;
    sigaction(SIGCHLD, &sa, NULL);
}

Cмысл этой конструкции заключается в том, что мы берем старую структуру из sigaction.
Изменяем ее поле и кладем структуру обратно. О странных параметрах sigaction, настоятельно рекомендую почитать man sigaction.

Мы специально вынесли эту функцию отдельно, а не вызываем ее в рабочей функции сервера. На то есть несколько причин.
Во первых. Функция Server::run(), может запускаться несколько раз. И не зачем выполнять один и тот же код.
Во вторых. Правильная работа с зомби важна в любом приложении, которое использует fork. Я думаю, на свете найдется мало TCP-серверов, которые этого не делают.

Если мы решили не только запускать наш сервер но и соединяться с клиентами, то всего скорее нам понадобится использовать функцию accept. В нашем HTTP-сервере мы используем обертку.

bool
Server::_accept()
{
  struct sockaddr_in cli_addr;
  socklen_t clilen;
  clilen = sizeof(cli_addr);
  _data_socket = accept(_listen_socket, (struct sockaddr *)
        &cli_addr, &clilen);
  if (_data_socket < 0)
    return false;
  return true;
}

Внутри обертки члену _data_socket класса Server присваивается дескриптор информационного (присоединенного) сокета. Чтение и запись данных происходит именно по этому дескриптору.

В общем случае, чтение и запись должны происходить через низкоуровневые функции.

#include <unistd.h>
 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

Но никто не запрещает написать свои, используя стандартные.
Например, так мы прочитаем, все что нам запишут в сокет.

#include<cstdlib>
#include<cstring>
#include <unistd.h>
 
char *
Server::_sread()
{
  char buffer[BUFFER_SIZE];
  memset(buffer, 0, BUFFER_SIZE);
  short symbols = -1;
  long times = -1;
  long char_size = 0;
  long memo_size = sizeof(char)*BUFFER_SIZE;
  char *res;
  if ((res = (char* )malloc(memo_size)) == NULL){
    perror("ERROR malloc failure");
    return NULL;
  }
  memset(res, 0, memo_size);
  for(;;){
    symbols = read(_data_socket, buffer, BUFFER_SIZE - 1);
    if (0 > symbols) {
      perror("Error reading from socket");
      free(res);
      res = NULL;
      return res;
    }
    char_size += symbols;
    if(char_size >= memo_size){
      memo_size += sizeof(char) * (BUFFER_SIZE);
      if ((res = (char* )realloc (res, memo_size)) == NULL)  {
        fprintf(stderr,
            "ERROR realloc failure: memo_size = %li", memo_size );
        free(res);
        res = NULL;
        return res;
      }
    }
    // TODO use memcpy
    res = strncat(res, buffer, symbols);
    if(BUFFER_SIZE - 1 > symbols){ // REQUEST
      break;
    }
    symbols = -1;
    memset(buffer, 0, BUFFER_SIZE);
    times += 1;
  }
  return res;
}

А вот так, мы сможем реализовать свой форматный вывод:

#include<unistd.h>
#include<cstdlib>
#include<cstring>
#include<cstdarg>
 
void
Server::_sprintf(const char* fmt, ...)
{
  int symbols;
  size_t size = BUFFER_SIZE;
  char *p = NULL;
  va_list ap;
  if ((p = (char* )malloc (sizeof(char) * size)) == NULL){
    perror("ERROR malloc failure");
    if(p) free(p);
    p = NULL;
    return;
  }
  for(;;){
    memset(p, 0, size);
    va_start(ap, fmt);
    symbols = vsnprintf (p, size, fmt, ap);
    va_end(ap);
    if (symbols > -1 && (size_t )symbols < size)
      break;
    if (symbols > -1){ 
      // для glibc 2.1
      size = symbols + 1;
    }
    else{
      // для glibc 2.0
      size *= 2;
    }
    if ((p = (char* )realloc(p, sizeof(char) * size)) == NULL)  {
      perror("ERROR realloc  failure");
      if(p) free(p);
      p = NULL;
      return;
    }
  }
  int status = write(_data_socket, p, (size_t)symbols);
  if (0 > status)
    perror("ERROR writing to socket");
  if(p) free(p);
  p = NULL;
}

Более того, на сокетах можно заставить работать всем знакомые функции форматного ввода-вывода. Например:

FILE *ds_fp = fdopen(_data_socket, "w");
fprintf(ds_fp, "some new data for mr. browser");
fflush(ds_fp);

Только вот fclose тут уже вызывать не стоит. Она закроет соединение с информационным сокетом. Если говорить о закрытии, то в работе сервера так же понадобятся:

void /// Закрытие информационного сокета
Server::_close() 
{
	close(_data_socket);
}
void /// Закрытие информационного сокета, остановка сервера
Server::stop()
{
	close(_listen_socket);
}

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

Ваша оценка: Нет Средняя оценка: 5 (4 votes)
8
w-495

А вот на сколько это эффективно, вопрос остается открытым. Подал я своему HTTP-cерверу 10000 раз головную страничку liberatum.ru через ncat. Вроде справился.
Только форкнулся перед этим в 10000 процессов. Не думаю, что это хорошо.

Ваша оценка: Нет
Отправить комментарий
КАПЧА
Вы человек? Подсказка: зарегистрируйтесь, чтобы этот вопрос больше никогда не возникал. Кстати, анонимные ссылки запрещены.
CAPTCHA на основе изображений
Enter the characters shown in the image.
Linux I класса
Linux II класса
Linux III класса
Счетчики
  • Самый популярный сайт о Linux и Windows 10
О Либератуме

Liberatum — это новости мира дистрибутивов Linux, обзоры, сборки, блоги, а также лучший сайт об Ubuntu*.