Н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/
Постараюсь еще так же далее с ним играться.
Если, в чем-то наврал, поправьте пожалуйста.
Комментарии
w495
26 августа, 2011 - 06:53
А вот на сколько это эффективно, вопрос остается открытым. Подал я своему HTTP-cерверу 10000 раз головную страничку liberatum.ru через ncat. Вроде справился.
Только форкнулся перед этим в 10000 процессов. Не думаю, что это хорошо.
Комментировать