В этой главе будет продемонстрировано устройство простого веб-сервера на основе библиотеки Spock. В качестве демонстрации мы напишем игру «угадай число».
Веб-сервером называют приложение, обрабатывающее HTTP-запросы. Аббревиатурой HTTP обозначается протокол (формальный язык и договорённость о способах его применения), лежащий в основе Всемирной Паутины, но по факту вышедший далеко за её пределы.
Большинство пользователей вычислительной техники сталкиваются с веб-браузерами – клиентами веб-серверов. Получая запрос от пользователя вида
http://foobar.baz/foo/bar/baz
браузер обычно делает как минимум следующие две вещи:
Отправляет на DNS-сервер запрос о том, что вообще такое foobar.baz
. Обычно под такими именами скрываются идентификаторы различных устройств, подключённых к интернету. Эти идентификаторы называются IP-адресами и представляют собой либо 32-битные числа (IP версии 4), либо 128-битные числа (IP версии 6). DNS – служба, которая ставит в соответствие текстам (называемым доменными именами) какие-то данные (обычно – адреса устройств).
Отправляет по найденному 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
методами:
Входными данными для метода GET
являются идентификатор ресурса и т.н. Cookie. Результат GET
-запроса может быть сохранён браузером, поэтому, кроме некоторых специальных случаев, на один и тот же GET
запрос сервер должен давать один и тот же ответ.
Входными данными для метода POST
являются идентификатор ресурса, Cookie, а также – тело запроса. Тело запроса может содержать любые данные в любом объёме. Ответ на POST
-запрос никогда не сохраняется браузером.
При помощи 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
-ячейки являются наиболее примитивными. Они позволяют:
readIORef
writeIORef
modifyIORef'
и atomicModifyIORef'
(нештрихованные версии этих функций осуществляют ленивое изменение, надевая функцию на текущее значение, но не вычисляя результат)Два последних действия различаются только тем, в каком порядке выполняются чтение и запись в ячейку. 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