Туториал по написанию собственного веб-сервера
Введение
Интернет играет в нашей жизни большую роль. Мы берем почту, узнаем свежую информацию, отлавливаем своих знакомых через ICQ… Но что ассоциируется со словом Интернет у большинства людей в первую очередь? Сайты. Многие при слове «Интернет» вспоминают свои любимые сайты, а некоторые (не слишком продвинутые) ставят знак равенства между WWW и Интернетом, хотя в последнем есть много другого интересного: email, IRC, p2p-сети, MUD’ы и так далее. Но World Wide Web играет доминирующую роль.
В основе WWW лежит протокол HyperText Transfer Protocol. Надо сказать, что HTTP может использоваться не только для передачи сайтов, но и для передачи всего чтобы то ни было. С помощью протокола HTTP мы можем скачать, например, недавно вышедший фильм или свежие mp3 :). В p2p-сетях HTTP-протокол применяется именно в этих целях.
В данном пособии мы рассмотрим, как написать простой веб-сервер. Я предполагаю, что вы знакомы с основами программирования winsock и умеете создавать сокеты и коннектиться к чему-нибудь :).
Основы HyperText Transfer Protocol
Идея HTTP довольно проста. Клиент шлет запрос серверу, тот рассматривает его и шлет соответствующий ответ. Ответом может быть запрошенный файл, сообщение о том, что такого файла на сервере нет или что-то еще. Примерная структура запроса следующая:
<метод> <запрашиваемый_ресурс> HTTP/1.1<\n>
<заголовочное_поле>: <значение><\n>
<заголовочное_поле>: <значение><\n>
[..заголовочных полей может быть много..]<\n>
<заголовочное_поле>: <значение><\n>
<\n>
<метод> — вид запроса. Основных два: GET и POST. Друг от друга они отличаются, главным образом, способом передачи дополнительной информации, отсылающейся вместе с запросом. В этом туториале мы рассмотрим только метод GET — функционально он похож на POST, но несколько проще. О методе POST я расскажу во второй части данного туториала, если, конечно, таковая вообще появится на свет :).
<\n> — это два байта 0Dh, 0Ah, несомненно, хорошо знакомые всем ассемблерщикам :).
Таким образом, с методом мы определились. На данный момент запрос, который мы (в качестве клиента) должны будем послать серверу выглядит так:
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». Теперь наш запрос к серверу выглядит так:
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
Вот как теперь выглядит наш запрос:
GET /article.php?article=1016002 HTTP/1.1<\n>
User-Agent: ManualSender/1.0 :)<\n>
Host: www.wasm.ru<\n>
<\n>
Надо заметить, что хотя эти и другие заголовочные поля являются очень желательными, но, строго говоря, они не являются обязательными, то есть их может и не быть. Также я хочу обратить ваше внимание на то, что за последним заголовочным файлом следуют _два_ <\n>, а не один. Это важно.
Теперь взглянем на все вышеизложенное с точки зрения веб-сервера (ведь мы же собирались писать веб-сервер, помните? 🙂 ). Он ждет, пока к нему не поступит запрос, обрабатывает его и посылает ответ, который выглядит примерно так:
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 — здесь указывается длина данных (не включая сам ответ с заголовочными данными) в байтах.
Исходя из вышесказанного, ответ сервера будет выглядеть примерно так:
HTTP/1.1 200 Ok<\n>
Content-Type: text/html<\n>
Content-Length: <длина_документа><\n>
<\n>
<тело_документа>
Если мы хотим отослать простой html-файл, то можем сократить ответ до следующего:
HTTP/1.1 200 Ok<\n>
Content-Type: text/html<\n>
<\n>
<тело_документа>
В результате получаем следующее. Клиент шлет запрос на получение документа, лежащего, например, в корне сервера:
GET / HTTP/1.1<\n>
User-Agent: ManualSender/1.0 :)<\n>
Host: www.someoneserver.com<\n>
<\n>
Сервер получает этот запрос, обрабатывает и выдает корневую страницу:
HTTP/1.1 200 Ok<\n>
Content-Type: text/html<\n>
<\n>
<html>
<head><title>Добро пожаловать на HTTP-сервер!</title></head>
<body>Вы находитесь на нашем http-сервере</body>
</html>
Код приложения
;---------------------------------------------------------------------
; 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 мы создаем сокет:
; Создаем сокет
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
Вот что об этой функции говорит Platform SDK:
[ начало описания функции WSAAsynctSelect ]
WSAAsyncSelect
Функция WSAAsyncSelect указывает Windows посылать сообщения о событиях, касающихся определенного сокета.
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 (в нижнем слове — код события).
Теперь, когда кто-нибудь попытаемся приконнектиться к сокету, наше окно получит соответствующее уведомление от операционной системы. Впрочем, сначала нужно ассоциировать созданный сокет с определенным адресом и портом, к которым и должны будут коннектиться посетители веб-сервера.
; Получаем адрес 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-ому порту.
; Начинаем слушать порт
invoke listen, [sock], 10
test eax, eax
jnz error.listen_error
Собственно, в данных строчках и содержится ответ на то, как сделать из приложения сервер (не обязательно web). Это делает функция listen.
[ начало описания функции listen ]
listen
Функция listen устанавливает сокет в состояние, в котором он слушает порт на предмет входящих соединений.
int listen(
SOCKET <>,
int backlog <>
);
Параметры
s — Дескриптор сокета
backlog — Максимальное количество входящих соединений.
Возвращаемые значения
Если во время вызова не произошло никакой ошибки, listen возвратит ноль. В противном случае будет возвращено значение SOCKET_ERROR, а код ошибки можно будет получить с помощью функции WSAGetLastError.
[ конец описания ]
; Сообщение от 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. Это значит, что произошло какое-то интересующее нас событие, связанное со слушающим сокетом.
.fd_accept:
invoke accept, [wparam], iaddr, 0
cmp eax, -1
je error.accept_error
Кто-то пытается подсоединиться к нашему веб-серверу. Вызываем функцию accept, чтобы разрешить входящее соединение.
[ начало описания функции accept ]
accept
Функция accept разрешает входящее соединение.
SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);
Параметры
s — Дескриптор сокета, который ранее был помещен в состояние прослушивания с помощью функции listen. Фактическое соединение осуществляется с помощью сокета, который возвращается accept’ом.
addr — Необязательный указатель на буфер, который получит адрес того, кто пытается подсоединиться к серверу.
addrlen — Необязательный указатель на двойное слово, которое содержит длину addr.
Возвращаемые значения
Если не произошло никакой ошибки, accept возвратит дескриптор нового сокета, через который и будет происходить соединение.
В противном случае будет возвращен INVALID_SOCKET, а код ошибки можно будет получить с помощью функции WSAGetLastError.
Переменная, на которую указывает addrlen, вначале содержит объем, занятый структурой, на которую указывает addr. По возвращении она будет содержать длину возвращенного адреса в байтах.
[ конец описания ]
invoke WSAAsyncSelect, eax, [hwnd], WM_SOCKET, FD_READ + FD_CLOSE
jmp .done
Соединение разрешено, и мы вызываем функцию WSAAsyncSelect, чтобы получить соответствующее уведомление, когда можно будет читать из сокета или он будет закрыт.
.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.
.fd_close:
invoke closesocket, [wparam]
jmp .done
Если сокет был закрыт клиентом, то мы его тоже закрываем со своей стороны.
Дополнительная литература
Для получения подробной информации о протоколе HTTP я рекомендую вам обратиться к RFC 2068.
Заключение
Надеюсь, вы почерпнули из этого туториала какую-нибудь полезную информацию. Напоследок мне хотелось бы сказать, что хотя составлять конкуренцию таким грандам как Apache и IIS без веских на то оснований, возможно, и не стоит, тем не менее, собственный маленький веб-сервер может быть очень полезен. Мне, например, предложили встроить в него механизм самораспространения, «чтобы он сам приходил к людям на дом» и устанавливался «через упрощенную процедуру инсталляции» ака Outlook. Другим, менее чреватым в плане возможных последствий для автора, вариантом может быть создание утилиты удаленного (не обязательно скрытого) администрирования, причем в качестве клиента будет выступать браузер, что весьма удобно, так как отпадет надобность в написании сопутствующей серверу клиентской программы. Возможно, вы найдете еще какое-нибудь применение для http-сервера. Все в ваших руках!
(c) Aquila / Hi-Tech, 2002
[C] Aquila / WASM.RU
Источник: wasm.ru /05.08.2002/