allasm.ru |
|
Введение Интернет играет в нашей жизни большую роль. Мы берем почту, узнаем свежую информацию, отлавливаем своих знакомых через ICQ... Но что ассоциируется со словом Интернет у большинства людей в первую очередь? Сайты. Многие при слове "Интернет" вспоминают свои любимые сайты, а некоторые (не слишком продвинутые) ставят знак равенства между WWW и Интернетом, хотя в последнем есть много другого интересного: email, IRC, p2p-сети, MUD'ы и так далее. Но World Wide Web играет доминирующую роль. В основе WWW лежит протокол HyperText Transfer Protocol. Надо сказать, что HTTP может использоваться не только для передачи сайтов, но и для передачи всего чтобы то ни было. С помощью протокола HTTP мы можем скачать, например, недавно вышедший фильм или свежие mp3 :). В p2p-сетях HTTP-протокол применяется именно в этих целях. В данном пособии мы рассмотрим, как написать простой веб-сервер. Я предполагаю, что вы знакомы с основами программирования winsock и умеете создавать сокеты и коннектиться к чему-нибудь :). Основы HyperText Transfer Protocol Идея HTTP довольно проста. Клиент шлет запрос серверу, тот рассматривает его и шлет соответствующий ответ. Ответом может быть запрошенный файл, сообщение о том, что такого файла на сервере нет или что-то еще. Примерная структура запроса следующая:
<метод> - вид запроса. Основных два: GET и POST. Друг от друга они отличаются, главным образом, способом передачи дополнительной информации, отсылающейся вместе с запросом. В этом туториале мы рассмотрим только метод GET - функционально он похож на POST, но несколько проще. О методе POST я расскажу во второй части данного туториала, если, конечно, таковая вообще появится на свет :). <\n> - это два байта 0Dh, 0Ah, несомненно, хорошо знакомые всем ассемблерщикам :). Таким образом, с методом мы определились. На данный момент запрос, который мы (в качестве клиента) должны будем послать серверу выглядит так:
Теперь нам нужно задать <запрашиваемый_ресурс>. Возьмем типичную ссылка на одном из лучших сайтов по программированию в Рунете WASM.RU (немного рекламы не помешает :) ): http://www.wasm.ru/article.php?article=1016002 Здесь "http://" указывает на то, что используется протокол HTTP, "www.wasm.ru" говорит о том, что необходимо подсоединиться к сайту www.wasm.ru, а все, что идет после знака вопроса - это дополнительные параметры страницы. Последние используются не всегда и не на всех сайтах. Предположим, что мы подсоединились к www.wasm.ru и хотим получить article.php с необходимыми параметрами. Очевидно, что раз мы уже подсоединились к данному серверу и знаем, что необходимо использовать протокол HTTP, то пересылать "http://www.wasm.ru" не нужно, а значит, мы пошлем только "/article.php?article=1016002". Теперь наш запрос к серверу выглядит так:
Теперь осталось разобраться с заголовочными полями. С их помощью клиент передает дополнительную информацию о себе или характере запроса. Например, довольно часто используемым заголовочным полем является 'User-Agent'. Опера, скажем, шлет следующее: User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows 98) Opera 6.02 [en] Другим еще более важным полем является 'Host'. В нем задается имя веб-сервера, с которого мы хотим получить документ. "Как же так", - можете сказать вы, - "Ведь мы же уже указывали имя веб-сервера, когда создавали сокет и коннектились к нему!" Да, это так. Когда мы коннектились к серверу, мы вызвали функцию gethostbyname, которой передали имя веб-сервера, а она возвратила нам адрес структуры, содержащей адрес сервера. Дело в том, что одному IP-адресу может соответствовать несколько имен сайтов, поэтому когда веб-сервер получает запрос, он должен знать к какому сайту обращается клиент (один веб-сервер может обслуживать тысячи различных сайтов, которые физически будут расположены на одной машине с одним адресом). Для этого мы пишем в 'Host' адрес сайта, к которому обращаемся: Host: www.wasm.ru Вот как теперь выглядит наш запрос:
Надо заметить, что хотя эти и другие заголовочные поля являются очень желательными, но, строго говоря, они не являются обязательными, то есть их может и не быть. Также я хочу обратить ваше внимание на то, что за последним заголовочным файлом следуют _два_ <\n>, а не один. Это важно. Теперь взглянем на все вышеизложенное с точки зрения веб-сервера (ведь мы же собирались писать веб-сервер, помните? :) ). Он ждет, пока к нему не поступит запрос, обрабатывает его и посылает ответ, который выглядит примерно так:
Получив запрос, сервер должен выдать клиенту код ответа (это трехзначное число), а также сопутствующее ему сообщение. Например, если запрошенный документ был найден, первая строка ответа будет примерно следующей: HTTP/1.1 200 Ok Если коды ответов жестко заданы стандартом (200 означает, что запрос был успешно обработан и будет возвращен соответствующий документ/информация), то <сообщение> зависит только от вашей фантазии (конечно, по смыслу оно должно совпадать с <кодом_ответа>. Например, вместо "HTTP/1.1 200 Ok" мы можем послать "HTTP/1.1 200 You want it, you'll get it" :). В ответе сервера также могут быть заголовочные поля. Они содержат дополнительные сведения об ответе и данных, идущих вместе с ним. Далее я приведу важнейшие (для нас). Content-Type - этот заголовок задает тип отдаваемых данных. Главнейшие типы заданы в стандарте, и от того, что вы напишите в данном поле, зависит поведение клиента при приеме данных. Например, если вы посылаете браузеру пользователя html-файл, то необходимо указать тип данных 'text/html', иначе браузер может отобразить файл неправильно, либо вообще не будет его отображать, а предложит пользователю его скачать. Content-Length - здесь указывается длина данных (не включая сам ответ с заголовочными данными) в байтах. Исходя из вышесказанного, ответ сервера будет выглядеть примерно так:
Если мы хотим отослать простой html-файл, то можем сократить ответ до следующего:
В результате получаем следующее. Клиент шлет запрос на получение документа, лежащего, например, в корне сервера:
Сервер получает этот запрос, обрабатывает и выдает корневую страницу:
Код приложения
Анализ кода "Веб-сервер", чей исходный код был приведен выше, очень примитивен. Он умеет только принимать запрос и, не проверяя его на правильность, выдавать только приветственную html-страницу. В http.asm все, я надеюсь, достаточно понятно. Мы инициализируем сокеты, создаем окно (для чего, я поясню позже), входим в цикл обработки сообщений, а перед тем, как завершить работу приложения, вызываем WSACleanup. Самое интересное находится в http_window.asm. При обработке сообщения WM_CREATE мы создаем сокет:
А потом вызываем следующую функцию:
Вот что об этой функции говорит Platform SDK: [ начало описания функции WSAAsynctSelect ] WSAAsyncSelect Функция WSAAsyncSelect указывает Windows посылать сообщения о событиях, касающихся определенного сокета.
Параметры: s - Дескриптор сокета, о событиях, связанных с которым, будет сообщаться. hWnd - Хэндл окна, которому будут посылаться эти сообщения. wMsg - Сообщение, которое будет посылаться. lEvent - Битовая маска, в которой задаются интересующие события. Возвращаемые значения Если вызов функции WSAAsyncSelect прошел успешно, возвращаемое значение будет равно нулю. В противном случае будет возвращено SOCKET_ERROR, а код ошибки можно будет получить, вызвав WSAGetLastError. [ конец описания ] Учтите, что после того, как WSAAsyncSelect отошлет вам сообщение о конкретном событии, связанном с сокетом, то пока вы не предпримите определенных действий, нового сообщения о таком же событии вы не получите. Например, если вы получили сообщение FD_ACCEPT (кто-то пытается законнектиться к вам), то сообщения о другой попытки коннекта вы не получите до тех пор, пока не вызовите функцию accept. Мы задаем WM_SOCKET, определенное в http.asm, в качестве сообщение, которое будет присылаться Windows, когда произойдет интересующее нас сообщение. Необходимая информация будет находиться в wParam (дескриптор сокета, с которым связано событие) и в lParam (в нижнем слове - код события). Теперь, когда кто-нибудь попытаемся приконнектиться к сокету, наше окно получит соответствующее уведомление от операционной системы. Впрочем, сначала нужно ассоциировать созданный сокет с определенным адресом и портом, к которым и должны будут коннектиться посетители веб-сервера.
Веб-сервер будет "висеть" на localhost'е (т.е. на локальной машине) на 80-ом порту, который является стандартным HTTP-портом. Если в адресе сайта прямо не указан порт, то браузер будет обращаться к 80-ому порту.
Собственно, в данных строчках и содержится ответ на то, как сделать из приложения сервер (не обязательно web). Это делает функция listen. [ начало описания функции listen ] listen Функция listen устанавливает сокет в состояние, в котором он слушает порт на предмет входящих соединений.
Параметры s - Дескриптор сокета backlog - Максимальное количество входящих соединений. Возвращаемые значения Если во время вызова не произошло никакой ошибки, listen возвратит ноль. В противном случае будет возвращено значение SOCKET_ERROR, а код ошибки можно будет получить с помощью функции WSAGetLastError. [ конец описания ]
Было получено сообщение WM_SOCKET. Это значит, что произошло какое-то интересующее нас событие, связанное со слушающим сокетом.
Кто-то пытается подсоединиться к нашему веб-серверу. Вызываем функцию accept, чтобы разрешить входящее соединение. [ начало описания функции accept ] accept Функция accept разрешает входящее соединение.
Параметры s - Дескриптор сокета, который ранее был помещен в состояние прослушивания с помощью функции listen. Фактическое соединение осуществляется с помощью сокета, который возвращается accept'ом. addr - Необязательный указатель на буфер, который получит адрес того, кто пытается подсоединиться к серверу. addrlen - Необязательный указатель на двойное слово, которое содержит длину addr. Возвращаемые значения Если не произошло никакой ошибки, accept возвратит дескриптор нового сокета, через который и будет происходить соединение. В противном случае будет возвращен INVALID_SOCKET, а код ошибки можно будет получить с помощью функции WSAGetLastError. Переменная, на которую указывает addrlen, вначале содержит объем, занятый структурой, на которую указывает addr. По возвращении она будет содержать длину возвращенного адреса в байтах. [ конец описания ]
Соединение разрешено, и мы вызываем функцию WSAAsyncSelect, чтобы получить соответствующее уведомление, когда можно будет читать из сокета или он будет закрыт.
Здесь все просто. Пришло сообщение о том, что можно читать из сокета, что мы и делаем. Все считанное мы выводим на консоль (интересно же, что клиент прислал). По-хорошему, здесь мы должны были бы провести синтаксический разбор запроса: выяснить, какой конкретно документ он хочет, отдать его, если такого документа нет, послать сообщение об ошибке и т.п. Но поскольку я минимализировал сервер почти до предела в плане функциональности :), ничего этого здесь нет. Вместо этого мы шлем клиенту приветственный html.
Если сокет был закрыт клиентом, то мы его тоже закрываем со своей стороны. Дополнительная литература Для получения подробной информации о протоколе HTTP я рекомендую вам обратиться к RFC 2068. Заключение Надеюсь, вы почерпнули из этого туториала какую-нибудь полезную информацию. Напоследок мне хотелось бы сказать, что хотя составлять конкуренцию таким грандам как Apache и IIS без веских на то оснований, возможно, и не стоит, тем не менее, собственный маленький веб-сервер может быть очень полезен. Мне, например, предложили встроить в него механизм самораспространения, "чтобы он сам приходил к людям на дом" и устанавливался "через упрощенную процедуру инсталляции" ака Outlook. Другим, менее чреватым в плане возможных последствий для автора, вариантом может быть создание утилиты удаленного (не обязательно скрытого) администрирования, причем в качестве клиента будет выступать браузер, что весьма удобно, так как отпадет надобность в написании сопутствующей серверу клиентской программы. Возможно, вы найдете еще какое-нибудь применение для http-сервера. Все в ваших руках! (c) Aquila / Hi-Tech, 2002 [C] Aquila / WASM.RU |