Архитектура REpresentational State Transfer (REST) предоставляет очень удобный механизм для передачи данных между клиентами и серверами. Веб-сервисы и протоколы, такие как HTTP, уже много лет используют архитектуру REST, поэтому это проверенная и зрелая концепция.
Синатра может быть использована для эффективной реализации архитектуры REST. DSL (язык, специфичный для предметной области) Синатры, похоже, настроен на дух REST. На самом деле язык маршрутизации Синатры включает в себя те же самые глаголы, используемые HTTP, как GET
, POST
, PUT
и DELETE
. Поскольку эти транзакции REST API выглядят и ведут себя очень похоже на HTTP, сетевые устройства будут обрабатывать ваши REST-транзакции практически одинаково.
В этой статье мы рассмотрим реализацию REST на стороне сервера с использованием Sinatra. Мы также рассмотрим простое клиентское REST-приложение для проверки реализации сервера Sinatra. Мы продемонстрируем использование простой карточной игры, где одна колода карт будет храниться на сервере и раздана четырем клиентам. В конце мы увидим, как просто создавать приложения, работающие на клиентах и серверах, используя REST для перемещения данных вперед и назад.
Быстрый вопрос, прежде чем мы начнем: почему бы не использовать Ruby on Rails для перемещения данных? Ну, вы, безусловно, могли бы использовать Rails (или любую другую веб-среду), но учтите, что Rails не только извлекает данные, но и отображает представление. Этот процесс рендеринга создает дополнительную нагрузку на сервер, особенно когда данные очень динамичны и не могут быть кэшированы. Например, когда речь идет об онлайн-играх, данные постоянно меняются. Поэтому лучше позволить клиентам отображать свое собственное представление, чем заставлять сервер перерисовывать представление каждый раз, когда игрок делает ход.
Настройка Синатры
Перед установкой Sinatra помогает понять, как он работает и что требуется для его работы.
Как уже упоминалось выше, Синатра является DSL. По сути, он оборачивает другую программу, называемую «Rack», и делает ее более привлекательной для программистов на Ruby. Например, Sinatra позволяет вам написать следующий «маршрут», который фиксирует HTTP- GET
клиента GET
для страницы, расположенной по адресу http://www.example.com/getmypage.html:
get '/getmypage.html' do 'Hello World!' end
С помощью приведенного выше кода, работающего на сервере под Sinatra, браузер, направленный на URL, получит простой ответ «Hello World». (Обратите внимание, что для простоты я не заключил ответ в теги HTML.)
Rack предоставляет стандартный интерфейс между веб-платформами Ruby (такими как Sinatra и Rails) и реальным веб-сервером (таким как WEBrick или Thin).
В этом примере мы будем использовать Sinatra + Rack + Thin в качестве основы для нашего сервера.
Чтобы настроить эту среду, убедитесь, что вы установили гемы для Sinatra, Rack и Thin. Как правило, когда вы устанавливаете Sinatra, он устанавливает Rack. Тонкий, не будет автоматически установлен, поэтому вам нужно будет сделать это вручную.
$ gem install sinatra $ gem install thin
После того, как Sinatra установлен, вам просто нужно require
его в исходных файлах. Он будет автоматически использовать Тонкий веб-сервер.
Вы можете запустить этот очень простой файл myapp.rb
, как показано ниже, и протестировать его локально. (Этот тестовый файл был взят прямо со страницы GitHub Sinatra.)
# myapp.rb require 'sinatra' get '/' do 'Hello World' end
Запустите файл с этой командной строкой:
ruby myapp.rb
Затем вы можете просмотреть результаты в локальном веб-браузере по адресу http: // localhost: 4567.
Обратите внимание, что сокет TCP автоматически назначается порту 4567. Вы можете изменить его на стандартный порт 80 при развертывании приложения.
Также обратите внимание, что в качестве DSL Sinatra позволяет вам реализовывать команды PUT
, POST
и DELETE
а также глагол GET
. Например, мы можем обработать POST
запрос клиента следующим образом:
post '/join' do # process the POST command here ... end
Приведенный выше код может быть помещен в файл myapp.rb
, чуть ниже предыдущего маршрута get
. Подпрограмма Sinatra просто просматривает файл, от начала до конца, ищет глаголы и обрабатывает их соответствующим образом.
Посетите веб-сайт Sinatra для получения отличной документации, а также советов по началу работы с этим легким и эффективным веб-DSL.
Карточная игра
Мы реализуем начало развлекательной карточной игры под названием Uno, в которую играют все карты колоды, включая двух джокеров. Основная цель этой игры — проиллюстрировать REST API Sinatra в действии, поэтому мы не будем полностью развивать игровую логику.
Мы создаем колоду карт в виде единого массива строк, где каждая строка однозначно описывает карту. Например, «4-бриллиант» представляет карту из четырех бриллиантов.
Мы создадим три функции игры в карты для сервера:
-
join_game
— разрешить игроку присоединиться к игре - раздача — перетасуйте и раздайте карты каждому игроку
-
get_cards
— подаритьget_cards
карты игроку
Мы ограничим игру четырьмя игроками, и сервер будет запускать только одну игру за раз.
Чтобы реализовать всю эту логику, мы разработаем два файла:
-
uno-server.rb
— Содержит логику игрока и код сервера REST API Sinatra -
uno-client.rb
— содержит логику проигрывателя клиента
Ядро карточной игры работает в классе UnoServer
, как показано ниже. Обратите внимание, что этот класс полностью независим от сервера, хотя код сервера будет помещен в тот же файл.
# Ruby Uno Server require 'sinatra' require 'json' class UnoServer attr_reader :deck, :pool, :hands, :number_of_hands MAX_HANDS = 4 def initialize @hands = Array.new @number_of_hands = 0 @pool = Array.new @deck = %w(2-diamond 3-diamond 4-diamond 5-diamond 6-diamond 7-diamond 8-diamond 9-diamond 10-diamond) @deck.concat %w(2-heart 3-heart 4-heart 5-heart 6-heart 7-heart 8-heart 9-heart 10-heart) @deck.concat %w(2-club 3-club 4-club 5-club 6-club 7-club 8-club 9-club 10-club) @deck.concat %w(2-spade 3-spade 4-spade 5-spade 6-spade 7-spade 8-spade 9-spade 10-spade) @deck.concat %w(jack-diamond jack-heart jack-club jack-spade) @deck.concat %w(queen-diamond queen-heart queen-club queen-spade) @deck.concat %w(king-diamond king-heart king-club king-spade) @deck.concat %w(ace-diamond ace-heart ace-club ace-spade) @deck.concat %w(joker joker) end def join_game player_name return false unless @hands.size < MAX_HANDS player = { name: player_name, cards: [] } @hands.push player true end def deal return false unless @hands.size > 0 @pool = @deck.shuffle @hands.each {|player| player[:cards] = @pool.pop(5)} true end def get_cards player_name cards = 0 @hands.each do |player| if player[:name] == player_name cards = player[:cards].dup break end end cards end end
Обратите внимание на звонок в библиотеку Синатра в строке 2.
Колода карт строится в строках с 12 по 20 как один большой массив. Перемешанные версии этой колоды будут помещены в массив pool
, из которого будут сданы карты.
Игроки вступают в игру, используя метод join_game
в строке 23. Игроки join_game
имя, а количество игроков ограничено четырьмя ( MAX_HANDS
).
Карты раздаются с использованием метода deal
в строке 33. По сути, колода перемешивается, и результаты помещаются в пул. Затем каждому игроку предоставляется пять карт из пула.
Наконец, игроки могут взглянуть на свои карты, используя метод get_cards
в строке 40. Им просто нужно идентифицировать себя, и им будет предоставлена копия их карт.
Вторая половина этого файла содержит захватывающую часть: реализацию REST API Sinatra.
uno = UnoServer.new ###### Sinatra Part ###### set :port, 8080 set :environment, :production get '/cards' do return_message = {} if params.has_key?('name') cards = uno.get_cards(params['name']) if cards.class == Array return_message[:status] == 'success' return_message[:cards] = cards else return_message[:status] = 'sorry - it appears you are not part of the game' return_message[:cards] = [] end end return_message.to_json end post '/join' do return_message = {} jdata = JSON.parse(params[:data],:symbolize_names => true) if jdata.has_key?(:name) && uno.join_game(jdata[:name]) return_message[:status] = 'welcome' else return_message[:status] = 'sorry - game not accepting new players' end return_message.to_json end post '/deal' do return_message = {} if uno.deal return_message[:status] = 'success' else return_message[:status] = 'fail' end return_message.to_json end
Обратите внимание, что в строке 53 мы создаем единственную игру Uno как объект uno
. API REST Sinatra в последующих строках будет использовать этот объект.
Строки 57 и 58 позволяют нам определить среду этой реализации. Мы хотим использовать TCP-порт 8080, и мы настроили производственную среду. Производственная среда оптимизируется по скорости за счет предоставления меньше отладочной информации.
API REST обращается к трем основным функциям, установленным в карточной игре: добавление игроков ( join
), раздача карт (раздача) и просмотр карт ( cards
). Каждому был предоставлен свой собственный URL.
Обратите внимание на использование глаголов GET
и POST
в этой реализации. Когда пользователи хотят видеть свои карты, они выдают команду GET
используя URL-адрес http: // localhost: 8080 / cards . Эта команда GET
включает структуру JSON, которая указывает имя игрока. Код просто вызывает метод get_cards
локального объекта uno
для получения карт. Если get_cards
возвращается с массивом, массив отправляется обратно пользователю. Если массив не возвращается, пользователь не является игроком в этой игре, поэтому возвращается сообщение «извините».
Когда игроки хотят присоединиться к игре, они POST
команду POST
по URL-адресу http: // localhost :: 8080 / join . Если в текущей игре есть место, пользователь может присоединиться. Если нет, возвращается сообщение «извините».
Когда игроки хотят раздать карты, они POST
команду POST
по URL-адресу http: // localhost: 8080 / deal . Подобно вышеописанным сценариям, локальному объекту uno
посылается команда, чтобы перетасовать колоду и раздать по пять карт каждому игроку. Он может не справиться, если в игре нет игроков.
Во всех вышеупомянутых случаях значения, основанные на JSON, возвращаются игрокам.
Теперь мы рассмотрим клиентский код, чтобы увидеть, как REST API на стороне клиента реализует игровую среду.
# Ruby Uno Client require 'json' require 'rest-client' class UnoClient attr_reader :name def initialize name @name = name end def join_game response = RestClient.post 'http://localhost:8080/join', :data => {name: @name}.to_json, :accept => :json puts JSON.parse(response,:symbolize_names => true) end def get_cards response = RestClient.get 'http://localhost:8080/cards', {:params => {:name => @name}} puts response end def deal response = RestClient.post 'http://localhost:8080/deal', :data =>{}.to_json, :accept => :json puts response end end
Как видно, коммуникация на стороне клиента обманчиво проста. Мы используем Ruby-библиотеку rest-client
для создания минимально функциональных методов, которые имитируют игровые методы на сервере.
Мы даем имя этому игроку при создании UnoClient
класса UnoClient
. После этого мы можем присоединиться к игре, раздавать карты и получать копии наших карт.
Для объединения и обработки требуются команды POST
, как показано в строках 13 и 23.
Для получения карт требуется команда GET
, как показано в строке 18.
Обратите внимание, что мы просто распечатываем ответ с сервера, который отправляется нам в формате JSON. Очевидно, что полностью разработанная игра будет считывать возвращенную структуру JSON и отображать соответствующие результаты в браузере.
Список ниже показывает, что происходит, когда мы создаем экземпляры клиентов. Используя irb, пять объектов игрока для Боба, Кэрол, Теда, Алисы и Ральфа создаются вручную. Обратите внимание на сообщение об ошибке, когда бедный Ральф попытался присоединиться к игре. Также обратите внимание, что каждому игроку сдается уникальный набор карт из колоды. Все действие происходит на сервере, и только результаты отправляются обратно клиентам через API. Похоже, что Sinatra REST API работает!
> require './uno-client.rb' > bob_uno = UnoClient.new 'bob' > carol_uno = UnoClient.new 'carol' > ted_uno = UnoClient.new 'ted' > alice_uno = UnoClient.new 'alice' > ralph_uno = UnoClient.new 'ralph' > bob_uno.join_game {:status=>"welcome"} > carol_uno.join_game {:status=>"welcome"} > ted_uno.join_game {:status=>"welcome"} > alice_uno.join_game {:status=>"welcome"} > ralph_uno.join_game {:status=>"sorry - game not accepting new players"} > bob_uno.deal {"status":"success"} > bob_uno.get_cards {"cards":["3-diamond","2-club","4-spade","9-diamond","8-spade"]} > carol_uno.get_cards {"cards":["9-club","5-spade","king-spade","6-club","7-heart"]} > ted_uno.get_cards {"cards":["4-heart","6-heart","2-spade","8-club","ace-heart"]} > alice_uno.get_cards {"cards":["4-club","7-club","3-club","king-heart","jack-club"]}
Резюме
Архитектура REST предоставляет очень удобный механизм для перемещения данных между клиентами и серверами. Это особенно важно, когда вам нужно уменьшить нагрузку на ваш веб-сервер при работе с высокодинамичными данными. Как показано в примере из этой статьи, библиотека Sinatra может быть использована для реализации архитектуры REST очень простым и понятным способом.