Code44free's Blog

Старая статья, примитивный HTTP сервер

Posted in c/c++, programming by code44free on Январь 2, 2013

Simple HTTPD.

Когда мы в браузере набираем http://www.sex.com, запрос от нашего браузера уходит на
шлюз, тот в свою очередь запрашивает DNS сервер, об IP-адресе принадлежащем хосту
sex.com, 209.81.7.23. Далее запрос направляется на этот IP-адрес на соответствующий
службе порт (www-80, ftp-21 и т.д.). Так выглядит эта схема с точки зрения канального
уровня сетевой модели OSI. Сам же диалог между браузером и веб-сервером проходит
на прикладном уровне. Типичный диалог выглядит следующим образом:

	Браузер клиента:
		GET /index.html HTTP/1.0
		Host: 209.81.7.23
		User-Agent: Mozilla/5.0
		. . . . .

	Ответ сервера:
		HTTP/1.0 200 OK
		Date: Sat, 27 Jul 2002 14:48:29 GMT
		Server: Apache 1.3.1
		Content-Legnth: 132
		Content-type: text/html		

		<html><body>
		. . . . .
		</body></html>

Как видно из листинга, браузер посылает вебсерверу запрос GET /index.html (дай мне
index.html), сообщает версию протокола HTTP (HTTP/1.0 или HTTP/1.1 через прокси
соединение). Далее идет служебная информация о типе браузера, версии операционной
системы пользователя и т.д. На данный запрос сервер отвечает 200 OK (хорошо), или
хорошо известный 404 Not Found (не могу найти запрашиваемый документ). Далее
передается некоторая служебная информация, после нее через две пустые строки(“\n\n”)
передается сам документ. Подробно о протоколе HTTP можно почитать в RFS 2109 и RFS 2616.

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

Сетевая часть.

Алгоритм работы любого сетевого сервера в случае ОС UNIX(и ее клонов) выгладит следующим
образом. В начале создаем двунаправленный канал связи, с помощью системного вызова socket(2):

	#include <resolv.h>
	#include <sys/socket.h>
	#include <sys/types.h>
	int socket(int domain, int type, int protocol);

Где параметр domain задает семейство сетевых протоколов(AF_INET –семейство протоколов
IPv4, AF_LOCAL, AF_UNIX –доступ к локальным именованым каналам и т.д.). Параметр type
задает сетевой уровень работы сокета(SOCK_STREAM –(TCP) надежное двухсторонне соединение,
SOCK_DGRAM –(UDP) ненадежное соединение без установления соединения, SOCK_RAW –(IP)
доступ к внутренним полям сетевых пакетов и т.д.). Третий параметр protocol задает
конкретный протокол (обычно равен нулю).

После создания сокета, его необходимо привязать к какому либо порту, или локальному
файлу с помощью системного вызова bind(2):

	#include <sys/socket.h>
	#include <resolv.h>
	int bind(int sockfd, struct sockaddr *addr, int addrlen);

Прим. В скобках bind(2) указан номер страницы справочного руководства.
Т.е. в данном случае информацию по функции можно посмотреть дав
команду man 2 bind.

Где sockfd дескриптор сокета возвращенный функцией socket(2), struct sockaddr *addr
указатель на заполненую структуру типа sockaddr, addrlen длинна структуры sockaddr.
В структуре sockaddr необходимо заполнить следующие поля:

	addr.sin_family=AF_INET; 	// Семейство протоколов TCP
	addr.sin_port=htons(port); 	// Номер порта преобразованный в сетевой
					// порядок функцией htons(3)
	addr.sin_addr.s_addr=INADDR_ANY;// Любой IP-адрес

Затем перевести сокет в режим ожидания запросов на подключение системным вызовом listen(2):

	#include <sys/socket.h>
	#include <resolv.h>
	int listen(int sockfd, int queue_len);

Где sockfd дескриптор сокета, queue_len максимальное число запросов в очереди (как
правило, этот параметр устанавливается в значение от 5 до 20, большее количество
оказывается избыточным).

Теперь мы можем создать сокет, привязать его к нужному порту, и перейти к приему запросов от клиентов:

	#define PORT 80				// Номер порта по умолчанию 

	int  sd;				// Будущий дескриптор сокета
	struct sockaddr_in addr;		// Структура sockaddr
	int port = PORT				// Переменная для номера порта 
						// проинициализированная
						// значением по умолчанию	
	bzero(&addr, sizeof(addr)); 		// Обнулим структуру sockaddr
	sd=socket(AF_INET, SOCK_STREAM, 0);	// Создадим сокет
	addr.sin_family=AF_INET; 		// Протокол TCP
	addr.sin_port=htons(port); 		// Порт
	addr.sin_addr.s_addr=INADDR_ANY; 	// Любой адрес
	openlog(“httpd”, LOG_PID|LOG_CONS, LOG_DAEMON);	// Откроем лог-файл
	if(bind(sd, &addr, sizeof(addr))){	// Привяжемся к порту
	syslog(LOG_ERR, "http: Can't bind()\n"); exit(0);} // В случае ошибки отметим
           					// это в логе и выйдем из программы
	if(listen(sd, 5)){			// Переведем сокет в режим прослушивания
	syslog(LOG_ERR, "http: Can't listen()\n"); exit(0);} 
	syslog(LOG_INFO, "http starting with 	// Отметим старт в логе
	current parameters\nport: %d\nwww_dir: %s", port, wwwPath); 
	closelog(); 				// Закроем лог

В данном листинге незнакомы только функции openlog(3), syslog(3) и closelog(3). Поскольку мы
пишем сервер, который будет не интерактивной программой, а демоном, то обычные способы
диагностики (вывод диагностических сообщений на консоль) ему не подходят. Как известно
в практически любой UNIX’Like системе присутствует демон syslog, который ведет системный
журнал. С помощью этих функций программы могут выводить свои сообщения в системный журнал.
Openlog(3) –“открывает” системный журнал:

	#include <syslog.h>
	void openlog(const char *ident, int logopt, int facility);

Из параметров полезны logopt –опции ведения журнала(LOG_PID –каждое сообщение предваряется
идентификатором программы(PID), LOG_CONS –в случае невозможности вывода в журнал сообщение
посылается на консоль, facility –источник сообщений(LOG_DAEMON –системный демон)).
Syslog(3) – записывает сообщение в журнал:

	void syslog(int priority, char *longstring, /*параметры*/);

priority –приоритет сообщения(LOG_ERR -ошибка, LOG_INFO –информационное сообщение,
LOG_EMERG –идентифицирует “панику” в системе, как правило, все накрывается медным тазом).
Longstring –текстовое сообщение, допускаются елементы форматирования, такие же, как и у
функции printf(3). Closelog(3) –закрывает системный журнал.

Следующий этап –принятие запроса и его обработка. После вызова функции listen(2),
необходимо организовать прием сообщений. Это делается с помощью системного вызова accept(2):

	#include <sys/socket.h>
	#include <resolv.h>
	int accept(int sockfd, struct sockaddr *addr, int *addrlen);

Функция возвращает дескриптор нового сокета, не зависящего от дескриптора функции
socket(2), созданного для обслуживания конкретного запроса. Вызов данной функции
возможен только для сокетов типа SOCK_STREAM. Первый параметр функции sockfd –дескриптор
сокета, addr –если этот параметр не равен нулю, функция помещает в него адрес клиента,
addrlen –ссылка на переменную содержащую размер адресной структуры, в нее же функция
возвращает реальный размер адреса. Теперь напишем код для обработки запросов:

	#define MAXPATH 100		// Длинна пути к запрашиваемому файлу 
					// по умолчанию		

	int csd;			// Дескриптор сокета
	int addr_len=sizeof(addr);	// Переменная, содержащая размер структуры addr
	char wwwPath[MAXPATH];		// Переменная для хранения пути к 
					// запрашиваемому файлу

	for(;;){			// Бесконечный цикл для обработки запросов
		if((csd=accept(sd, &addr, &addr_len))>0){// Принимаем запросы
			sesHendler(csd, wwwPath);	 // Вызываем обработчик запросов
			close(csd);		// Закрываем сокет
		} else {			// В случае ошибки отметим это в логе
			openlog(“httpd”, LOG_PID|LOG_CONS, LOG_DAEMON);
			syslog(LOG_ERR, “http: Can’t accept(%s)\n”, inet_ntoa(addrsin_addr));
		closelog();
		}
	}

На этом сетевая часть сервера закончена.

Обработчик запросов.

После того как сервер установил соединение с потенциальным клиентом, необходимо получить
от него данные(что ему собственно нужно), и на основании полученных данных ответить
200 OK или 505(версия протокола не поддерживается:) или выполнить какие либо еще действия(см.
RFS на предмет HTTP методов). Поэтому приступаем к созданию обработчика клиентских запросов.
После создания канала между сервером и клиентом получить данные, посланные клиентом можно с
помощью системного вызова recv(2):

	#include <sys/socket.h>
	#include <resolv.h>
	int recv(int sockfd, void *buf, int maxbuf, int options);

sockfd –дескриптор подключенного сокета(в нашем случае дескриптор сокета созданный функцией
accept(2)), buf –буфер в который будет помещено полученное сообщение, размер буфера,
options –набор флагов(MSG_OOB –получение внеполосных данных, MSG_WAITALL –блокировка
программы до получения всего сообщения целиком и т.д.). После получения сообщения от
клиента в буфере будет содержатся примерно следующая информация:

	GET /index.html HTTP/1.0
	Host: 209.81.7.23
	User-Agent: Mozilla/5.0
	. . . . .

Из этого сообщения необходимо теперь выделить имя файла необходимого клиенту(/index.html).
Самый удобный вариант воспользоватся функцией sscanf(3):

	#include <stdio.h>
	int sscanf(const char *str, const char *format, …);

str –строка символов из которой функция читает данные, format –шаблон считываемых
данных(соответствует елементам форматирования функции printf(3)):

	sscanf(buf, “GET %s HTTP”, file); // Помещает строку символов, 
					  // содержащуюся в buf между словами
					  // GET и HTTP в переменную file

Теперь необходимо проверить, указан ли в запросе конкретный файл или было набрано
только имя хоста на котором размещен веб-сервер, в данном случае в запросе будет
следующая запись: GET / HTTP/1.0. В результате после выполнения функции sscanf(3)
переменная file будет содержать два символа ‘/’ и ‘’ –символ конца строки,
наличие которого и необходимо проверить:

	if (file[1]==’\0’) strcat(file, “index.html”);

Теперь подготовим полное имя файла запрашиваемого сервером, с учетом пути к файлу:

	#define   WWW_DIR   “/var/www”	// Директория для веб-страниц по умолчанию
	#define   SIZE      2048			
	#define   MAXPATH   100	// Максимальная длинна полного пути к файлу

	char file[MAXPATH];	// Путь к запрашиваемому файлу
	char tmp[MAXPATH];	// Временный буфер для парсинга пути к файлу
	char buf[SIZE];		// Буфер для входящих сообщений размером SIZE
	int i, j=0;		// Счетчики

	for(i=0; file[i]; i++){	// Отрежем доменную часть в запросе и 
				// оставим только путь и имя файла	
	if(file[i]=='/')for(i; file[i]; i++){tmp[j++]=file[i];} 	
	}
	j=i=0;			// Обнулим счетчики
	bzero(&file, MAXPATH);	// Очистим переменную file
	strcpy(file, www_dir);	// Скопируем в file путь к директории в вебстраницами
	strcat(file, tmp);	// Добавим к ней имя необходимого клиенту файла

В результате переменная file будет содержать полный путь к требуемому файлу. Теперь мы
можем прочесть данный файл(если он существует) и отправить клиенту:

	int fd;
			// Если не удалось открыть файл, отправим сообщение
	if((fd=fopen(file, "r"))==NULL){errors(sock, 0);}	
	else{		// об ошибке(404)

Отправка данных клиенту осуществляется с помощью системного вызова send(2):

	#include <sys/socket.h>
	#include <resolve.h>
	int send(int sockfd, void *buffer, int msg_len, int options);

buffer –отправляемые данные, msg_len –число посылаемых байтов, options –набор
флагов, указывающий на особые режимы обработки сообщений. Поскольку изначально
неизвестно файл, какого размера потребуется отправить, поэтому выделять статический
массив(char reply[65535]) не имеет смысла, гораздо эфективней выделять память динамически,
исходя из размера затребованного файла. Выяснить размер файла, а также некоторые
другие его атрибуты можно с помощью системного вызова stat(2):

	#include 
	#include 
	int stat(const char *path, struct stat *sb);

path –полный путь к файлу, sb –указатель на структуру stat.
В результате в поле st_size структуры stat будет содержаться размер файла в байтах, на
основании которого мы можем выделить достаточно памяти с помощью функции malloc(3):

	
	struct stat filestat;		// Структура для возврата значений

	stat(file, &filestat);		// Определим размер файла
	reply=(char*)malloc(filestat.st_size);		// Выделим память
	while(fread(reply, filestat.st_size, 1, fd)){	// Прочитаем файл в буффер
		send(sock, reply, filestat.st_size, 0);	// Отправим ответ клиенту
		free(reply);				// Очистим память 
	}

Но перед этим необходимо отправить клиенту сообщение 200 OK, что файл найден:

	char head[SIZE];	// Буфер для сообщения

	bzero(&head, SIZE);	// Очистим буфер для сообщения
				// Сформируем сообщение
	sprintf(head, "HTTP/1.0 200 OK\n Server: Simple httpd\nContent-Legnth: %d\n \
				 Content-type: text/html\n\n\n", filestat.st_size);
	send(sock, head, strlen(head), 0);	// Отправим его клиенту

Обратите внимание, что отсылаемое сообщение заканчивается на “\n\n\n”, т.е. конец
текущей строки, плюс две пустых. Таким образом клиенту мы отсылаем следующий текст:

	HTTP/1.0 200 OK
	Server: Simple httpd
	Content-Legnth: XXX
	Content-type: text/html


	<html><body>
	. . . . . 

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

Служебная часть.

Теперь поговорим о темной стороне UNIX, о демонах. Для того чтобы наш сервер был
настоящим сервером ему необходимо стать демоном. Т.е. не быть ассоцированным (по
сути зависимым) от какого либо пользовательского терминала, закрыть все открытые
файлы, в особенности стандартные потоки ввода/вывода, изменить текущий каталог на корневой:

	if(getpid()!=1){		 // Если предок не процесс init переопредилим сигналы 
		signal(SIGTTOU, SIG_IGN);// связанные с вводом/выводом терминала
		signal(SIGTTIN, SIG_IGN);// с помощью функции signal(3)
		signal(SIGTSTP, SIG_IGN);
	}
	if(fork()!=0) exit(0);	// Если вызов fork(2) успешен, родитель заканчивает работу exit(3)
	setsid();		// Дочерний вызов становиться лидером группы

Теперь необходимо закрыть все открытые дескрипторы файлов и изменить текущий каталог
на корневой. Число открытых файлов можно получить с помощью системного вызова getrlimit(2).

	struct rlimit flim;			// Структура для возвращаемых значений

	getrlimit(RLIMIT_NOFILE, &flim);		// Получим данные
	for(fd=0; fd<flim.rlim_max; fd++) close(fd);	// Закроем все файлы
	chdir("/");					// Изменим каталог на корневой

Все наш сервер стал демоном.

Сервисные функции.

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

    -p номер порта;
    -d директория содержащая веб-страницы;
    -h вывод помощи;

Для обработки опций переданных программе можно использовать функцию getopt(3):

	#include <unistd.h>
	int getopt(int argc, char *const *argv, const char *optstring);

argc –количество аргументов переданных программе, argv – сами аргументы, optstring –модификаторы.

	#define PORT 80			// Номер порта по умолчанию
	#define WWW_DIR  “/var/www”

	int port=PORT;		// Переменная, содержащая номер порта, 
				// присваиваем значение по умолчанию
	char wwwPath[MAXPATH];	// Переменная, содержащая путь к директории 
				// с веб-страницами
				// Считываем аргументы
	while((ch = getopt(argc, argv, "p:d:h"))!=-1)
	switch(ch){
		case 'p':			// Если опция –p, optarg содержит номер порта
			port=atoi(optarg);	// преобразуем его в тип integer и присвоим значение
			break;			// переменной port (заменив значение по умолчанию)
		case 'd':			// Если опция –d
			strcpy(wwwPath, optarg);// Присвоим значение параметра переменной wwwPath
			break;
		case 'h':			// Если опция –h
			usage();		// выведем помощь
	}
				// Если директория в веб-страницами не определена выше
				// скопируем в нее значение по умолчанию
	if((strlen(wwwPath))==0) strcpy(wwwPath, WWW_DIR);	

Теперь мы можем указать серверу номер порта по своему усмотрению или директорию с веб-страницами.

Заключение.

К сожалению, размер статьи не позволяет рассмотреть все аспекты создания сетевых серверов.
Остались полностью за кадром интереснейшие темы, многозадачность, распределение нагрузки,
контроль ресурсов и т.д. Данный материал необходимо рассматривать как шаблон, скелет
создания сетевого сервера. Данный пример можно расширить для поддержки других методов HTTP
(POST, PUT, LINK, TRACE и т.д), для поддержки скриптов perl, PHP. Вобщем: “Даже если программа
состоит из трех строк она, когда нибудь будет развита”.

Alex Smovch

Исходный текст (httpd.c).

/*
	Компиляция: gcc –o httpd httpd.c
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <resolv.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <getopt.h>
#include <sys/stat.h>
#include <syslog.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/resource.h>

#define PORT 80
#define WWW_DIR "/var/www"
#define SIZE 1024
#define MAXPATH 100

void usage()	// Функция для вывода справочного сообщения об опциях программы
{
	printf("Applied options:\n -p port\n -d directory contains www files\n -h help\n");
	exit(0);
}

void errors(int sock, int tok)	//Функция для отсылки клиенту сообщения об ошибке
{
	static char *e[]={
		"HTTP/1.0 404 Not Found\n",
	send(sock, e[tok], strlen(e[tok]), 0);
}

void sesHandler(int sock, char *www_dir)	//Обработчик клиентских запросов
{
	char file[MAXPATH];	//Путь к запрашиваемому файлу
	char tmp[MAXPATH];	//Временный буфер для парсинга пути к файлу
	char buf[SIZE];		//Буффер для входящих сообщений
	char head[SIZE];
	char *reply;
	FILE *fd;
	int i, j=0;
	struct stat filestat;

	bzero(&buf, sizeof(buf));
	bzero(&tmp, MAXPATH);
	bzero(&file, MAXPATH);
	recv(sock, buf, SIZE, 0);

	sscanf(buf, "GET %s HTTP", file);
	if(file[1]=='\0') strcat(file,"index.html");
	for(i=0; file[i]; i++){
		if(file[i]=='/')for(i; file[i]; i++){tmp[j++]=file[i];}
	}
	j=i=0;
	bzero(&file, MAXPATH);
	strcpy(file, www_dir);
	strcat(file, tmp);

	if((fd=fopen(file, "r"))==NULL){errors(sock, 0);}	//Error 404 Not Found
	else{
		stat(file, &filestat);			//Определим размер файла
		reply=(char*)malloc(filestat.st_size);
		bzero(&head, SIZE);			//Отправим заголовок HTTP
		sprintf(head, "HTTP/1.0 200 OK\n Server: Simple httpd\nContent-Legnth: %d\n \
					 Content-type: text/html\n\n\n", filestat.st_size);
		send(sock, head, strlen(head), 0);
		while(fread(reply, filestat.st_size, 1, fd)){	//Отправим файл
			send(sock, reply, filestat.st_size, 0);
			free(reply);
		}
	}
}

int main(int argc, char *const *argv)
{
	struct sockaddr_in addr;
	struct rlimit flim;
	char wwwPath[MAXPATH];
	int sd, csd, addr_len=sizeof(addr), ch, port=PORT, fd;

	bzero(&wwwPath, MAXPATH);	//Обработаем аргументы переданные при запуске
	while((ch = getopt(argc, argv, "p:d:h"))!=-1)
		switch(ch){
			case 'p':
				port=atoi(optarg);
				break;
			case 'd':
				strcpy(wwwPath, optarg);
				break;
			case 'h':
				usage();
		}

	if((strlen(wwwPath))==0) strcpy(wwwPath, WWW_DIR);

	if(getpid()!=1){		//Переопределим сигналы и станем демоном
		signal(SIGTTOU, SIG_IGN);
		signal(SIGTTIN, SIG_IGN);
	signal(SIGTSTP, SIG_IGN);
	}
	if(fork()!=0) exit(0);
	setsid();

	getrlimit(RLIMIT_NOFILE, &flim);
	for(fd=0; fd<flim.rlim_max; fd++) close(fd);
	chdir("/");

	bzero(&addr, sizeof(addr));	//Создадим сокет
	sd=socket(AF_INET, SOCK_STREAM, 0);
	addr.sin_family=AF_INET;
	addr.sin_port=htons(port);
	addr.sin_addr.s_addr=INADDR_ANY;
					// Прибиндимся и отметимся в логе
	openlog("httpd", LOG_PID|LOG_CONS, LOG_DAEMON);
	if(bind(sd, &addr, sizeof(addr))){syslog(LOG_ERR, "http: Can't bind()\n"); exit(0);}
	if(listen(sd, 5)){syslog(LOG_ERR, "http: Can't listen()\n"); exit(0);}
	syslog(LOG_INFO, "http starting with current parameters\nport: %d\nwww_dir:
			%s", port, wwwPath);
	closelog();

	for(;;){			//Обрабатываем запросы
		if((csd=accept(sd, &addr, &addr_len))>0){
			sesHandler(csd, wwwPath);
			close(csd);
		} else {	openlog("httpd", LOG_PID|LOG_CONS, LOG_DAEMON);
			syslog(LOG_ERR, "http: Can't accept(%s)\n", inet_ntoa(addr.sin_addr));
			closelog();}
	}
	return 0;
}
Реклама

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: