На главную Назад Вперёд

Простой веб-сервер

В этой главе будет продемонстрировано устройство простого веб-сервера на основе библиотеки Spock. В качестве демонстрации мы напишем игру «угадай число».

Что такое веб-сервер?

Веб-сервером называют приложение, обрабатывающее HTTP-запросы. Аббревиатурой HTTP обозначается протокол (формальный язык и договорённость о способах его применения), лежащий в основе Всемирной Паутины, но по факту вышедший далеко за её пределы.

Большинство пользователей вычислительной техники сталкиваются с веб-браузерами – клиентами веб-серверов. Получая запрос от пользователя вида

http://foobar.baz/foo/bar/baz

браузер обычно делает как минимум следующие две вещи:

  1. Отправляет на DNS-сервер запрос о том, что вообще такое foobar.baz. Обычно под такими именами скрываются идентификаторы различных устройств, подключённых к интернету. Эти идентификаторы называются IP-адресами и представляют собой либо 32-битные числа (IP версии 4), либо 128-битные числа (IP версии 6). DNS – служба, которая ставит в соответствие текстам (называемым доменными именами) какие-то данные (обычно – адреса устройств).

  2. Отправляет по найденному IP-адресу HTTP-запрос GET /foo/bar/baz на 80-й порт. Словом порт в данном контексте называется 16-битное число, позволяющее операционной системе определить, какому именно приложению предназначен тот или иной пакет, пришедший по сети.

Текст /foo/bar/baz называется идентификатором ресурса (URI). Полный адрес http://foobar.baz/foo/bar/baz называется локатором ресурса (URL). К сожалению, разница между терминами URI и URL довольно размыта, поэтому часто они используются взаимозаменяемо.

Слово GET называется HTTP-методом. Также часто встречается метод POST. Остальные методы протокола HTTP используются гораздо реже. Выделим основные различия между GET и POST методами:

Hello-сервер

При помощи Cabal Install или Stack создайте новый проект с зависимостями mtl, text и Spock. Пакет Spock – одна из огромного количества высокоуровневых библиотек для написания веб-серверов на Haskell. Почти все такие библиотеки основаны на низкоуровневой библиотеке wai, которую мы напрямую трогать не будем.

Вот – код простейшего сервера:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import           Web.Spock(runSpock,spock)
import qualified Web.Spock        as Spock
import           Web.Spock.Config as Spock


main = do
    cfg <- defaultSpockCfg () PCNoDatabase ()  -- стандартные параметры сервера
    
    runSpock 3000          -- порт
             (server cfg)  -- маршрутизатор



server cfg = spock cfg $ do
    
    Spock.get Spock.root $ do
        Spock.text "Hello world"

Запустив его и пройдя в браузере по ссылке

http://127.0.0.1:3000/

Вы увидите текст Hello world.

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

Наш первый сервер обрабатывает только один вид запросов: GET /. Маршрутизатор запросов в Spock строится из элементарных маршрутизаторов при помощи комбинатора (>>) (или do-нотации). Элементарные маршрутизаторы строятся функциями get, post и т.п. (по названиям HTTP-методов). Каждая такая функция имеет два аргумента: шаблон идентификатора ресурса и обработчик запроса. Шаблон root обозначает идентификатор /.

Сложение чисел

Добавим к нашему серверу функциональнось, позволяющую складывать числа X и Y по запросу с идентификатором /add/X/Y.

{-# LANGUAGE OverloadedStrings #-}
module Main where

import           Web.Spock(runSpock,spock,(<//>))
import qualified Web.Spock        as Spock
import           Web.Spock.Config as Spock


main = do
    cfg <- defaultSpockCfg () PCNoDatabase ()
    
    runSpock 3000          -- порт
             (server cfg)  -- маршрутизатор



server cfg = spock cfg $ do
    
    Spock.get Spock.root $ do
        Spock.text "Hello world"
        
    Spock.get (Spock.static "add" <//> Spock.var <//> Spock.var) $ \x y -> do
        Spock.text (Text.pack $ show (x+y :: Integer))

Здесь используются шаблоны static и var, а также – комбинатор шаблонов (<//>). Обработчик же принимает два аргумента, на которые подаются части идентификатора, соответствующие var-шаблонам. Стоит обратить внимание на то, что конкретный тип обработчика нетривиально зависит от типа первого аргумента конструктора маршрутизатора. Такое поведение достигается при помощи семейств типов – слабой форма т.н. зависимых типов.

Счётчик

А сейчас мы реализуем сервис, который хранит число и позволяет его увеличивать. Первым делом мы создадим простую HTML-страничку следующего вида

<!DOCTYPE html>

<title>Счётчик</title>

<meta charset="utf8">


<p><b>Текущее значение счётчика:</b> <span id="counter"></span></p>

<form method="POST">
<button autofocus>Увеличить счётчик</button>
</form>

Сохраните этот текст в файл counter.html. Теперь в файл counter.js сохраните текст

function main() {
  document.getElementById("counter").innerHTML = COUNTER;
}

window.onload = main;

Текст сервера будет немного сложнее, чем ранее:

{-# LANGUAGE OverloadedStrings #-}
module Main where

import Control.Monad.Trans
import Data.IORef
import Data.Monoid

import           Data.Text(Text)
import qualified Data.Text          as Text
import qualified Data.Text.IO       as Text

import           Web.Spock(runSpock,spock)
import qualified Web.Spock        as Spock
import           Web.Spock.Config as Spock


data State = State {
    counter :: IORef Integer
  , page    :: Text
  , script  :: Text
  }


main = do
    c <- newIORef 0
    p <- Text.readFile "counter.html"
    s <- Text.readFile "counter.js"
    
    cfg <- defaultSpockCfg () PCNoDatabase (State c p s)
    
    runSpock 3000 (server cfg)



server cfg = spock cfg $ do
    
    Spock.get Spock.root $ do
        state <- Spock.getState
        
        value <- liftIO $ readIORef (counter state)
        
        let contents = page state 
                    <> "<script>" 
                    <> script state 
                    <> "COUNTER=" <> (Text.pack $ show value) <> ";\n"
                    <> "</script>"
        
        Spock.html contents
        
    
    Spock.post Spock.root $ do
        state <- Spock.getState
        
        liftIO $ modifyIORef' (counter state) (\x -> x+1)
        
        Spock.redirect "/"

Здесь используется сразу несколько новых концепций. Во-первых – определение типа вида

data State = State {
    counter :: IORef Integer
  , page    :: Text
  , script  :: Text
  }

Оно эквивалентно следующему определению:

data State = State (IORef Integer) Text Text

counter (State x _ _) = x
page    (State _ x _) = x
script  (State _ _ x) = x

Но предоставляет дополнительные удобства, среди которых:

-- создание объектов:
State {
    page    = x
  , counter = y
  , script  = z
  }
-- то же самое, что и
State y x z


-- "модификация" объектов
s { page = x }
-- семантически (по модулю ленивости) то же самое, что и
State (counter s) x (script s)

В настоящее время «модификацию» принято производить при помощи т.н. линз (см. пакет lenses), тем не менее, для наших целей будет достаточно встроенных в язык средств.

Во-вторых, ячейки типа IORef x. Поскольку сервер является многопоточным приложением, общие для всех потоков участки памяти приходится выносить в ячейки тех или иных типов. IORef-ячейки являются наиболее примитивными. Они позволяют:

Два последних действия различаются только тем, в каком порядке выполняются чтение и запись в ячейку. atomic-версия гарантирует, что изменение не поменяется местами с близкостоящими операциями чтения. Не-atomic-версия таких гарантий не даёт.

В-третьих, последовательность post-redirect-get. Нажатие на кнопку формы, определённой в коде HTML-странички приводит к отправке POST-запроса на сервер. Сервер увеличивает счётчик и отправляет браузеру ответ с просьбой сделать перенаправление (новый GET-запрос) на ресурс /. Наконец, браузер делает предложенное ему перенаправление. Такая последовательность защищает от случайной повторной отправки POST-запроса, но может иметь ряд других проблем.

Обычно GET-запрос в последовательности post-redirect-get игнорирует часть механизмов кеширования, встроенных в браузер, поскольку содержимое ответа на этот запрос зависит от входных данных POST-запроса. Тем не менее, некоторые браузеры всё равно сохраняют результаты прошлого GET-запроса. В связи с этим можно к каждому GET-запросу приписывать какой-нибудь уникальный идентификатор. Например, так:

server cfg = spock cfg $ do
    
    Spock.get Spock.wildcard $ \_ -> do
        state <- Spock.getState
        
        value <- liftIO $ readIORef (counter state)
        
        let contents = page state 
                    <> "<script>" 
                    <> script state 
                    <> "COUNTER=" <> (Text.pack $ show value) <> ";\n"
                    <> "</script>"
        
        Spock.html contents
        
    
    Spock.post Spock.wildcard $ \_ -> do
        state <- Spock.getState
        
        value <- liftIO $ atomicModifyIORef' (counter state) (\x -> (x+1,x+1))
                
        Spock.redirect $ "/" <> Text.pack (show value)
        
-- если используется версия Spock 0.11, то комбинатор wildcard можно импортировать
-- из модуля Web.Routing.Combinators пакета reroute

Угадай число

В ПРОЦЕССЕ НАПИСАНИЯ

@ 2016 arbrk1, all rights reversed