Предыстория
Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
Введение в gen_server: «Erlybank»
Это вторая статья из серии «Введение в OTP». Рекомендую вам прочитать первую статью, в которой говорится о gen_server и закладываются основы нашей банковской системы перед тем, как читать эту. Если вы быстро схватываете, можете посмотреть завершенную версию сервера и двигаться дальше.
Сценарий: Мы поставили ErlyBank сервер заказчикам, и они остались очень довольны. Но на дворе 21 век, и они хотят также безопасный и простой в использовании банкомат, поэтому они попросили нас расширить наш сервер и создать программное обеспечение для банкомата. Пользовательские аккаунты должны быть защищены 4-цифровым ПИН-кодом. В банкомате можно залогиниться с помощью ранее созданного аккаунта, сделать депозит или снять деньги со счета. Делать красивый интерфейс не требуется, этим занимаются другие люди.
Цель: Сначала мы расширим сервер, добавив поддержку ПИН-кода для аккаунтов и авторизации через ПИН-код. Потом мы будем использовать gen_fsm чтобы создать бэкенд банкомата. Проверка данных будет проводиться на стороне сервера.
gen_fsm — это еще один Erlang/OTP интерфейсный модуль. Он используется для реализации конечного автомата.
Я заранее извиняюсь, так как в этой статье понятие «состояние»(state) будет использоваться для обозначения двух вещей:
Немного не ловко, конечно, но я постараюсь ссылаться на них только в контексте вышеобозначенных условий.
gen_fsm начинает работу в некотором состоянии. Любые call/cast вызовы к gen_fsm обрабатываются в специальных callback-методах, которые должны называться именем текущего состояния gen_fsm(конечного автомата). Основываясь на произведенном действии, модуль может менять состояние( [текущее состояние, входящий символ] -> новое состояние, прим.). Хрестоматийный пример конечного автомата — это закрытая дверь. В начале дверь находится в состоянии «закрыта». Необходимо ввести 4-значный код, чтобы открыть ее. После ввода 1 цифры дверь сохраняет ее, но одной цифры мало, поэтому она продолжает ждать в состоянии «закрыта». После ввода 4 цифр, если они правильные, дверь меняет состояние на «открыта» на некоторое время. Если цифры не правильные, она остается в состоянии «закрыта» и очищает память. Возможно, сейчас у вас уже появились догадки о том, как мы будем реализовывать конечный автомат с помощью gen_fsm:)
Так же, как и в случае с gen_server, представляю список callback-методов, которые должны быть реализованы в gen_fsm. Вы найдете много общего с gen_server:
Так же, как и с gen_server, я начинаю создавать конечный автомат с некоторого общего скелета. Скелет для gen_fsm можете найти здесь.
Там нет ничего экстраординарного. start_link похож на тот, что мы создали для gen_server. :) Сохраните скелет, как eb_atm.erl. И вот, мы готовы начать!
Это еще одно задание, которое, я оставляю вам. Изменения, которые нам необходимы:
Также, было бы замечательно требовать ПИН-код при каждой операции депозит/снятие, но, чтобы сэкономить время, а также из-за того, что банк у нас фейковый(мое сердце разбито:( ха!), мы не будем этого делать.
По-честному, это не так-то просто сделать, но если вы учите Erlang сами, вы должны быть достаточно смышленными ;) Так что я думаю, вы можете сделать это! Протестируйте свои изменения перед тем, как продолжать или, по крайней мере, сравните их с ответом внизу.
После внесения изменений ваш eb_server.erl должен выглядеть примерно так. Обратите внимание, что сообщения, которые вы посылаете серверу могут быть разными, и это нормально. Мышление у всех разное. Очень важно, чтобы API выводил те же данные, правильно. (The important thing is that the API outputs the same data, correctly, англ.)
Я хочу взять маленькую «без кода» паузу, чтобы рассказать план работы конечного автомата ATM. Мы собираемся его выполнить в соответствии с диаграммой ниже:
Три голубых блока представляют разные состояния сервера. Стрелки обозначают, какие действия необходимы, чтобы перейти из одного состояния в другое.
Чтобы Запустить ATM, мы используем такой же метод start_link, как и в gen_server. Но инициализация немного отличается.
Метод init/1 модуля gen_fsm должен возвращать
Теперь нам нужно реализовать API авторизации для ATM. Сначала, определение API:
Метод sync_send_event эквивалентен методу call модуля gen_server. Он посылает сообщение(вторым аргументом) текущему состоянию сервера(первый аргумент). Поэтому теперь нам надо написать обработчик этого сообщения:
Функция называется unauthorized потому, что она должна получать сообщение, когда сервер находится в состоянии unauthorized. Делаем сопоставление с образцом, чтобы обработать тапл
Если имя пользователя и ПИН-код правильные, мы посылаем
Если информация по аккаунту была не верной, мы посылаем в ответ error и причину ошибки, состояние и состояние(данные) при этом не меняются.
В конце мы реализуем еще одну «catch-all» функцию. Вам следует делать так всегда, но здесь это особенно важно, так как состояния могут получать сообщения, адресованные другим состояниям. Например: что будет, если по какой-то причине кто-либо попытается сделать депозит в неаторизованном состоянии? Нам нужен «catch-all» метод, чтобы послать обратно сообщение об ошибке.
Как только мы перешли в авторизованное состояние, пользователь собирается сделать депозит или снять деньги со своего банковского аккаунта. Мы реализуем депозит, используя асинхронный вызов к серверу. Опять же, это не очень безопасно: мы вообще не проверяем, был ли депозит успешным, но так как наш банк — это фейк, я на это забью. ;)
Итак, в начале, API!
Все просто, в этот раз мы используем метод
Опять, все очень просто. Этот метод просто перенаправляет информацию в метод deposit модуля eb_server, который также проводит всю проверку. Но есть что-то необычное в возвращаемом значении метода deposit! Не только состояние меняется на thank_you, но еще и это число «5000» там в конце. Это просто таймаут. Если не будет получено никакого сообщения в течение 5000 милисекунд(5 секунд), то текущему состоянию пошлется
Которе ведет нас к следующей теме…
Многие(или все), кто пользовался банкоматами, знают, что есть такой маленький «Thank You!» экран, который показывается в течение небольшого времени. Вообще-то мы могли спокойно обойтись и без этого экрана в нашей реализации — я просто хотел показать вам фичу с таймаутом в gen_fsm. После 5000 милисекунд, или если не будет получено никакого сообщения, я меняю состояние на обратно на «unauthorized», и таким образом, ATM может начать работу заново со следующим пользователем. Вот код:
Примечание: Тренированный глаз заметит, что оба метода эквивалентны, и нет необходимости для первого образца. Это правда, я просто включил первый образец, чтобы быть уверенным, что поймаю таумаут.
А здесь законченная на данный момент версия eb_atm.erl.
Снова я оставлю разработку методов для снятия денег как упражнение читателю. Вы можете реализовать эту задачку, как хотите! Просто удостоверьтесь, что ваши реально снимают деньги ;)
Вот моя версия eb_atm.erl после реализации механизмов снятия денег со счета. Обратите внимание, что успешная операция перевадит автомат в состояние thank_you с таймаутом.
Одна из самых больших проблем компьютеров — это отсутствие кнопки «Отменить», которая прерывала бы все, что вы делаете. И хотя я знаю, что кнопка power off на компьютере справляется с этой задачей на ура, пользователи банкоматов Erlybank лишены такой возможности. Поэтому давайте реализуем cancel метод, который отменял бы все транзакции, вне зависимости от того, в каком вы состоянии находитесь.
Как бы вы реализовали это? В общем, я бы предположил, что вы, основываясь на информации из этой статьи, сделали бы cancel метод, посылающий сообщение
Остроумно, но не правильно, но в этом нет вашей вины! Я не указал (или слишком кратко, вы возможно, пропустили это), что есть метод
Наш API:
Это сообщение посылается
Если мы получаем cancel сообщение, сервер переводит состояние в unauthorized и сосяние(данные) в nobody: свеженький ATM!
Как всегда, текущую версию eb_atm.erl можно посмотреть тут.
В этой статье я показал, как создать простую ATM систему, построенную на конечном автомате, используя gen_fsm. Я показал, как обрабатывать сообщения в разных состояниях, менять состояние, менять состояние по таймауту, и «send-to-all» сообщения.
Однако, есть еще немного «бородавок» в нашей системе, и я оставлю вам возможность исправить их. Я подготовил 2 задания для вас, если хотите. Поверьте мне, вы можете их выполнить:
Эти две фичи из упражнений не будут использоваться в будущем, и раз так, я не буду постить ответы здесь. Вы можете проверить себя, заставив их работать! :)
Вторая часть этих статей закончена. Третья статья уже почти готова и будет опубликована в ближайшие дни. Она будет раскрывать тему gen_event. Чтобы поразвлечься, вы можете подумать о том, что я добавлю в Erlybank с помощью gen_event! :D
Надеюсь, вам понравились эти вводные статьи в Erlang/OTP так же, как и мне понравилось их писать. Спасибо всем за поддержку и удачи!
Введение в Open Telecom Platform/Открытую Телекомуникационную Платформу(OTP/ОТП)
Введение в gen_server: «Erlybank»
Это вторая статья из серии «Введение в OTP». Рекомендую вам прочитать первую статью, в которой говорится о gen_server и закладываются основы нашей банковской системы перед тем, как читать эту. Если вы быстро схватываете, можете посмотреть завершенную версию сервера и двигаться дальше.
Сценарий: Мы поставили ErlyBank сервер заказчикам, и они остались очень довольны. Но на дворе 21 век, и они хотят также безопасный и простой в использовании банкомат, поэтому они попросили нас расширить наш сервер и создать программное обеспечение для банкомата. Пользовательские аккаунты должны быть защищены 4-цифровым ПИН-кодом. В банкомате можно залогиниться с помощью ранее созданного аккаунта, сделать депозит или снять деньги со счета. Делать красивый интерфейс не требуется, этим занимаются другие люди.
Цель: Сначала мы расширим сервер, добавив поддержку ПИН-кода для аккаунтов и авторизации через ПИН-код. Потом мы будем использовать gen_fsm чтобы создать бэкенд банкомата. Проверка данных будет проводиться на стороне сервера.
Что такое gen_fsm?
gen_fsm — это еще один Erlang/OTP интерфейсный модуль. Он используется для реализации конечного автомата.
Я заранее извиняюсь, так как в этой статье понятие «состояние»(state) будет использоваться для обозначения двух вещей:
- состояние gen_fsm — Состояние конечного автомата, текущий «режим» его работы. Оно не имеет ничего общего с состоянием(данными) из gen_server.
- состояние(данные) — Данные состояния сервера — то, о чем вы узнали из предыдущей статьи про gen_server.
Немного не ловко, конечно, но я постараюсь ссылаться на них только в контексте вышеобозначенных условий.
gen_fsm начинает работу в некотором состоянии. Любые call/cast вызовы к gen_fsm обрабатываются в специальных callback-методах, которые должны называться именем текущего состояния gen_fsm(конечного автомата). Основываясь на произведенном действии, модуль может менять состояние( [текущее состояние, входящий символ] -> новое состояние, прим.). Хрестоматийный пример конечного автомата — это закрытая дверь. В начале дверь находится в состоянии «закрыта». Необходимо ввести 4-значный код, чтобы открыть ее. После ввода 1 цифры дверь сохраняет ее, но одной цифры мало, поэтому она продолжает ждать в состоянии «закрыта». После ввода 4 цифр, если они правильные, дверь меняет состояние на «открыта» на некоторое время. Если цифры не правильные, она остается в состоянии «закрыта» и очищает память. Возможно, сейчас у вас уже появились догадки о том, как мы будем реализовывать конечный автомат с помощью gen_fsm:)
Так же, как и в случае с gen_server, представляю список callback-методов, которые должны быть реализованы в gen_fsm. Вы найдете много общего с gen_server:
- init/1 — Инициализирует сервер конечного автомата. Практически идентичен gen_server.
- StateName/2 — StateName будет заменено на название состояния. Этот метод вызывается, когда конечный автомат находится в этом состоянии и получает сообщение. В результате, выполняется определенное действие. Это асинхронный callback-метод.
- handle_event/3 — То же, что и StateName/2, за исключением того, что этот метод срабатывает, когда клиент вызывает gen_fsm:send_all_state_event, независимо от текущего состояния автомата. Опять, же асинхронный.
- StateName/3 — Синхронная версия StateName/2. Клиент дожидается ответа сервера.
- handle_sync_event/4 — Синхронная версия handle_event/3.
- handle_info/3 — Эквивалентно gen_server handle_info. Этотм метод получает все сообщения, которые были посланы нестандартными средствами gen_fsm. Это могут быть таймаут-сообщения, process exit сообщения или любые другие сообщения, посланные серверному процессу с помощью "!".
- terminate/3 — Вызывается, когда сервер завершает работу, в нем вы можете освободить занятые ресурсы.
- code_change/4 — Вызывается, когда сервер обновляется в реальном времени. Мы не используем его сейчас, но он будет использоваться в будущих статьях.
gen_fsm скелет
Так же, как и с gen_server, я начинаю создавать конечный автомат с некоторого общего скелета. Скелет для gen_fsm можете найти здесь.
Там нет ничего экстраординарного. start_link похож на тот, что мы создали для gen_server. :) Сохраните скелет, как eb_atm.erl. И вот, мы готовы начать!
Расширение eb_server для создания механизма авторизации аккаунта.
Это еще одно задание, которое, я оставляю вам. Изменения, которые нам необходимы:
- Теперь при создании аккаунта необходимо требовать ПИН-код, который будет храниться вместе с аккаунтом, без шифрования.
- Добавить метод authorize/2 с аргументами Name и PIN. Возвращаемые значения должны быть
ok
или{error, Reason}
.
Также, было бы замечательно требовать ПИН-код при каждой операции депозит/снятие, но, чтобы сэкономить время, а также из-за того, что банк у нас фейковый(мое сердце разбито:( ха!), мы не будем этого делать.
По-честному, это не так-то просто сделать, но если вы учите Erlang сами, вы должны быть достаточно смышленными ;) Так что я думаю, вы можете сделать это! Протестируйте свои изменения перед тем, как продолжать или, по крайней мере, сравните их с ответом внизу.
После внесения изменений ваш eb_server.erl должен выглядеть примерно так. Обратите внимание, что сообщения, которые вы посылаете серверу могут быть разными, и это нормально. Мышление у всех разное. Очень важно, чтобы API выводил те же данные, правильно. (The important thing is that the API outputs the same data, correctly, англ.)
Стратегия проектирования ATM (ATM Design Strategy)
Я хочу взять маленькую «без кода» паузу, чтобы рассказать план работы конечного автомата ATM. Мы собираемся его выполнить в соответствии с диаграммой ниже:
Три голубых блока представляют разные состояния сервера. Стрелки обозначают, какие действия необходимы, чтобы перейти из одного состояния в другое.
Инициализация gen_fsm
Чтобы Запустить ATM, мы используем такой же метод start_link, как и в gen_server. Но инициализация немного отличается.
init([]) -> {ok, unauthorized, nobody}.
Метод init/1 модуля gen_fsm должен возвращать
{ok, StateName, StateData}
. StateName — это начальное состояние сервера, а StateData — это начальное состояние(данные) сервера. В нашем случае мы запускаемся в состоянии unauthorized и данные выставляются в nobody
. Состоянием(данными) будет имя аккаунта, с которым мы работаем, так что сначала там ничего нет. В Erlang нет типа данных null/nil/nothing, вместо которых обычно используется говорящий атом, как у нас nobody, например.Авторизация аккаунта
Теперь нам нужно реализовать API авторизации для ATM. Сначала, определение API:
authorize(Name, PIN) -> gen_fsm:sync_send_event(?SERVER, {authorize, Name, PIN}).
Метод sync_send_event эквивалентен методу call модуля gen_server. Он посылает сообщение(вторым аргументом) текущему состоянию сервера(первый аргумент). Поэтому теперь нам надо написать обработчик этого сообщения:
unauthorized({authorize, Name, Pin}, _From, State) -> case eb_server:authorize(Name, Pin) of ok -> {reply, ok, authorized, Name}; {error, Reason} -> {reply, {error, Reason}, unauthorized, State} end; unauthorized(_Event, _From, State) -> Reply = {error, invalid_message}, {reply, Reply, unauthorized, State}.
Функция называется unauthorized потому, что она должна получать сообщение, когда сервер находится в состоянии unauthorized. Делаем сопоставление с образцом, чтобы обработать тапл
{authorize, Name, Pin}
и используем API методы, экспортированные eb_server сервером, чтобы авторизировать пользователя.Если имя пользователя и ПИН-код правильные, мы посылаем
ok
клиенту. Формат ответа: {reply, Response, NewStateName, NewStateData}
. В соответствии с форматом, мы изменяем состояние в authorized и сохраняем имя аккаунта в состоянии(данных).Если информация по аккаунту была не верной, мы посылаем в ответ error и причину ошибки, состояние и состояние(данные) при этом не меняются.
В конце мы реализуем еще одну «catch-all» функцию. Вам следует делать так всегда, но здесь это особенно важно, так как состояния могут получать сообщения, адресованные другим состояниям. Например: что будет, если по какой-то причине кто-либо попытается сделать депозит в неаторизованном состоянии? Нам нужен «catch-all» метод, чтобы послать обратно сообщение об ошибке.
Депозит
Как только мы перешли в авторизованное состояние, пользователь собирается сделать депозит или снять деньги со своего банковского аккаунта. Мы реализуем депозит, используя асинхронный вызов к серверу. Опять же, это не очень безопасно: мы вообще не проверяем, был ли депозит успешным, но так как наш банк — это фейк, я на это забью. ;)
Итак, в начале, API!
%%-------------------------------------------------------------------- %% Function: deposit(Amount) -> ok %% Description: Deposits a certain amount in the currently authorized %% account. %%-------------------------------------------------------------------- deposit(Amount) -> gen_fsm:send_event(?SERVER, {deposit, Amount}).
Все просто, в этот раз мы используем метод
send_event/2
вместо sync_send_event. Он посылает асинхронный вызов серверу. А теперь, обработчик…authorized({deposit, Amount}, State) -> eb_server:deposit(State, Amount), {next_state, thank_you, State, 5000}; authorized(_Event, State) -> {next_state, authorized, State}.
Опять, все очень просто. Этот метод просто перенаправляет информацию в метод deposit модуля eb_server, который также проводит всю проверку. Но есть что-то необычное в возвращаемом значении метода deposit! Не только состояние меняется на thank_you, но еще и это число «5000» там в конце. Это просто таймаут. Если не будет получено никакого сообщения в течение 5000 милисекунд(5 секунд), то текущему состоянию пошлется
таймаут-сообщение
.Которе ведет нас к следующей теме…
Короткое «Thank You!» состояние
Многие(или все), кто пользовался банкоматами, знают, что есть такой маленький «Thank You!» экран, который показывается в течение небольшого времени. Вообще-то мы могли спокойно обойтись и без этого экрана в нашей реализации — я просто хотел показать вам фичу с таймаутом в gen_fsm. После 5000 милисекунд, или если не будет получено никакого сообщения, я меняю состояние на обратно на «unauthorized», и таким образом, ATM может начать работу заново со следующим пользователем. Вот код:
thank_you(timeout, _State) -> {next_state, unauthorized, nobody}; thank_you(_Event, _State) -> {next_state, unauthorized, nobody}.
Примечание: Тренированный глаз заметит, что оба метода эквивалентны, и нет необходимости для первого образца. Это правда, я просто включил первый образец, чтобы быть уверенным, что поймаю таумаут.
А здесь законченная на данный момент версия eb_atm.erl.
Изъятие денег со счета
Снова я оставлю разработку методов для снятия денег как упражнение читателю. Вы можете реализовать эту задачку, как хотите! Просто удостоверьтесь, что ваши реально снимают деньги ;)
Вот моя версия eb_atm.erl после реализации механизмов снятия денег со счета. Обратите внимание, что успешная операция перевадит автомат в состояние thank_you с таймаутом.
«Отменить-по-фигу-че»(«Cancel-No-Matter-What») Кнопка
Одна из самых больших проблем компьютеров — это отсутствие кнопки «Отменить», которая прерывала бы все, что вы делаете. И хотя я знаю, что кнопка power off на компьютере справляется с этой задачей на ура, пользователи банкоматов Erlybank лишены такой возможности. Поэтому давайте реализуем cancel метод, который отменял бы все транзакции, вне зависимости от того, в каком вы состоянии находитесь.
Как бы вы реализовали это? В общем, я бы предположил, что вы, основываясь на информации из этой статьи, сделали бы cancel метод, посылающий сообщение
cancel
. Затем, в каждом состоянии вы бы обработали его и вышли бы обратно в unauthorized
состояние.Остроумно, но не правильно, но в этом нет вашей вины! Я не указал (или слишком кратко, вы возможно, пропустили это), что есть метод
gen_fsm:send_all_state_event/2
, который послылает сообщение серверу вне зависимости от того, в каком состоянии сервер. Мы используем его, чтобы наш код был чист.Наш API:
%%-------------------------------------------------------------------- %% Function: cancel/0 %% Description: Cancels the ATM transaction no matter what state. %%-------------------------------------------------------------------- cancel() -> gen_fsm:send_all_state_event(?SERVER, cancel).
Это сообщение посылается
handle_event/3
, который мы расширяем ниже:handle_event(cancel, _StateName, _State) -> {next_state, unauthorized, nobody}; handle_event(_Event, StateName, State) -> {next_state, StateName, State}.
Если мы получаем cancel сообщение, сервер переводит состояние в unauthorized и сосяние(данные) в nobody: свеженький ATM!
Как всегда, текущую версию eb_atm.erl можно посмотреть тут.
Заключительные примечания
В этой статье я показал, как создать простую ATM систему, построенную на конечном автомате, используя gen_fsm. Я показал, как обрабатывать сообщения в разных состояниях, менять состояние, менять состояние по таймауту, и «send-to-all» сообщения.
Однако, есть еще немного «бородавок» в нашей системе, и я оставлю вам возможность исправить их. Я подготовил 2 задания для вас, если хотите. Поверьте мне, вы можете их выполнить:
- Добавить проверку ошибок в операции с депозитом. Заставьте их возвращать
{error, Reason}
и{ok, Balance}
вместо просто «ok» все время. - Добавьте функцию проверки баланса в ATM. Она должна быть доступна только в состоянии authorized и не должна завершать транзакцию. Это означает, что она не должна переводить состояние в thank_you. Это так потому, что обычно люди, помотрев свой баланс, хотят снять или положить денег себе на счет.
Эти две фичи из упражнений не будут использоваться в будущем, и раз так, я не буду постить ответы здесь. Вы можете проверить себя, заставив их работать! :)
Вторая часть этих статей закончена. Третья статья уже почти готова и будет опубликована в ближайшие дни. Она будет раскрывать тему gen_event. Чтобы поразвлечься, вы можете подумать о том, что я добавлю в Erlybank с помощью gen_event! :D
Надеюсь, вам понравились эти вводные статьи в Erlang/OTP так же, как и мне понравилось их писать. Спасибо всем за поддержку и удачи!