Туториал по написанию собственного веб-сервера — Архив WASM.RU
Введение
Интернет играет в нашей жизни большую роль. Мы берем почту, узнаем свежую информацию, отлавливаем своих знакомых через ICQ... Но что ассоциируется со словом Интернет у большинства людей в первую очередь? Сайты. Многие при слове "Интернет" вспоминают свои любимые сайты, а некоторые (не слишком продвинутые) ставят знак равенства между WWW и Интернетом, хотя в последнем есть много другого интересного: email, IRC, p2p-сети, MUD'ы и так далее. Но World Wide Web играет доминирующую роль.
В основе WWW лежит протокол HyperText Transfer Protocol. Надо сказать, что HTTP может использоваться не только для передачи сайтов, но и для передачи всего чтобы то ни было. С помощью протокола HTTP мы можем скачать, например, недавно вышедший фильм или свежие mp3 . В p2p-сетях HTTP-протокол применяется именно в этих целях.
В данном пособии мы рассмотрим, как написать простой веб-сервер. Я предполагаю, что вы знакомы с основами программирования winsock и умеете создавать сокеты и коннектиться к чему-нибудь .
Основы HyperText Transfer Protocol
Идея HTTP довольно проста. Клиент шлет запрос серверу, тот рассматривает его и шлет соответствующий ответ. Ответом может быть запрошенный файл, сообщение о том, что такого файла на сервере нет или что-то еще. Примерная структура запроса следующая:
Код (Text):
<метод> <запрашиваемый_ресурс> HTTP/1.1<\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> [..заголовочных полей может быть много..]<\n> <заголовочное_поле>: <значение><\n> <\n><метод> - вид запроса. Основных два: GET и POST. Друг от друга они отличаются, главным образом, способом передачи дополнительной информации, отсылающейся вместе с запросом. В этом туториале мы рассмотрим только метод GET - функционально он похож на POST, но несколько проще. О методе POST я расскажу во второй части данного туториала, если, конечно, таковая вообще появится на свет .
<\n> - это два байта 0Dh, 0Ah, несомненно, хорошо знакомые всем ассемблерщикам .
Таким образом, с методом мы определились. На данный момент запрос, который мы (в качестве клиента) должны будем послать серверу выглядит так:
Код (Text):
GET <запрашиваемый_ресурс> HTTP/1.1<\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> [...] <заголовочное_поле>: <значение><\n> <\n>Теперь нам нужно задать <запрашиваемый_ресурс>. Возьмем типичную ссылка на одном из лучших сайтов по программированию в Рунете 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". Теперь наш запрос к серверу выглядит так:
Код (Text):
GET /article.php?article=1016002 HTTP/1.1<\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> [...] <заголовочное_поле>: <значение><\n> <\n>Теперь осталось разобраться с заголовочными полями. С их помощью клиент передает дополнительную информацию о себе или характере запроса. Например, довольно часто используемым заголовочным полем является '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
Вот как теперь выглядит наш запрос:
Код (Text):
GET /article.php?article=1016002 HTTP/1.1<\n> User-Agent: ManualSender/1.0 <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"><\n> Host: www.wasm.ru<\n> <\n>Надо заметить, что хотя эти и другие заголовочные поля являются очень желательными, но, строго говоря, они не являются обязательными, то есть их может и не быть. Также я хочу обратить ваше внимание на то, что за последним заголовочным файлом следуют _два_ <\n>, а не один. Это важно.
Теперь взглянем на все вышеизложенное с точки зрения веб-сервера (ведь мы же собирались писать веб-сервер, помните? ). Он ждет, пока к нему не поступит запрос, обрабатывает его и посылает ответ, который выглядит примерно так:
Код (Text):
HTTP/1.1 <код_ответа> <сообщение><\n> <заголовочное_поле>: <значение><\n> <заголовочное_поле>: <значение><\n> [..заголовочных полей может быть много..]<\n> <заголовочное_поле>: <значение><\n> <\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 - здесь указывается длина данных (не включая сам ответ с заголовочными данными) в байтах.
Исходя из вышесказанного, ответ сервера будет выглядеть примерно так:
Код (Text):
HTTP/1.1 200 Ok<\n> Content-Type: text/html<\n> Content-Length: <длина_документа><\n> <\n> <тело_документа>Если мы хотим отослать простой html-файл, то можем сократить ответ до следующего:
Код (Text):
HTTP/1.1 200 Ok<\n> Content-Type: text/html<\n> <\n> <тело_документа>В результате получаем следующее. Клиент шлет запрос на получение документа, лежащего, например, в корне сервера:
Код (Text):
GET / HTTP/1.1<\n> User-Agent: ManualSender/1.0 <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"><\n> Host: www.someoneserver.com<\n> <\n>Сервер получает этот запрос, обрабатывает и выдает корневую страницу:
Код (Text):
HTTP/1.1 200 Ok<\n> Content-Type: text/html<\n> <\n> <html> <head><title>Добро пожаловать на HTTP-сервер!</title></head> <body>Вы находитесь на нашем http-сервере</body> </html>Код приложения
Код (Text):
;--------------------------------------------------------------------- ; http.asm ;--------------------------------------------------------------------- format PE console entry start ; Подключаемые файлы include '..\..\include\kernel.inc' include '..\..\include\user.inc' include '..\..\include\macro\stdcall.inc' include '..\..\include\macro\import.inc' include 'winsock.inc' include 'macros.inc' ; Используемые значения WM_SOCKET = WM_USER+100 INBUF_LEN = 100000 ; Секции программы section '.data' data readable writeable include 'strings.inc' hInstance dd ? http_class dd ? hwnd dd ? msg dd ? sock dd ? rv dd ? wc WNDCLASS wsadata WSADATA saddr SOCKADDR_TCP iaddr SOCKADDR_TCP buf rb INBUF_LEN section '.code' code executable readable include 'http_window.asm' start: invoke GetModuleHandle, 0 mov [hInstance], eax ; Инициализируем сокеты invoke WSAStartup, 101h, wsadata test eax, eax jnz error.wsanotinit ; Создаем окно jmp make_http_window ; Цикл обработки сообщений .msg_loop: invoke GetMessage,msg,NULL,0,0 test eax,eax jz .exit invoke TranslateMessage,msg invoke DispatchMessage,msg jmp .msg_loop .exit: ; Очищаем сокеты invoke WSACleanup invoke ExitProcess, 0 error: .recv_error: ccall [printf], _recv_error jmp start.exit .connection_was_closed: ccall [printf], _connection_was_closed jmp start.exit .wsanotinit: ccall [printf], _wsanotinit, eax, wsadata.size jmp start.exit .bind_error: ccall [printf], _bind_error jmp start.exit .listen_error: ccall [printf], _listen_error jmp start.exit .cant_createsocket: invoke WSAGetLastError ccall [printf], _cant_createsocket, eax jmp start.exit .cant_resolve: ccall [printf], _fmtstr, _cant_resolve jmp start.exit .select_error: invoke WSAGetLastError cinv printf, _select_error, eax jmp start.exit .accept_error: invoke WSAGetLastError cinv printf, _accept_error, eax jmp start.exit section '.idata' import data readable writeable library kernel32, 'kernel32.dll', \ winsock, 'ws2_32.dll', \ msvcrt, 'msvcrt.dll', \ user32, 'user32.dll' kernel32: import ExitProcess, 'ExitProcess', \ GetModuleHandle, 'GetModuleHandleA', \ GetLastError, 'GetLastError', \ WriteFile, 'WriteFile', \ GetStdHandle, 'GetStdHandle', \ RtlZeroMemory, 'RtlZeroMemory', \ lstrlen, 'lstrlen', \ lstrcmp, 'lstrcmp' user32: import RegisterClass, 'RegisterClassA', \ DefWindowProc, 'DefWindowProcA', \ CreateWindowEx, 'CreateWindowExA', \ DestroyWindow, 'DestroyWindow', \ GetMessage, 'GetMessageA', \ TranslateMessage, 'TranslateMessage', \ DispatchMessage, 'DispatchMessageA', \ MessageBox, 'MessageBoxA', \ ShowWindow, 'ShowWindow' winsock: import WSAStartup, 'WSAStartup', \ WSACleanup, 'WSACleanup', \ socket, 'socket', \ gethostbyname, 'gethostbyname', \ connect, 'connect', \ WSAGetLastError, 'WSAGetLastError', \ recv, 'recv', \ send, 'send', \ htons, 'htons', \ bind, 'bind', \ listen, 'listen', \ WSAAsyncSelect, 'WSAAsyncSelect', \ accept, 'accept', \ closesocket, 'closesocket' msvcrt: import printf, 'printf' ;--------------------------------------------------------------------- ; http.asm ;--------------------------------------------------------------------- ; Создание скрытого окна, которое будет получать сообщения от сокета make_http_window: ; Регистрируем класс окна mov eax, [hInstance] mov [wc.hInstance], eax mov [wc.hIcon], 0 mov [wc.hCursor], 0 mov [wc.style], 0 mov [wc.lpfnWndProc], http_window_proc mov [wc.cbClsExtra], 0 mov [wc.cbWndExtra], 0 mov [wc.hbrBackground], COLOR_BTNFACE+1 mov [wc.lpszMenuName], 0 mov [wc.lpszClassName], _http_window_class invoke RegisterClass, wc mov [http_class], eax test eax, eax jnz .create_http_window cinv printf, _fmtstr, _class_not_registered jmp start.exit .create_http_window: ; Создаем окно invoke GetModuleHandle, 0 invoke CreateWindowEx, NULL, [http_class], _http_window_name, \ WS_SYSMENU, 100, 100, 100, 100, NULL, NULL, \ [hInstance], 0 mov [hwnd], eax test eax, eax jnz start.msg_loop cinv printf, _fmtstr, _window_not_created br invoke GetLastError cinv printf, _fmtnum, eax jmp start.exit proc http_window_proc, hWnd, wmsg, wparam, lparam enter push ebx esi edi cmp [wmsg], WM_CREATE je .wmcreate cmp [wmsg], WM_DESTROY je .wmdestroy cmp [wmsg], WM_SOCKET je .wmsocket .defwndproc: invoke DefWindowProc, [hWnd], [wmsg], [wparam], [lparam] jmp .finish .wmcreate: cinv printf, _start_sockets ; Создаем сокет invoke socket, AF_INET, SOCK_STREAM, 0 cmp eax, -1 je error.cant_createsocket mov [sock], eax ; Указываем Windows, чтобы она извещала нас о входящих соединениях invoke WSAAsyncSelect, [sock], [hWnd], WM_SOCKET, FD_ACCEPT cmp eax, -1 je error.select_error ; Получаем адрес localhost invoke gethostbyname, _localhost test eax, eax jz error.cant_resolve mov eax, [eax+12] mov eax, [eax] mov eax, [eax] ; Подготавливаем saddr mov [saddr.sin_addr], eax mov [saddr.sin_family], AF_INET invoke htons, 80 mov [saddr.sin_port], ax ; Биндим сокет к локальному адресу invoke bind, [sock], saddr, saddr.size test eax, eax jnz error.bind_error ; Начинаем слушать порт invoke listen, [sock], 10 test eax, eax jnz error.listen_error jmp .done .wmdestroy: jmp .done ; Сообщение от WSAAsyncSelect .wmsocket: mov eax, [lparam] and eax, 0FFFFh cmp eax, FD_ACCEPT je .fd_accept cmp eax, FD_READ je .fd_read cmp eax, FD_CLOSE je .fd_close jmp .done .fd_accept: invoke accept, [wparam], iaddr, 0 cmp eax, -1 je error.accept_error invoke WSAAsyncSelect, eax, [hwnd], WM_SOCKET, FD_READ + FD_CLOSE jmp .done .fd_read: ; Обнуляем буфер invoke RtlZeroMemory, buf, INBUF_LEN ; Читаем данные из сокета invoke recv, [wparam], buf, INBUF_LEN, 0 push eax invoke GetStdHandle, -11 pop edx invoke WriteFile, eax, buf, edx, rv, 0 invoke send, [wparam], index, index_size, 0 invoke closesocket, [wparam] jmp .done .fd_close: invoke closesocket, [wparam] jmp .done .done: xor eax, eax .finish: pop ebx esi edi return ;--------------------------------------------------------------------- ; strings.inc ;--------------------------------------------------------------------- _http_window_class db 'http_server_window_class', 0 _http_window_name db 'Noname', 0 _fmtstr db '%s', 0 _fmtnum db '%d', 0 _br db 0Dh, 0Ah, 0 _window_not_created db 'Window is not created', 0 _class_not_registered db 'Class is not registered', 0 _localhost db 'localhost', 0 _cant_resolve db "Can't resolve host", 0 _cant_createsocket db "Can't create socket: %d", 0 _bind_error db 'Error binding to host', 0 _listen_error db 'Error starting listening', 0 _wsanotinit db 'WSA not initialized: %d, %d', 0 _connection_was_closed db 'Connection was closed', 0 _recv_error db 'Receiving error', 0 _select_error db 'WSAAsyncSelect error %d', 0 _accept_error db 'Accept error %d', 0 _start_sockets db 'Starting sockets', 0Dh, 0Ah, 0 _shutdown_sockets db 'Shutdowning sockets', 0 _inbound_connection db 'Inbound connection', 0 _method_get db 'GET', 0 index db 'HTTP/1.1 200 Ok', 0Dh, 0Ah db 'Content-type: text/html', 0Dh, 0Ah db 0Dh, 0Ah db '<html>', 0Dh, 0Ah db '<head>', 0Dh, 0Ah db '<title>Welcome to HTTP Server!</title>', 0Dh, 0Ah db '</head>', 0Dh, 0Ah db '<body>', 0Dh, 0Ah db '<h2>HTTP Server Online</h2>', 0Dh, 0Ah db 'Best http server in the world!', 0Dh, 0Ah db '</body>', 0Dh, 0Ah db '</html>' index_size = $-indexАнализ кода
"Веб-сервер", чей исходный код был приведен выше, очень примитивен. Он умеет только принимать запрос и, не проверяя его на правильность, выдавать только приветственную html-страницу.
В http.asm все, я надеюсь, достаточно понятно. Мы инициализируем сокеты, создаем окно (для чего, я поясню позже), входим в цикл обработки сообщений, а перед тем, как завершить работу приложения, вызываем WSACleanup.
Самое интересное находится в http_window.asm. При обработке сообщения WM_CREATE мы создаем сокет:
Код (Text):
; Создаем сокет invoke socket, AF_INET, SOCK_STREAM, 0 cmp eax, -1 je error.cant_createsocket mov [sock], eaxА потом вызываем следующую функцию:
Код (Text):
; Указываем Windows, чтобы она извещала нас о входящих соединениях invoke WSAAsyncSelect, [sock], [hWnd], WM_SOCKET, FD_ACCEPTВот что об этой функции говорит Platform SDK:
[ начало описания функции WSAAsynctSelect ]
WSAAsyncSelect
Функция WSAAsyncSelect указывает Windows посылать сообщения о событиях, касающихся определенного сокета.
);Код (Text):
int WSAAsyncSelect( SOCKET <>, HWND hWnd <>, unsigned int wMsg <>, long lEvent <>Параметры:
s - Дескриптор сокета, о событиях, связанных с которым, будет сообщаться.
hWnd - Хэндл окна, которому будут посылаться эти сообщения.
wMsg - Сообщение, которое будет посылаться.
lEvent - Битовая маска, в которой задаются интересующие события.
Возвращаемые значения
Если вызов функции WSAAsyncSelect прошел успешно, возвращаемое значение будет равно нулю. В противном случае будет возвращено SOCKET_ERROR, а код ошибки можно будет получить, вызвав WSAGetLastError.
[ конец описания ]
Учтите, что после того, как WSAAsyncSelect отошлет вам сообщение о конкретном событии, связанном с сокетом, то пока вы не предпримите определенных действий, нового сообщения о таком же событии вы не получите. Например, если вы получили сообщение FD_ACCEPT (кто-то пытается законнектиться к вам), то сообщения о другой попытки коннекта вы не получите до тех пор, пока не вызовите функцию accept.
Мы задаем WM_SOCKET, определенное в http.asm, в качестве сообщение, которое будет присылаться Windows, когда произойдет интересующее нас сообщение. Необходимая информация будет находиться в wParam (дескриптор сокета, с которым связано событие) и в lParam (в нижнем слове - код события).
Теперь, когда кто-нибудь попытаемся приконнектиться к сокету, наше окно получит соответствующее уведомление от операционной системы. Впрочем, сначала нужно ассоциировать созданный сокет с определенным адресом и портом, к которым и должны будут коннектиться посетители веб-сервера.
Код (Text):
; Получаем адрес localhost invoke gethostbyname, _localhost test eax, eax jz error.cant_resolve mov eax, [eax+12] mov eax, [eax] mov eax, [eax] ; Подготавливаем saddr mov [saddr.sin_addr], eax mov [saddr.sin_family], AF_INET invoke htons, 80 mov [saddr.sin_port], axВеб-сервер будет "висеть" на localhost'е (т.е. на локальной машине) на 80-ом порту, который является стандартным HTTP-портом. Если в адресе сайта прямо не указан порт, то браузер будет обращаться к 80-ому порту.
Код (Text):
; Начинаем слушать порт invoke listen, [sock], 10 test eax, eax jnz error.listen_errorСобственно, в данных строчках и содержится ответ на то, как сделать из приложения сервер (не обязательно web). Это делает функция listen.
[ начало описания функции listen ]
listen
Функция listen устанавливает сокет в состояние, в котором он слушает порт на предмет входящих соединений.
);Код (Text):
int listen( SOCKET <>, int backlog <>Параметры
s - Дескриптор сокета
backlog - Максимальное количество входящих соединений.
Возвращаемые значения
Если во время вызова не произошло никакой ошибки, listen возвратит ноль. В противном случае будет возвращено значение SOCKET_ERROR, а код ошибки можно будет получить с помощью функции WSAGetLastError.
[ конец описания ]
Код (Text):
; Сообщение от WSAAsyncSelect .wmsocket: mov eax, [lparam] and eax, 0FFFFh cmp eax, FD_ACCEPT je .fd_accept cmp eax, FD_READ je .fd_read cmp eax, FD_CLOSE je .fd_close jmp .doneБыло получено сообщение WM_SOCKET. Это значит, что произошло какое-то интересующее нас событие, связанное со слушающим сокетом.
Код (Text):
.fd_accept: invoke accept, [wparam], iaddr, 0 cmp eax, -1 je error.accept_errorКто-то пытается подсоединиться к нашему веб-серверу. Вызываем функцию accept, чтобы разрешить входящее соединение.
[ начало описания функции accept ]
accept
Функция accept разрешает входящее соединение.
);Код (Text):
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlenПараметры
s - Дескриптор сокета, который ранее был помещен в состояние прослушивания с помощью функции listen. Фактическое соединение осуществляется с помощью сокета, который возвращается accept'ом.
addr - Необязательный указатель на буфер, который получит адрес того, кто пытается подсоединиться к серверу.
addrlen - Необязательный указатель на двойное слово, которое содержит длину addr.
Возвращаемые значения
Если не произошло никакой ошибки, accept возвратит дескриптор нового сокета, через который и будет происходить соединение.
В противном случае будет возвращен INVALID_SOCKET, а код ошибки можно будет получить с помощью функции WSAGetLastError.
Переменная, на которую указывает addrlen, вначале содержит объем, занятый структурой, на которую указывает addr. По возвращении она будет содержать длину возвращенного адреса в байтах.
[ конец описания ]
Код (Text):
invoke WSAAsyncSelect, eax, [hwnd], WM_SOCKET, FD_READ + FD_CLOSE jmp .doneСоединение разрешено, и мы вызываем функцию WSAAsyncSelect, чтобы получить соответствующее уведомление, когда можно будет читать из сокета или он будет закрыт.
Код (Text):
.fd_read: ; Обнуляем буфер invoke RtlZeroMemory, buf, INBUF_LEN ; Читаем данные из сокета invoke recv, [wparam], buf, INBUF_LEN, 0 push eax invoke GetStdHandle, -11 pop edx invoke WriteFile, eax, buf, edx, rv, 0 invoke send, [wparam], index, index_size, 0 invoke closesocket, [wparam] jmp .doneЗдесь все просто. Пришло сообщение о том, что можно читать из сокета, что мы и делаем. Все считанное мы выводим на консоль (интересно же, что клиент прислал). По-хорошему, здесь мы должны были бы провести синтаксический разбор запроса: выяснить, какой конкретно документ он хочет, отдать его, если такого документа нет, послать сообщение об ошибке и т.п. Но поскольку я минимализировал сервер почти до предела в плане функциональности , ничего этого здесь нет. Вместо этого мы шлем клиенту приветственный html.
Код (Text):
.fd_close: invoke closesocket, [wparam] jmp .doneЕсли сокет был закрыт клиентом, то мы его тоже закрываем со своей стороны.
Дополнительная литература
Для получения подробной информации о протоколе HTTP я рекомендую вам обратиться к RFC 2068.
Заключение
Надеюсь, вы почерпнули из этого туториала какую-нибудь полезную информацию. Напоследок мне хотелось бы сказать, что хотя составлять конкуренцию таким грандам как Apache и IIS без веских на то оснований, возможно, и не стоит, тем не менее, собственный маленький веб-сервер может быть очень полезен. Мне, например, предложили встроить в него механизм самораспространения, "чтобы он сам приходил к людям на дом" и устанавливался "через упрощенную процедуру инсталляции" ака Outlook. Другим, менее чреватым в плане возможных последствий для автора, вариантом может быть создание утилиты удаленного (не обязательно скрытого) администрирования, причем в качестве клиента будет выступать браузер, что весьма удобно, так как отпадет надобность в написании сопутствующей серверу клиентской программы. Возможно, вы найдете еще какое-нибудь применение для http-сервера. Все в ваших руках!
(c) Aquila / Hi-Tech, 2002 © Aquila / WASM.RU
Туториал по написанию собственного веб-сервера
Дата публикации 5 авг 2002