RPC на основе WAI

Введение

Сейчас web-разработка на Haskell достаточно проста, даже для новичка. Этому способствует наличие таких пакетов, как Yesod и Snap. Но не всегда их мощь и полнота охвата необходимы. Порой от “сервера” требуется столь мало, что не хочется иметь в зависимостях подобных “монстров”, особенно в тех случаях, когда задача достаточно легко решаема и более простыми средствами.

Пусть примером послужит такая задача: требуется реализовать сервис, позволяющий вызывать на сервере некие функции и получать результат вызова, или, говоря общепринятым языком, выполнять RPC.

Такую задачу можно решить, используя Spock, scotty или, скажем, servant, но “мы пойдем другим путем”©!

Большинство библиотек для web-разработки внутри использует так называемый Web Application Interface (WAI) - обобщенный протокол общения web-сервера и web-приложения. Приложения, реализующие этот протокол, называют WAI-приложениями и запускают с помощью сервера wai-приложений - warp.

Реализуем же и мы простой сервис на чистом WAI!

WAI-сервис

Задача

Реализовать сервис вызова функций типа String -> String. Для примера реализуем функции reverse, upper и lower.

API будет следующим:

  • GET на / возвращает список имен доступных функций (по одному на строку),
  • GET на /<function_name> возвращает описание функции,
  • GET на /<function_name>?<argument> возвращает результат вызова функции.

“Hello, World!”

Для начала создадим проект и реализуем сервер-заглушку, отвечающий известной строкой на любой запрос. Обратите внимание: исходники проекта доступны на github.

Создаем проект:

$ stack new wai-rpc simple --resolver lts-3.2

ВНИМАНИЕ: предполагается, что у вас установлена утилита stack, а ключ --resolver lts-3.2 означает, что будет использоваться снимок версии 3.2 - именно этот снимок был актуален на момент написания статьи. (подробнее о снимках можно почитать в документации к stack).

После создания проекта добавляем зависимости http-types, wai и warp в .cabal-файл:

-- ...часть файла опущена...
executable wai-rpc
  hs-source-dirs:      src
  main-is:             Main.hs
  default-language:    Haskell2010
  build-depends:       base >= 4.7 && < 5,
                       http-types, wai, warp -- <-- добавлено

Затем содержимое файла src/Main.hs заменяем на:

К слову, этот код практически слово-в-слово повторяет helloworld от авторов библиотеки WAI ;)

Осталось собрать проект:

$ stack build

И запустить:

$ stack exec wai-rpc
Serving (hit Ctrl+C to stop)...

Если при запущенном сервере открыть в браузере url http://localhost:8000, то в окне отобразится ожидаемое приветствие. Сервер работает!

Теперь стоит разобрать, из чего же состоит наш сервер.

main содержит строку

run 8000 application

Это запуск сервера warp на порту 8000 с единственным WAI-приложением - application.

Приложение application имеет тип Application, который является синонимом для

type Application = Request
                   -> (Response -> IO ResponseReceived)
                   -> IO ResponseReceived

Здесь первый аргумент, это тип Request, описывающий запрос, а второй, это “ответчик” - функция, призванная возвращать ответ Response в процессе выполнения некой работы (для этого в типе монада IO).

В данном случае приложение сразу же отвечает фиксированным сообщением, поэтому тело приложения - единственный вызов ответчика respond.

Ответ же в данном случае выглядит так:

Всё достаточно привычно: статус, заголовки и тело.

Вот, собственно и всё! Это уже вполне самостоятельный сервер, можно пускать в production :) И это не шутка - warp испытан и проверен, и, ко всему прочему, весьма быстр и пригоден для “вывешивания наружу” (т.е. не требует заворачивания во всякие Nginx).

Маршрутизация

Сервер работает, настало время решать конкретную задачу. Для начала заведем несколько вспомогательных функций для работы с ответами:

(в зависимости проекта нужно будет добавить bytestring)

Так как мы собираемся обрабатывать только GET-запросы, добавим отсечку по типу запроса с соответствующим сообщением об ошибке:

GET-запросы мы уже фильтруем, теперь на запрос к корневому url нужно возвращать список функций. Значит нужна библиотека функций:

(в зависимости проекта нужно будет добавить containers)

Самих функций пока нет, но библиотека есть, как есть и функции для работы с ней. Можно уже выводить список функций, но перед этим нужно понять, что запрос производится на “корневой” url и не содержит параметров. Добавим ветвления в наше приложение, заодно переписав if-ветки в виде охранных выражений:

Теперь у нашего сервера есть маршрутизация, пусть и в зачаточном виде :)

Проверим работу того, что уже наработано, с помощью curl (предполагается, что сервер запущен в другом окне терминала):

$ curl http://localhost:8000
Available functions:
$ curl http://localhost:8000?asdf
No query parameters needed!

Получение описание и вызов функций

Теперь корневой url обрабатывается. Настало время поиска функции в библиотеке:

Функций пока нет, но поиск уже работает. Проверим:

$ curl http://localhost:8000/func
Unknown function!

Добавим же наконец пару функций в библиотеку:

И, разумеется, проверим:

$ curl http://localhost:8000
Available functions:
reverse
upper
$ curl http://localhost:8000/reverse
returns string with characters in reverset order
$ curl http://localhost:8000/reverse?Hello+World
dlroW olleH

Готово! Есть функции, и их можно вызывать удалённо!

Финальные штрихи

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

Когда при работе с WAI-приложениями возникает необходимость сделать что-то с запросами и/или ответами на них, на сцену выходит тип Middleware:

Middleware - это преобразователь приложений, настоящая функция высшего порядка! Как же такие преобразователи пишутся? Довольно просто:

Ничего сверх-естественного, оборачивание вызова функции, как оно есть.

Выглядит вывод logger’а примерно так:

$ stack exec wai-rpc
Serving (hit Ctrl+C to stop)...
200: /reverse?Hello%20World
200: /
404: /asdf
400: /?asdf
...

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

Заключение

Даже такой простой пример позволяет понять, что разработка сервисов на “голом” WAI не только довольно проста, но и вполне удобна и приятна :)