Н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

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

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


#include
#include
#include

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
#include
#include
#include

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
Средняя: 5 (4 оценки)

Комментарии

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

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

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

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-теги не обрабатываются и показываются как обычный текст
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Строки и параграфы переносятся автоматически.