Контакты

Для связи с нами можно использовать:
irc://irc.chatnet.ru:#gfs
icq://546460
email://cobalt[@]gfs-team.ru

Все материалы предоставлены только с ознакомительной целью
ГлавнаяСтатьиКодингОсновы http протокола: разработка компактного web сервера
© Sun Inventor 10.05.2007 статья не оптимизирована

Исходник к статье тут
  1. Введение
  2. Основы http протокола
  3. Реализация web сервера
  4. Заключение

1. Введение

  Раньше наиболее используемой технологией, технологией ради которой люди 
пользовались Интернет была электронная почта. После появления http и web 
технологий произошел буквально переворот, и главное место в сети начали 
занимать именно web ресурсы. Поэтому темой данной статьи и является http 
протокол. Изучение этого протокола даст хороший фундамент для понимания 
web технологий, а как известно нет лучшего пути разобраться в проблеме чем 
написать программу из проблемной области. Примеры, приведенные здесь 
написаны на Си и тестировались в ОС Linux.

2. Основы http протокола

 Протокол http реализован на прикладом уровне модели OSI и базируется на 
архитектуре клиент - сервер. Клиент посылает серверу запрос на который 
сервер в свою очередь отвечает. Простой пример: вы решили зайти на 
www.google.ru для того чтобы что-нибудь найти. Вы (то есть клиент) 
применяете для этого свой браузер, который, являясь клиентским 
приложением, отсылает запрос серверу, в котором просит предоставить 
ему содержимое главной страницы сайта. Сервер обрабатывает этот запрос 
и посылает вам ответ, содержащий запрашиваемую страницу. Ваш браузер в 
свою очередь обрабатывает html код и в результате
вы видите то что вы видите.

 Запрос в большенстве случаев выглядит так:

<метод> <запрашиваемый_ресурс> HTTP/1.1<\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> ... <заголовочное_поле>: <значение><\n> <\n>

 Метод - характеризует текущий запрос. Их несколько, 
например head, post, put, delete, get и другие. Здесь будет рассмотрен 
метод get как наиболее часто используемый. Он предназначен для того 
чтобы что-нибудь получить с сервера, например главную страницу 
www.google.ru, как это было сделано во введении.

Запрашиваемый ресурс - например index.html
HTTP/1.1 - протокол и его версия.
<заголовочное_поле>: <значение> - преднозначенно для 
указания дополнительных параметров таких как тип передоваемого 
содержимого, кодировка итд.

 Ответ сервера выглядит следующим образом:
 
HTTP/1.1 <код ответа> <сообщение><\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> ... <заголовочное_поле>: <значение><\n> <\n> <тело документа>

Здесь, думаю, стоит пояснить два параметра.

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

  • 200 ОК
  • 201 Успешная команда post
  • 202 Запрос принят
  • 203 Запрос get либо head выполнен
  • 204 Запрос выполнен, но нет содержимого
  • 300 Ресурс обнаружен в нескольких местах
  • 301 Ресурс удален навсегда
  • 302 Ресурс временно удален
  • 304 Ресурс изменен
  • 400 Плохой запрос от клиента
  • 401 Неавторизованный запрос
  • 402 Необходима оплата за запрос
  • 403 Доступ к ресурсу запрещен
  • 404 Ресурс не найден
  • 405 Метод неприменим для данного ресурса
  • 406 Недопустимый тип ресурса
  • 410 Ресурс недоступен
  • 500 Внутренняя ошибка сервера
  • 501 Метод не выполнен
  • 502 Перегрузка сервера или неисправный шлюз
  • 503 Сервер недоступен или таймаут шлюза

     сообщение - это то сообщение которое соответствует данному 
    коду ответа. Стандартом они не заданны но все же некоторых моральных устоев 
    придерживаться стоит. Например обычно коду 200 соответствует OK а 404 
    соответствует NOT FOUND.
    

     Ну что, пришло время немного попрактиковаться. Как мне кажется протокол 
    http,     как в принципе и любой другой, гораздо приятнее и нагляднее
    изучать на примере. Для этого предлагаю воспользоваться замечательным 
    сетевым анализатором ethereal, который совершенно бесплатно доступен на
    web-узле www.ethereal.com.
     Проследим что будет происходить если мы напишем в адресной строке 
    браузера www.google.ru/index.html и нажмем Enter. Обратимся к 
    рис.1, на нем видно что
    сниффер поймал 2 пакета первый из них - ваш запрос, второй - ответ
    сервера.
    
    
    
    (рис.1)
    
    
     Стоит пояснить еще одно важное поле - host. Как вы знаете
    к одному ip адресу можно обращаться используя разные dns адреса. Поле host 
    как раз предназначено для конкретизации цели.
     Дальше думаю разъяснять смысла нет, ethereal все расскажет за меня. Также 
    рекомендую вам обратиться к rfc2068, вполне сносный перевод которого вы 
    можете найти например здесь - www.helloworld.ru/texts/comp/inet/http/http1/index.htm.
    

    3. Реализация web сервера.

     Разумеется применение http не ограничивается созданием познавательных/
    увеселительных/итд сайтов. С его помощью можно построить например web 
    интерфейс     для вашей программы. Подобный подход довольно часто 
    применяется в RAT, если интересны конкретные реализации стоит взглянуть 
    на webmin(www.webmin.com). Системами удаленного администрирования применение 
    подобных технологий не ограничивается, взять хотя бы утилиту archmage которая
    реализует весьма интересное решение проблемы просмотра chm файлов.    
    

    Я предлагаю написать небольшой web сервер, который сможет выдавать запрашиваемую клиентом web страницу по методу get. Это во первых послужит хорошим практическим примером, а во вторых я попробую проиллюстрировать некоторые тонкости создания сетевых приложений.
    Для начала распакуем web_server_0.0.1.tar.gz.
    _si@security:~/coding/networks/old_works/test$ tar zxvf web_server_0.0.1.tar.gz web/ web/Makefile web/error.c web/config.c web/functions.c web/io.c web/main.c web/signal.c web/unp.h web/wrap.c web/html/ web/html/index.html web/html/hello.html web/html/404.html web/web.cfg
    Далее приведены некоторые исходники с комментариями.
    В файле main.c происходит создание сокета а также некоторые предворительные действия
    // main.c /* main file of web server project */ #include "unp.h" int main(int argc, char *argv[]) { int connfd, listenq, port; int listenfd; char temp[MAXLINE+1]; char document_root[MAXLINE+1]; char index[MAXLINE+1]; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; if (argc != 2) usage(argv[0]); /* читаем из конфига параметры: listenq - означает количество возможных соединений в очереди port - порт сервера, по умолчанию 80 document_root - директория в которой сервер будет искать запрашиваемые файлы */ if (argc != 2) usage(argv[0]); if (get_param(argv[1], "listenq", temp, MAXLINE) == 0) listenq = LISTENQ_DEFAULT; else listenq = atoi(temp); if (get_param(argv[1], "port", temp, MAXLINE) == 0) port = PORT_DEFAULT; else port = atoi(temp); if (get_param(argv[1], "document_root", temp, MAXLINE) == 0) strcpy(document_root, DOCUMENT_DEFAULT); else strcpy(document_root, temp); if (get_param(argv[1], "index", temp, MAXLINE) == 0) strcpy(index, INDEX_DEFAULT); else strcpy(index, temp); printf("port: %d\n", port); printf("listenq: %d\n", listenq); printf("document_root: %s\n", document_root); printf("index: %s\n", index); /* создание сокета */ listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port); Bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)); Listen(listenfd, listenq); Signal(SIGCHLD, sig_chld); /* есть два общепринятых метода организации параллельных серверов - используя процессы и используя потоки. Вариант с потоками работает быстрее, но он сложнее в реализации. Так как наш сервер не рассчитан на обработку большого числа пользователей, то вполне приемлемым в данном случае будет использование процессов */ while(1) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (struct sockaddr *) &cliaddr, &clilen); if ( (childpid = Fork()) == 0) { Close(listenfd); web_main(connfd, document_root, index); /* основная функция которая и выдает клиенту необходимый файл */ exit(0); } Close(connfd); } } В сетевом программировании большую роль играет обработка ошибок. Поэтому для каждой функции, такой как socket, bind итд приведены обертки, в которых происходит обработка ошибок. Вам может показаться что это пустая трата процесорного времени, однако это не так - при написании больших программ такой подход себя полностью оправдывает. // wrap.c ... int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) { int n; if ( (n = accept(fd, sa, salenptr)) < 0) { err_sys("accept error\n"); } return(n); } ... Следующей особенностью являются функции readline, writen. Почему не следует использовать стандартные read, write? Как пишет господин Р. Стивенс это может быть связанно с "достижением границ буфера для сокета в ядре". В любом случае лишняя подстраховка при написании сетевых приложений не повредит. // io.c #include "unp.h" /* input/output function */ static ssize_t my_read(int fd, char *ptr) { static int read_cnt = 0; static char *read_ptr; static char read_buf[MAXLINE]; if (read_cnt <= 0) { again: if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR) goto again; return(-1); } else if (read_cnt == 0) return(0); read_ptr = read_buf; } read_cnt--; *ptr = *read_ptr++; return(1); } ssize_t readline(int fd, void *vptr, size_t maxlen) { int n, rc; char c, *ptr; ptr = vptr; for (n = 1; n < maxlen; n++) { if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c; if (c == '\n') break; } else if (rc == 0) { if (n == 1) return(0); else break; } else return(-1); } *ptr = 0; return(n); } ssize_t writen(int fd, const void *vptr, size_t n) { size_t nleft; ssize_t nwritten; const char *ptr; ptr = vptr; nleft = n; while (nleft > 0) { if ( (nwritten = write(fd, ptr, nleft)) <= 0) { if (errno == EINTR) nwritten = 0; else return(-1); } nleft -= nwritten; ptr += nwritten; } return(n); } Основными функциями которые выполняют обрабоку запроса клиента являются web_main и show_file // functions.c #include "unp.h" void web_main(int sockfd, char *document_root, char *index) { /* советую всегда инициализировать массивы таким макаром - MAXLINE+1, это снизит вероятность однобайтового переполнения */ struct request { char metod[MAXLINE+1]; char file[MAXLINE+1]; char protocol[MAXLINE+1]; }; ssize_t n; char string[MAXLINE+1]; struct request request; /* читаем запрос сервера */ Readline(sockfd, string, sizeof(string) - 1); /*разбиваем запрос на выражения */ if ( sscanf(string, "%s%s%s", &request.metod, // metod - // GET for example &request.file, // what file you nead? &request.protocol) == 3) { // HTTP/1.1 /* если метод get и протокол HTTP/1.1 то вызываем функцию обработки запроса */ if ((strcmp(request.metod, GET) == 0 && strcmp(request.protocol, HTTP) == 0)) show_file(sockfd, request.file, document_root, index); } } int show_file(int sockfd, char *filename, char *document_root, char *index) { FILE *fd; char buf[MAXLINE+1]; char html_file[MAXLINE+1]; char line[MAXLINE+1]; /* шаблон ответа сервера */ char echo[] = "HTTP/1.1 %d %s\r\n" "Content-Type: text/html\r\n" "\r\n"; bzero(line, sizeof(line)); /* формирование имени файла */ if (strcmp(filename, "/") == 0) sprintf(html_file, "%s%s", document_root, index); else sprintf(html_file, "%s%s", document_root, filename); /* попытка открыть файл */ if ( (fd = fopen(html_file, "r")) == NULL) { sprintf(buf, echo, NOT_FOUND); Writen(sockfd, buf, strlen(buf)); /* если попытка не удалась то возвращаем ошибку 404. Разумеется это некоректно, коды ошибок в разных ситуациях разные но для простоты изложения здесь рассмотрена только эта */ if ( (fd = fopen("html/404.html", "r")) == NULL) { printf("error\n"); return(0); } while (fgets(line, sizeof(line), fd) != NULL) Writen(sockfd, line, strlen(line)); return(0); } /* возвращаем требуемый файл */ sprintf(buf, echo, OK); Writen(sockfd, buf, strlen(buf)); while (fgets(line, sizeof(line), fd) != NULL) Writen(sockfd, line, strlen(line)); fclose(fd); return(1); } В файле config.c - приведена реализация чтения параметра из файла, надеюсь проблем она не вызовет. // config.c #include "unp.h" int get_param(char *config_filename, char *nead_param, char *buf, size_t nead_param_len) { int i; char first_symbol; char current_string[MAXLINE+1]; char param[MAXLINE+1], temp[MAXLINE+1], result[MAXLINE+1]; FILE *config_filename_fd; if ((config_filename_fd = fopen(config_filename, "r")) == NULL) return(-1); first_symbol = current_string[0]; while (fgets(current_string, MAXLINE, config_filename_fd) != NULL) { while ( (first_symbol == ' ') || (first_symbol == '\t')) { first_symbol = current_string[i]; i++; } if (first_symbol == '#') continue; sscanf(current_string, "%s%s%s", param, temp, result); if (strcmp(param, nead_param) == 0) { strncpy(buf, result, nead_param_len); return(1); } } return(-1); } Для того чтобы сделать комментарий необходимо в начале поставить символ #. Параметр и значения должны быть разделены пробелом. Вот пример конфиг-файла: //web.cfg port = 80 listenq = 1024 document_root = /tmp/web index = index.html
    Ну вот в общем то наш сервер готов к запуску. Для сборки необходимо выполнить команду make. После чего появится файл server который и нужно запустить.

    4. Заключение

     Разумеется этот сервер нужно доработать. Необходимо добавить обработку 
    еще нескольких методов, добавить нормальное логирование, организовать более
    тщательную обработку получаемых данных. Надеюсь что это не вызовет у вас 
    больших трудностей. На последок хочу дать несколько советов начинающим 
    сетевым специалистам: пользуйтесь сниферами и изучайте готовые реализации. 
    Из литературы стоит выделить великолепные книги Р. Стивенса, в частности 
    "Unix:разработка сетевых приложений". Если у вас есть достаточно времени 
    ознакомьтесь со всеми его трудами. Что касается общей теории сетей то 
    отличным стартом послужат книги Таненбаума, Олифера и Столлингса.
    Удачи.

    --
    Sun Inventor < sun_inventor[at]mail.ru >

     




  • © Sun Inventor 10.05.2007 статья не оптимизирована

    e-Commerce Partners Network
    Ник:

    Текст:
    P Br B I Qute



    Код: обновить
    Последние комментарии
    18.11.2017 14:18:50 EvseyEdich написал:
    groomma st plazoni ...
    Пишем guestbook
    18.11.2017 12:08:52 IrineyVep написал:
    Seew pere Inoguic ...
    Пишем guestbook
    18.11.2017 03:10:20 EgorJap написал:
    DurlSoipuri envegname Pago ...
    Пишем guestbook
    Реклама

    Тут должна была быть ваша реклама, но мы потеряли глиняную табличку с ее текстом. SapeId: 665044

    Rambler's Top100