Туториал по написанию собственного веб-сервера

Дата публикации 5 авг 2002

Туториал по написанию собственного веб-сервера — Архив WASM.RU

Введение

Интернет играет в нашей жизни большую роль. Мы берем почту, узнаем свежую информацию, отлавливаем своих знакомых через ICQ... Но что ассоциируется со словом Интернет у большинства людей в первую очередь? Сайты. Многие при слове "Интернет" вспоминают свои любимые сайты, а некоторые (не слишком продвинутые) ставят знак равенства между WWW и Интернетом, хотя в последнем есть много другого интересного: email, IRC, p2p-сети, MUD'ы и так далее. Но World Wide Web играет доминирующую роль.

В основе WWW лежит протокол HyperText Transfer Protocol. Надо сказать, что HTTP может использоваться не только для передачи сайтов, но и для передачи всего чтобы то ни было. С помощью протокола HTTP мы можем скачать, например, недавно вышедший фильм или свежие mp3 :smile3:. В p2p-сетях HTTP-протокол применяется именно в этих целях.

В данном пособии мы рассмотрим, как написать простой веб-сервер. Я предполагаю, что вы знакомы с основами программирования winsock и умеете создавать сокеты и коннектиться к чему-нибудь :smile3:.

Основы HyperText Transfer Protocol

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

Код (Text):
  1.  
  2. <метод> <запрашиваемый_ресурс> HTTP/1.1<\n>
  3. <заголовочное_поле>: <значение><\n>
  4. <заголовочное_поле>: <значение><\n>
  5. [..заголовочных полей может быть много..]<\n>
  6. <заголовочное_поле>: <значение><\n>
  7. <\n>

<метод> - вид запроса. Основных два: GET и POST. Друг от друга они отличаются, главным образом, способом передачи дополнительной информации, отсылающейся вместе с запросом. В этом туториале мы рассмотрим только метод GET - функционально он похож на POST, но несколько проще. О методе POST я расскажу во второй части данного туториала, если, конечно, таковая вообще появится на свет :smile3:.

<\n> - это два байта 0Dh, 0Ah, несомненно, хорошо знакомые всем ассемблерщикам :smile3:.

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

Код (Text):
  1.  
  2. GET &lt;запрашиваемый_ресурс&gt; HTTP/1.1&lt;\n&gt;
  3. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  4. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  5. [...]
  6. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  7. &lt;\n&gt;

Теперь нам нужно задать <запрашиваемый_ресурс>. Возьмем типичную ссылка на одном из лучших сайтов по программированию в Рунете WASM.RU (немного рекламы не помешает :smile3: ):

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):
  1.  
  2. GET /article.php?article=1016002 HTTP/1.1&lt;\n&gt;
  3. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  4. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  5. [...]
  6. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  7. &lt;\n&gt;

Теперь осталось разобраться с заголовочными полями. С их помощью клиент передает дополнительную информацию о себе или характере запроса. Например, довольно часто используемым заголовочным полем является '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):
  1.  
  2. GET /article.php?article=1016002 HTTP/1.1&lt;\n&gt;
  3. User-Agent: ManualSender/1.0 <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">&lt;\n&gt;
  4. Host: www.wasm.ru&lt;\n&gt;
  5. &lt;\n&gt;

Надо заметить, что хотя эти и другие заголовочные поля являются очень желательными, но, строго говоря, они не являются обязательными, то есть их может и не быть. Также я хочу обратить ваше внимание на то, что за последним заголовочным файлом следуют _два_ <\n>, а не один. Это важно.

Теперь взглянем на все вышеизложенное с точки зрения веб-сервера (ведь мы же собирались писать веб-сервер, помните? :smile3: ). Он ждет, пока к нему не поступит запрос, обрабатывает его и посылает ответ, который выглядит примерно так:

Код (Text):
  1.  
  2. HTTP/1.1 &lt;код_ответа&gt; &lt;сообщение&gt;&lt;\n&gt;
  3. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  4. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  5. [..заголовочных полей может быть много..]&lt;\n&gt;
  6. &lt;заголовочное_поле&gt;: &lt;значение&gt;&lt;\n&gt;
  7. &lt;\n&gt;
  8. &lt;тело_документа&gt;

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

HTTP/1.1 200 Ok

Если коды ответов жестко заданы стандартом (200 означает, что запрос был успешно обработан и будет возвращен соответствующий документ/информация), то <сообщение> зависит только от вашей фантазии (конечно, по смыслу оно должно совпадать с <кодом_ответа>. Например, вместо "HTTP/1.1 200 Ok" мы можем послать "HTTP/1.1 200 You want it, you'll get it" :smile3:.

В ответе сервера также могут быть заголовочные поля. Они содержат дополнительные сведения об ответе и данных, идущих вместе с ним. Далее я приведу важнейшие (для нас).

Content-Type - этот заголовок задает тип отдаваемых данных. Главнейшие типы заданы в стандарте, и от того, что вы напишите в данном поле, зависит поведение клиента при приеме данных. Например, если вы посылаете браузеру пользователя html-файл, то необходимо указать тип данных 'text/html', иначе браузер может отобразить файл неправильно, либо вообще не будет его отображать, а предложит пользователю его скачать.

Content-Length - здесь указывается длина данных (не включая сам ответ с заголовочными данными) в байтах.

Исходя из вышесказанного, ответ сервера будет выглядеть примерно так:

Код (Text):
  1.  
  2. HTTP/1.1 200 Ok&lt;\n&gt;
  3. Content-Type: text/html&lt;\n&gt;
  4. Content-Length: &lt;длина_документа&gt;&lt;\n&gt;
  5. &lt;\n&gt;
  6. &lt;тело_документа&gt;

Если мы хотим отослать простой html-файл, то можем сократить ответ до следующего:

Код (Text):
  1.  
  2. HTTP/1.1 200 Ok&lt;\n&gt;
  3. Content-Type: text/html&lt;\n&gt;
  4. &lt;\n&gt;
  5. &lt;тело_документа&gt;

В результате получаем следующее. Клиент шлет запрос на получение документа, лежащего, например, в корне сервера:

Код (Text):
  1.  
  2. GET / HTTP/1.1&lt;\n&gt;
  3. User-Agent: ManualSender/1.0 <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">&lt;\n&gt;
  4. Host: www.someoneserver.com&lt;\n&gt;
  5. &lt;\n&gt;

Сервер получает этот запрос, обрабатывает и выдает корневую страницу:

Код (Text):
  1.  
  2. HTTP/1.1 200 Ok&lt;\n&gt;
  3. Content-Type: text/html&lt;\n&gt;
  4. &lt;\n&gt;
  5. &lt;html&gt;
  6. &lt;head&gt;&lt;title&gt;Добро пожаловать на HTTP-сервер!&lt;/title&gt;&lt;/head&gt;
  7. &lt;body&gt;Вы находитесь на нашем http-сервере&lt;/body&gt;
  8. &lt;/html&gt;

Код приложения

Код (Text):
  1.  
  2. ;---------------------------------------------------------------------
  3. ; http.asm
  4. ;---------------------------------------------------------------------
  5.  
  6. format PE console
  7.  
  8. entry start
  9.  
  10. ; Подключаемые файлы
  11.  
  12. include '..\..\include\kernel.inc'
  13. include '..\..\include\user.inc'
  14. include '..\..\include\macro\stdcall.inc'
  15. include '..\..\include\macro\import.inc'
  16. include 'winsock.inc'
  17. include 'macros.inc'
  18.  
  19. ; Используемые значения
  20.  
  21. WM_SOCKET = WM_USER+100
  22. INBUF_LEN     = 100000
  23.  
  24. ; Секции программы
  25.  
  26. section '.data' data readable writeable
  27.  
  28. include 'strings.inc'
  29.  
  30.   hInstance dd ?
  31.   http_class dd ?
  32.   hwnd dd ?
  33.   msg dd ?
  34.   sock dd ?
  35.   rv dd ?
  36.   wc WNDCLASS
  37.   wsadata WSADATA
  38.   saddr SOCKADDR_TCP
  39.   iaddr SOCKADDR_TCP
  40.   buf rb INBUF_LEN
  41.  
  42. section '.code' code executable readable
  43.  
  44. include 'http_window.asm'
  45.  
  46. start:
  47.  
  48.       invoke GetModuleHandle, 0
  49.       mov [hInstance], eax
  50.  
  51.       ; Инициализируем сокеты
  52.       invoke WSAStartup, 101h, wsadata
  53.       test eax, eax
  54.       jnz error.wsanotinit
  55.  
  56.       ; Создаем окно
  57.       jmp make_http_window
  58.  
  59.       ; Цикл обработки сообщений
  60.    .msg_loop:
  61.         invoke GetMessage,msg,NULL,0,0
  62.         test eax,eax
  63.         jz .exit
  64.         invoke TranslateMessage,msg
  65.         invoke DispatchMessage,msg
  66.         jmp .msg_loop
  67.  
  68.    .exit:
  69.       ; Очищаем сокеты
  70.       invoke WSACleanup
  71.       invoke ExitProcess, 0
  72.  
  73. error:
  74.  
  75.    .recv_error:
  76.  
  77.       ccall [printf], _recv_error
  78.       jmp start.exit
  79.  
  80.    .connection_was_closed:
  81.  
  82.       ccall [printf], _connection_was_closed
  83.       jmp start.exit
  84.  
  85.    .wsanotinit:
  86.  
  87.       ccall [printf], _wsanotinit, eax, wsadata.size
  88.       jmp start.exit
  89.  
  90.    .bind_error:
  91.  
  92.       ccall [printf], _bind_error
  93.       jmp start.exit
  94.  
  95.    .listen_error:
  96.  
  97.       ccall [printf], _listen_error
  98.       jmp start.exit
  99.  
  100.    .cant_createsocket:
  101.  
  102.       invoke WSAGetLastError
  103.       ccall [printf], _cant_createsocket, eax
  104.       jmp start.exit
  105.  
  106.    .cant_resolve:
  107.  
  108.       ccall [printf], _fmtstr, _cant_resolve
  109.       jmp start.exit
  110.  
  111.    .select_error:
  112.       invoke WSAGetLastError
  113.       cinv printf, _select_error, eax
  114.       jmp start.exit
  115.  
  116.    .accept_error:
  117.       invoke WSAGetLastError
  118.       cinv printf, _accept_error, eax
  119.       jmp start.exit
  120.  
  121.  
  122. section '.idata' import data readable writeable
  123.  
  124.   library kernel32, 'kernel32.dll', \
  125.           winsock, 'ws2_32.dll', \
  126.           msvcrt, 'msvcrt.dll', \
  127.           user32, 'user32.dll'
  128.  
  129.   kernel32:
  130.   import ExitProcess, 'ExitProcess', \
  131.          GetModuleHandle, 'GetModuleHandleA', \
  132.          GetLastError, 'GetLastError', \
  133.          WriteFile, 'WriteFile', \
  134.          GetStdHandle, 'GetStdHandle', \
  135.          RtlZeroMemory, 'RtlZeroMemory', \
  136.          lstrlen, 'lstrlen', \
  137.          lstrcmp, 'lstrcmp'
  138.  
  139.   user32:
  140.   import RegisterClass, 'RegisterClassA', \
  141.          DefWindowProc, 'DefWindowProcA', \
  142.          CreateWindowEx, 'CreateWindowExA', \
  143.          DestroyWindow, 'DestroyWindow', \
  144.          GetMessage, 'GetMessageA', \
  145.          TranslateMessage, 'TranslateMessage', \
  146.          DispatchMessage, 'DispatchMessageA', \
  147.          MessageBox, 'MessageBoxA', \
  148.          ShowWindow, 'ShowWindow'
  149.  
  150.   winsock:
  151.   import WSAStartup, 'WSAStartup', \
  152.          WSACleanup, 'WSACleanup', \
  153.          socket, 'socket', \
  154.          gethostbyname, 'gethostbyname', \
  155.          connect, 'connect', \
  156.          WSAGetLastError, 'WSAGetLastError', \
  157.          recv, 'recv', \
  158.          send, 'send', \
  159.          htons, 'htons', \
  160.          bind, 'bind', \
  161.          listen, 'listen', \
  162.          WSAAsyncSelect, 'WSAAsyncSelect', \
  163.          accept, 'accept', \
  164.          closesocket, 'closesocket'
  165.  
  166.   msvcrt:
  167.   import printf, 'printf'
  168.  
  169. ;---------------------------------------------------------------------
  170. ; http.asm
  171. ;---------------------------------------------------------------------
  172. ; Создание скрытого окна, которое будет получать сообщения от сокета
  173. make_http_window:
  174.  
  175.      ; Регистрируем класс окна
  176.      mov eax, [hInstance]
  177.      mov [wc.hInstance], eax
  178.      mov [wc.hIcon], 0
  179.      mov [wc.hCursor], 0
  180.      mov [wc.style], 0
  181.      mov [wc.lpfnWndProc], http_window_proc
  182.      mov [wc.cbClsExtra], 0
  183.      mov [wc.cbWndExtra], 0
  184.      mov [wc.hbrBackground], COLOR_BTNFACE+1
  185.      mov [wc.lpszMenuName], 0
  186.      mov [wc.lpszClassName], _http_window_class
  187.      invoke  RegisterClass, wc
  188.      mov [http_class], eax
  189.      test eax, eax
  190.      jnz .create_http_window
  191.      cinv printf, _fmtstr, _class_not_registered
  192.      jmp start.exit
  193.   .create_http_window:
  194.      ; Создаем окно
  195.      invoke GetModuleHandle, 0
  196.      invoke CreateWindowEx, NULL, [http_class], _http_window_name, \
  197.                             WS_SYSMENU, 100, 100, 100, 100, NULL, NULL, \
  198.                             [hInstance], 0
  199.      mov [hwnd], eax
  200.      test eax, eax
  201.      jnz start.msg_loop
  202.      cinv printf, _fmtstr, _window_not_created
  203.      br
  204.      invoke GetLastError
  205.      cinv printf, _fmtnum, eax
  206.  
  207.      jmp start.exit
  208.  
  209. proc http_window_proc, hWnd, wmsg, wparam, lparam
  210.      enter
  211.      push ebx esi edi
  212.      cmp [wmsg], WM_CREATE
  213.      je .wmcreate
  214.      cmp [wmsg], WM_DESTROY
  215.      je .wmdestroy
  216.      cmp [wmsg], WM_SOCKET
  217.      je .wmsocket
  218.  
  219.   .defwndproc:
  220.      invoke DefWindowProc, [hWnd], [wmsg], [wparam], [lparam]
  221.      jmp .finish
  222.  
  223.   .wmcreate:
  224.  
  225.      cinv printf, _start_sockets
  226.      ; Создаем сокет
  227.      invoke socket, AF_INET, SOCK_STREAM, 0
  228.      cmp eax, -1
  229.      je error.cant_createsocket
  230.      mov [sock], eax
  231.      ; Указываем Windows, чтобы она извещала нас о входящих соединениях
  232.      invoke WSAAsyncSelect, [sock], [hWnd], WM_SOCKET, FD_ACCEPT
  233.      cmp eax, -1
  234.      je error.select_error
  235.  
  236.      ; Получаем адрес localhost
  237.      invoke gethostbyname, _localhost
  238.      test eax, eax
  239.      jz error.cant_resolve
  240.      mov eax, [eax+12]
  241.      mov eax, [eax]
  242.      mov eax, [eax]
  243.  
  244.      ; Подготавливаем saddr
  245.      mov [saddr.sin_addr], eax
  246.      mov [saddr.sin_family], AF_INET
  247.      invoke htons, 80
  248.      mov [saddr.sin_port], ax
  249.      ; Биндим сокет к локальному адресу
  250.      invoke bind, [sock], saddr, saddr.size
  251.      test eax, eax
  252.      jnz error.bind_error
  253.  
  254.      ; Начинаем слушать порт
  255.      invoke listen, [sock], 10
  256.      test eax, eax
  257.      jnz error.listen_error
  258.  
  259.      jmp .done
  260.  
  261.   .wmdestroy:
  262.      jmp .done
  263.  
  264.   ; Сообщение от WSAAsyncSelect
  265.   .wmsocket:
  266.      mov eax, [lparam]
  267.      and eax, 0FFFFh
  268.      cmp eax, FD_ACCEPT
  269.      je .fd_accept
  270.      cmp eax, FD_READ
  271.      je .fd_read
  272.      cmp eax, FD_CLOSE
  273.      je .fd_close
  274.      jmp .done
  275.  
  276.   .fd_accept:
  277.      invoke accept, [wparam], iaddr, 0
  278.      cmp eax, -1
  279.      je error.accept_error
  280.      invoke WSAAsyncSelect, eax, [hwnd], WM_SOCKET, FD_READ + FD_CLOSE
  281.      jmp .done
  282.  
  283.   .fd_read:
  284.      ; Обнуляем буфер
  285.      invoke RtlZeroMemory, buf, INBUF_LEN
  286.      ; Читаем данные из сокета
  287.      invoke recv, [wparam], buf, INBUF_LEN, 0
  288.      push eax
  289.      invoke GetStdHandle, -11
  290.      pop edx
  291.      invoke WriteFile, eax, buf, edx, rv, 0
  292.      invoke send, [wparam], index, index_size, 0
  293.      invoke closesocket, [wparam]
  294.      jmp .done
  295.  
  296.   .fd_close:
  297.      invoke closesocket, [wparam]
  298.      jmp .done
  299.  
  300.   .done:
  301.      xor eax, eax
  302.  
  303.   .finish:
  304.      pop ebx esi edi
  305.      return
  306. ;---------------------------------------------------------------------
  307. ; strings.inc
  308. ;---------------------------------------------------------------------
  309. _http_window_class db 'http_server_window_class', 0
  310. _http_window_name db 'Noname', 0
  311. _fmtstr db '%s', 0
  312. _fmtnum db '%d', 0
  313. _br db 0Dh, 0Ah, 0
  314. _window_not_created db 'Window is not created', 0
  315. _class_not_registered db 'Class is not registered', 0
  316. _localhost db 'localhost', 0
  317. _cant_resolve db "Can't resolve host", 0
  318. _cant_createsocket db "Can't create socket: %d", 0
  319. _bind_error db 'Error binding to host', 0
  320. _listen_error db 'Error starting listening', 0
  321. _wsanotinit db 'WSA not initialized: %d, %d', 0
  322. _connection_was_closed db 'Connection was closed', 0
  323. _recv_error db 'Receiving error', 0
  324. _select_error db 'WSAAsyncSelect error %d', 0
  325. _accept_error db 'Accept error %d', 0
  326. _start_sockets db 'Starting sockets', 0Dh, 0Ah, 0
  327. _shutdown_sockets db 'Shutdowning sockets', 0
  328. _inbound_connection db 'Inbound connection', 0
  329. _method_get db 'GET', 0
  330.  
  331. index db 'HTTP/1.1 200 Ok', 0Dh, 0Ah
  332.       db 'Content-type: text/html', 0Dh, 0Ah
  333.       db 0Dh, 0Ah
  334.       db '&lt;html&gt;', 0Dh, 0Ah
  335.       db '&lt;head&gt;', 0Dh, 0Ah
  336.       db '&lt;title&gt;Welcome to HTTP Server!&lt;/title&gt;', 0Dh, 0Ah
  337.       db '&lt;/head&gt;', 0Dh, 0Ah
  338.       db '&lt;body&gt;', 0Dh, 0Ah
  339.       db '&lt;h2&gt;HTTP Server Online&lt;/h2&gt;', 0Dh, 0Ah
  340.       db 'Best http server in the world!', 0Dh, 0Ah
  341.       db '&lt;/body&gt;', 0Dh, 0Ah
  342.       db '&lt;/html&gt;'
  343. index_size = $-index

Анализ кода

"Веб-сервер", чей исходный код был приведен выше, очень примитивен. Он умеет только принимать запрос и, не проверяя его на правильность, выдавать только приветственную html-страницу.

В http.asm все, я надеюсь, достаточно понятно. Мы инициализируем сокеты, создаем окно (для чего, я поясню позже), входим в цикл обработки сообщений, а перед тем, как завершить работу приложения, вызываем WSACleanup.

Самое интересное находится в http_window.asm. При обработке сообщения WM_CREATE мы создаем сокет:

Код (Text):
  1.  
  2.      ; Создаем сокет
  3.      invoke socket, AF_INET, SOCK_STREAM, 0
  4.      cmp eax, -1
  5.      je error.cant_createsocket
  6.      mov [sock], eax

А потом вызываем следующую функцию:

Код (Text):
  1.  
  2.      ; Указываем Windows, чтобы она извещала нас о входящих соединениях
  3.      invoke WSAAsyncSelect, [sock], [hWnd], WM_SOCKET, FD_ACCEPT

Вот что об этой функции говорит Platform SDK:

[ начало описания функции WSAAsynctSelect ]

WSAAsyncSelect

Функция WSAAsyncSelect указывает Windows посылать сообщения о событиях, касающихся определенного сокета.

Код (Text):
  1.  
  2. int WSAAsyncSelect(
  3.   SOCKET &lt;&gt;,          
  4.   HWND hWnd &lt;&gt;,          
  5.   unsigned int wMsg &lt;&gt;,  
  6.   long lEvent &lt;&gt;
);

Параметры:

s - Дескриптор сокета, о событиях, связанных с которым, будет сообщаться.

hWnd - Хэндл окна, которому будут посылаться эти сообщения.

wMsg - Сообщение, которое будет посылаться.

lEvent - Битовая маска, в которой задаются интересующие события.

Возвращаемые значения

Если вызов функции WSAAsyncSelect прошел успешно, возвращаемое значение будет равно нулю. В противном случае будет возвращено SOCKET_ERROR, а код ошибки можно будет получить, вызвав WSAGetLastError.

[ конец описания ]

Учтите, что после того, как WSAAsyncSelect отошлет вам сообщение о конкретном событии, связанном с сокетом, то пока вы не предпримите определенных действий, нового сообщения о таком же событии вы не получите. Например, если вы получили сообщение FD_ACCEPT (кто-то пытается законнектиться к вам), то сообщения о другой попытки коннекта вы не получите до тех пор, пока не вызовите функцию accept.

Мы задаем WM_SOCKET, определенное в http.asm, в качестве сообщение, которое будет присылаться Windows, когда произойдет интересующее нас сообщение. Необходимая информация будет находиться в wParam (дескриптор сокета, с которым связано событие) и в lParam (в нижнем слове - код события).

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

Код (Text):
  1.  
  2.      ; Получаем адрес localhost
  3.      invoke gethostbyname, _localhost
  4.      test eax, eax
  5.      jz error.cant_resolve
  6.      mov eax, [eax+12]
  7.      mov eax, [eax]
  8.      mov eax, [eax]
  9.  
  10.      ; Подготавливаем saddr
  11.      mov [saddr.sin_addr], eax
  12.      mov [saddr.sin_family], AF_INET
  13.      invoke htons, 80
  14.      mov [saddr.sin_port], ax

Веб-сервер будет "висеть" на localhost'е (т.е. на локальной машине) на 80-ом порту, который является стандартным HTTP-портом. Если в адресе сайта прямо не указан порт, то браузер будет обращаться к 80-ому порту.

Код (Text):
  1.  
  2.      ; Начинаем слушать порт
  3.      invoke listen, [sock], 10
  4.      test eax, eax
  5.      jnz error.listen_error

Собственно, в данных строчках и содержится ответ на то, как сделать из приложения сервер (не обязательно web). Это делает функция listen.

[ начало описания функции listen ]

listen

Функция listen устанавливает сокет в состояние, в котором он слушает порт на предмет входящих соединений.

Код (Text):
  1.  
  2. int listen(
  3.   SOCKET &lt;&gt;,    
  4.   int backlog &lt;&gt;
);

Параметры

s - Дескриптор сокета

backlog - Максимальное количество входящих соединений.

Возвращаемые значения

Если во время вызова не произошло никакой ошибки, listen возвратит ноль. В противном случае будет возвращено значение SOCKET_ERROR, а код ошибки можно будет получить с помощью функции WSAGetLastError.

[ конец описания ]

Код (Text):
  1.  
  2.   ; Сообщение от WSAAsyncSelect
  3.   .wmsocket:
  4.      mov eax, [lparam]
  5.      and eax, 0FFFFh
  6.      cmp eax, FD_ACCEPT
  7.      je .fd_accept
  8.      cmp eax, FD_READ
  9.      je .fd_read
  10.      cmp eax, FD_CLOSE
  11.      je .fd_close
  12.      jmp .done

Было получено сообщение WM_SOCKET. Это значит, что произошло какое-то интересующее нас событие, связанное со слушающим сокетом.

Код (Text):
  1.  
  2.   .fd_accept:
  3.      invoke accept, [wparam], iaddr, 0
  4.      cmp eax, -1
  5.      je error.accept_error

Кто-то пытается подсоединиться к нашему веб-серверу. Вызываем функцию accept, чтобы разрешить входящее соединение.

[ начало описания функции accept ]

accept

Функция accept разрешает входящее соединение.

Код (Text):
  1.  
  2. SOCKET accept(
  3.   SOCKET s,
  4.   struct sockaddr FAR *addr,
  5.   int FAR *addrlen
);

Параметры

s - Дескриптор сокета, который ранее был помещен в состояние прослушивания с помощью функции listen. Фактическое соединение осуществляется с помощью сокета, который возвращается accept'ом.

addr - Необязательный указатель на буфер, который получит адрес того, кто пытается подсоединиться к серверу.

addrlen - Необязательный указатель на двойное слово, которое содержит длину addr.

Возвращаемые значения

Если не произошло никакой ошибки, accept возвратит дескриптор нового сокета, через который и будет происходить соединение.

В противном случае будет возвращен INVALID_SOCKET, а код ошибки можно будет получить с помощью функции WSAGetLastError.

Переменная, на которую указывает addrlen, вначале содержит объем, занятый структурой, на которую указывает addr. По возвращении она будет содержать длину возвращенного адреса в байтах.

[ конец описания ]

Код (Text):
  1.  
  2.      invoke WSAAsyncSelect, eax, [hwnd], WM_SOCKET, FD_READ + FD_CLOSE
  3.      jmp .done

Соединение разрешено, и мы вызываем функцию WSAAsyncSelect, чтобы получить соответствующее уведомление, когда можно будет читать из сокета или он будет закрыт.

Код (Text):
  1.  
  2.   .fd_read:
  3.      ; Обнуляем буфер
  4.      invoke RtlZeroMemory, buf, INBUF_LEN
  5.      ; Читаем данные из сокета
  6.      invoke recv, [wparam], buf, INBUF_LEN, 0
  7.      push eax
  8.      invoke GetStdHandle, -11
  9.      pop edx
  10.      invoke WriteFile, eax, buf, edx, rv, 0
  11.      invoke send, [wparam], index, index_size, 0
  12.      invoke closesocket, [wparam]
  13.      jmp .done

Здесь все просто. Пришло сообщение о том, что можно читать из сокета, что мы и делаем. Все считанное мы выводим на консоль (интересно же, что клиент прислал). По-хорошему, здесь мы должны были бы провести синтаксический разбор запроса: выяснить, какой конкретно документ он хочет, отдать его, если такого документа нет, послать сообщение об ошибке и т.п. Но поскольку я минимализировал сервер почти до предела в плане функциональности :smile3:, ничего этого здесь нет. Вместо этого мы шлем клиенту приветственный html.

Код (Text):
  1.  
  2.   .fd_close:
  3.      invoke closesocket, [wparam]
  4.      jmp .done

Если сокет был закрыт клиентом, то мы его тоже закрываем со своей стороны.

Дополнительная литература

Для получения подробной информации о протоколе HTTP я рекомендую вам обратиться к RFC 2068.

Заключение

Надеюсь, вы почерпнули из этого туториала какую-нибудь полезную информацию. Напоследок мне хотелось бы сказать, что хотя составлять конкуренцию таким грандам как Apache и IIS без веских на то оснований, возможно, и не стоит, тем не менее, собственный маленький веб-сервер может быть очень полезен. Мне, например, предложили встроить в него механизм самораспространения, "чтобы он сам приходил к людям на дом" и устанавливался "через упрощенную процедуру инсталляции" ака Outlook. Другим, менее чреватым в плане возможных последствий для автора, вариантом может быть создание утилиты удаленного (не обязательно скрытого) администрирования, причем в качестве клиента будет выступать браузер, что весьма удобно, так как отпадет надобность в написании сопутствующей серверу клиентской программы. Возможно, вы найдете еще какое-нибудь применение для http-сервера. Все в ваших руках!

(c) Aquila / Hi-Tech, 2002 © Aquila / WASM.RU


0 2.148
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532