1. Введение
Данный проект представляет собой простой пример использования Google App Engine в мобильном приложении.
Cерверная часть предоставляет список пингвинов в формате JSON. Мобильный клиент запрашивает этот список по HTTP или HTTPS.
Также серверная часть ведёт запись определённых событий в базу данных, а именно количество посещений конкретного пингвина и количество нажатий кнопок: скормить рыбку и почесать животик.
У каждого пингвина есть поля описания Name
, Bio
и поля счётчиков.
2. Тонкости перевода
Думал как можно перевести Penguin Daycare Simulator на русский язык, но «детский сад» в качестве «daycare» не подходит, «дневной уход» тоже. Поэтому так и осталось без перевода.
3. Подготовка
Если у вас не установлен Google App Engine Go SDK, то переходите по ссылке Google App Engine, нажимайте «Try it now» и следуйте всем пунктам. Дайте имя своему проекту, выберите Go, скачайте и установите SDK. Убедитесь, что у вас корректно установлены переменные окружения (
PATH
, GOROOT
, GOPATH
, APPENGINE_DEV_APPSERVER
), для этого в терминале у вас должна быть видна команда goapp
. Забегая вперёд, скажу, что для загрузки простого проекта на сервер GAE и его запуска нужно выполнить команду goapp deploy
в директории проекта. Она спросит у вас email гугло-аккаунта, на котором должен быть расположен проект. Важно чтобы имя проекта совпадало в app.yaml и на сайте. Но в данном проекте используются модули и процесс загрузки несколько отличается.В качестве IDE для Go я рекомендую LiteIDE, а для Lua и Corona SDK — ZeroBrane Studio. Скачать Corona SDK можно на их сайте.
4. Клинт-сервер
На картинке ниже представлена очень сложная схема общения между клиентом (слева) и сервером (справа).
Как видно клиент запрашивает только список пингвинов и отсылает только три события. Общение ведётся по HTTP, но можно использовать и HTTPS совершенно бесплатно. Это можно отнести к одному из плюсов использования GAE — нет необходимости платить за SSL сертификат и настраивать работу с ним.
Так как всё работает по HTTP, то можно непосредственно в браузере выполнять запросы без использования специального клиента.
penguin-daycare-simulator.appspot.com
Простое приветствие, не используется мобильным клиентом, но позволяет сказать работает ли сервис. Можете заменить http на https и убедиться, что так тоже работает.
http://ift.tt/1rvUqlU
Это самый важный запрос. С его помощью мобильный клиент получает список всех пингвинов, которые в данный момент находятся под присмотром.
Для более удобного просмотра этих данных я рекомендую расширение JSONview для Chrome.
http://ift.tt/1rvUqC8
http://ift.tt/1fhXLmv
http://ift.tt/1rvUou9
Эти три запроса увеличивают соответствующие счётчики для какого-либо пингвина. Id
пингвина передаётся в качестве POST параметра. Сервер ничего в ответ не возвращает, но вы можете, если хотите, добавить в ответ строку «OK» или другой сигнал успешного выполнения операции.
5. Ещё скриншоты, больше скриншотов!
Уже перед публикацией статьи, вспомнил про этого пингвинчика:
6. Серверная часть — Google App Engine
Теперь можем перейти непосредственно к коду. Рассмотрим файловую структуру проекта на Go.
PenguinDaycareSimulatorServer/
├── default/
│ ├── app.go
│ ├── default.yaml
│ └── penguins.json
├── static/
│ ├── favicon.ico
│ └── static.yaml
└── dispatch.yaml
default
и static
— это модули. Проект для GAE может быть разбит на модули, а может работать и без них. В этом случае нужны только три файла: app.yaml
, app/app.go
и penguins.json
. Изначально так и было в моём проекте (можно посмотреть первый коммит на GitHub), но мне захотелось добавить настройку max_concurrent_requests
, которая отвечает за то, сколько одновременных запросов может обрабатывать один instance вашего приложения. Значение по умолчанию — всего 10. Go явно способен на большее. Максимальное значение — 500. При росте нагрузки и превышении этого значения, запускаются дополнительные копии вашего приложения и нагрузка распределяется между ними. Если хотите укладываться только в бесплатные квоты для GAE, то использование этой настройки крайне желательно. Если приложение не справляется с такой нагрузкой, то снижайте это значение и переходите на платный биллинг.Так вот эта настройка доступна только для модулей. И в вашем приложении должно быть минимум 2 модуля, чтобы GAE посчитал его модульным.
static
— очень простой модуль, без которого можно было бы и обойтись (если бы не ограничение GAE выше), его задача только в том, чтобы отдавать статично файл favicon.ico
.
default
— основной модуль, который и выполняет всю работу.
Файлы *.yaml
— это настройки и описания. По одному на каждый модуль и один файл dispatch.yaml
, который описывает какие URL какой модуль обрабатывает.
application: penguin-daycare-simulator
dispatch:
- url: "*/favicon.ico"
module: static
- url: "*/"
module: default
application: penguin-daycare-simulator
module: static
version: 1
runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /favicon.ico
static_files: favicon.ico
upload: favicon.ico
application: penguin-daycare-simulator
module: default
version: 1
runtime: go
api_version: go1
automatic_scaling:
max_concurrent_requests: 500
handlers:
- url: /.*
script: _go_app
Обратите внимание, что в
static.yaml
runtime указан Python, а не Go. Это сделано потому, что GAE ругается, если пытаетесь загрузить модуль на Go без собственно Go файлов. Однако он не ругается на Python и PHP при такой ситуации.Внимательный читатель здесь может возразить мол «чем PHP хуже Python для отдачи статичных файлов» и попытаться развязать holywar, но Python лично мне ближе, поэтому и выбрал его. Любой другой может использовать PHP для этих целей. Конечно, это всё бессмысленно, так как ни Python, ни PHP не учавствуют в этом процессе.
handlers
в default.yaml
указывает какие исполняемые файлы обрабатывает определённые URL. В нашем случае app.go обрабатывает все приходящие запросы (с учётом dispatch.yaml
). Описание URL очень гибкое, использует регулярные выражения. Однако если для Python и PHP можно использовать разные файлы для обработки разных URL внутри одного модуля, то для Go это должен быть один единственный файл, который обозначается как "_go_app". Дальше уже внутри программы на Go можно выделить обработчики для разных URL и разбить всё приложение на несколько файлов, если необходимо.Больше про настройку и yaml файлы можно почитать тут.
penguins.json
— файл в формате JSON, содержащий в себе имена и описание всех используемых пингвинов.
[
{"id": "1",
"name": "Tux",
"bio": "Beloved Linux mascot"
},
{"id": "2",
"name": "Skipper",
"bio": "Small combat squad leader"
},
{"id": "3",
"name": "Lolo",
"bio": "Russian adventurer"
},
{"id": "4",
"name": "Gunter",
"bio": "The darkest character in Adventure Time"
},
{"id": "5",
"name": "The Penguin",
"bio": "Na, na, na, na, na, na, na, na, na, na... The Penguin! "
}
]
Добавление, редактирование пингвинов происходит через этот файл.
Теперь мы подошли к app.go
— сердцу всего приложения. Полный листинг удобно смотреть сразу на GitHub — app.go.
Упрощённая структура этого файла:
package app
Перечисление всех используемых библиотек.
import (...)
Структура каждого пингвина: Id, имя, описание, счётчики.
type penguin struct {...}
Слайс (массив) всех пингвинов.
var penguins []penguin
Структура записи в базу данных.
type penguinEntity struct {...}
Инициализация.
func init() {...}
Чтение penguins.json в слайс penguins.
func loadPenguinsJson() {...}
Обработчик / - вывод простого сообщения.
func rootHandler(w http.ResponseWriter, r *http.Request) {...}
Обработчик /penguins - вывод всех пингивнов со статистикой в формате JSON.
func penguinsHandler(w http.ResponseWriter, r *http.Request) {...}
Обработчик события /stat/visit - посещение пингвина.
func visitHandler(w http.ResponseWriter, r *http.Request) {...}
Обработчик события /stat/fish - кормление пингвина рыбкой.
func fishHandler(w http.ResponseWriter, r *http.Request) {...}
Обработчик события /stat/bellyrub - почёсывание пингвина по животику.
func bellyrubHandler(w http.ResponseWriter, r *http.Request) {...}
При запуске приложения первым делом запускается функция init(), которая производит чтение из файла penguins.json и устанавливает какая функция в ответе за разные запросы со стороны клиента. Вы уже могли ими воспользоваться по ссылкам в начале статьи.
penguinsHandler()
сериализует слайс penguins в JSON формат функцией json.Marshal()
и отдаёт клиентам через fmt.Fprint()
.
visitHandler()
, fishHandler()
, bellyrubHandler()
действуют по одной логике — берём пингвина из базы данных, увеличиваем на единицу соответсвующий параметр и записываем обратно в базу данных. База данных — Datastore — не является SQL совместимой, то есть она представляет собой NoSQL решение. Описание её работы достойно отдельной статьи.
Так как многие операции на GAE тарифицируются отдельно, в том числе и доступ к Datastore, то следует избегать излишнего использования ресурсов. Так, например, при запросе статистики по всем пингвинам совершенно необязательно предоставлять актуальные данные. Можно кэшировать этот запрос с временем жизни кэша скажем 10 минут. Для этого я ввёл дополнительную переменную lastUpdateTime
— метку времени последнего обновления слайса penguins
. А при каждом запросе /penguins
я вызываю функцию updatePenguinsStatistics()
, которая проверяет не истекло ли время жизни кэша и в цикле обновляет показания счётчиков для каждого пингвина в слайсе penguins
.
Чтобы форсировать обновление вручную, я ввёл дополнительный запрос /update и соответствующий обработчик updateHandler()
.
Каждый запрос обрабатывается в собственной goroutine, поэтому нужно защитить слайс penguins
от возможной одновременной записи или чтения во время записи. Для этого используется RWMutex
— мьютекс на чтение или запись. Его использование более эффективно, чем простого Mutex
.
Для избежания платного потребления ресурсов, можно также ввести отложенную запись в базу данных, накапливая значения всех поступивших событий.
Для загрузки проекта на сервер GAE нужно выполнить три команды в терминале в директории проекта:
goapp deploy default/default.yaml
goapp deploy static/static.yaml
appcfg.py update_dispatch .
В дальнейшем при изменении app.go, необходимо только будет запускать
goapp deploy default/default.yaml
.В заключении про серверную часть скажу, что для увеличения бесплатных лимитов я рекомендую подключить платный биллинг, но при этом задать максимальную стоимость в день равную $1. При этом некоторые бесплатные квоты увеличиваются, а вы всё ещё ничего не тратите.
7. Клиентская часть — Corona SDK
Corona SDK — это кроссплатформенный фреймворк для разработки мобильный приложений под Android, iOS, Windows Phone (скоро) и HTML5 (в разработке). Использую данный продукт уже довольно давно, пишу игры как для клиентов в качестве фрилансера, так и для себя. Отмечу достойную скорость работы и быстроту создания приложений.
Начнём тоже с файловой структуры проекта. Файлов здесь больше, в основном за счёт иконок и картинок, поэтому убираю под спойлер.
PenguinDaycareSimulator/
├── images/
│ ├── penguins/
│ │ ├── 1.png
│ │ ├── 1@2x.png
│ │ ├── 2.png
│ │ ├── 2@2x.png
│ │ ├── 3.png
│ │ ├── 3@2x.png
│ │ ├── 4.png
│ │ ├── 4@2x.png
│ │ ├── 5.png
│ │ └── 5@2x.png
│ ├── background.jpg
│ ├── background@2x.jpg
│ ├── button-over.png
│ ├── button-over@2x.png
│ ├── button.png
│ ├── button@2x.png
│ ├── dot-off.png
│ ├── dot-off@2x.png
│ ├── dot.png
│ ├── dot@2x.png
│ ├── fish.png
│ ├── fish@2x.png
│ ├── hand.png
│ ├── hand@2x.png
│ ├── popup.png
│ └── popup@2x.png
├── lib/
│ ├── api.lua
│ ├── app.lua
│ └── utils.lua
├── scenes/
│ ├── choose.lua
│ ├── menu.lua
│ └── penguin.lua
├── Default-568h@2x.png
├── Icon-60.png
├── Icon-60@2x.png
├── Icon-72.png
├── Icon-72@2x.png
├── Icon-76.png
├── Icon-76@2x.png
├── Icon-Small-40.png
├── Icon-Small-40@2x.png
├── Icon-Small-50.png
├── Icon-Small-50@2x.png
├── Icon-Small.png
├── Icon-Small@2x.png
├── Icon-hdpi.png
├── Icon-ldpi.png
├── Icon-mdpi.png
├── Icon-ouya.png
├── Icon-xhdpi.png
├── Icon-xxhdpi.png
├── Icon.png
├── Icon@2x.png
├── build.settings
├── config.lua
└── main.lua
Можете пока обратить внимание только на Lua файлы.
config.lua
, build.settings
— файлы настройки проекта для Corona SDK. Указывают портретный или ландшафтный вид имеет приложение, опорное разрешение экрана, способ масштабирования и другие разные настройки. Если Corona SDK для вас в новинку, то можете не обращать пока внимание на эти файлы.
Также в корне вы найдёте кучу иконок под iOS и Android, плюс Default-568h@2x.png
для корректной работы на iPhone 5. Внутри директории images/ есть обычные файлы и их удвоенные HD версии @2x
. Сейчас в принципе уже можно не поддерживать устройства с экранами вроде iPhone 3GS, их процент очень мал, но тем не менее отличен от нуля. Для полноценной поддержки iPad Retina вам уже нужны будут @4x
файлы и строчка в config.lua
, но большинство игр и так нормально работают.
Corona SDK запускает приложение начиная с файла main.lua
, в нём подключаются нужные библиотеки, объявляются некоторые переменные и происходит переход на сцену с кнопкой «Enter the Daycare». Все сцены (экраны) приложения хранятся в разных файлах и собраны в директории scenes/
, а все пользовательские библиотеки я разместил в lib/
. Разработчик волен располагать эти файлы как ему захочется, я предпочитаю так.
В lib/
находятся app.lua
и utils.lua
— вместе это мой сборник полезных функций для работы с Corona SDK. В app.lua
реализованы удобные обёртки над стандартными функциями Corona SDK для отображения картинок, текста, кнопок и др.
Переход из main.lua
в scenes/menu.lua
осуществляется через строчку
storyboard.gotoScene('scenes.menu')
Где, в свою очередь, уже выполняется запрос пингвинов на сервере. Вот основной кусок кода из
menu.lua
.function scene:createScene (event)
local group = self.view
app.newText{g = group, text = 'Penguin Daycare', size = 32, x = _CX, y = _CY - 150}
app.newText{g = group, text = 'Simulator', size = 32, x = _CX, y = _CY - 110}
local pleaseWait = app.newText{g = group, text = 'Please Wait', size = 16, x = _CX, y = _CY}
local button = app.newButton{g = group, x = _CX, y = _CY,
text = 'Enter the Daycare',
onRelease = function()
storyboard.gotoScene('scenes.choose', {effect = 'slideLeft', time = app.duration})
end}
button.isVisible = false
app.api:getPenguins(function()
pleaseWait.isVisible = false
button.isVisible = true
end)
end
Создаются три строчки текста и одна кнопка. Кнопка спрятана до тех пор, пока мы не получим ответ от сервера. Сам запрос выполняется функцией
app.api:getPenguins()
, в качестве аргумента у неё callback-функция.После нажатия на кнопку мы попадаем на сцену выбора пингвина, тоже приведу только основную часть кода из файла choose.lua
.
function scene:createScene(event)
local group = self.view
self.backButton = app.newButton{g = group, x = _L + 10, y = _T + 10, w = 48, h = 32, rp = 'TopLeft',
text = 'Back',
fontSize = 14,
onRelease = function()
storyboard.gotoScene('scenes.menu', {effect = 'slideRight', time = app.duration})
end}
local function gotoPenguin(ind)
storyboard.gotoScene('scenes.penguin', {effect = 'slideLeft', time = app.duration, params = ind})
end
local slideView = newSlideView{g = group, x = 0, y = _CY, dots_y = 180, onRelease = gotoPenguin}
for i = 1, #app.api.penguins do
local p = app.api.penguins[i]
local slide = display.newGroup()
app.newImage('images/popup.png', {g = slide, w = 300, h = 335})
app.newImage('images/penguins/' .. p.id .. '.png', {g = slide, w = 200, h = 256})
app.newText{g = slide, x = 0, y = -140, text = p.name, size = 18, color = 'white'}
app.newText{g = slide, x = 0, y = 140, text = p.bio, size = 14, color = 'white', w = 220, align = 'center'}
slideView:addSlide(slide)
end
slideView:makeDots()
slideView:gotoSlide(1)
end
Здесь
newSlideView()
это функция, создающая простой виджет, с помощью которого можно пролистывать слайды с пингвинами. Код этого виджета располагается тут же в choose.lua
в начале файла.Для каждого пингвина создаётся слайд. Изображения пингвинов хранятся внутри приложения и соответсвуют Id пингвинов. Это дело можно исправить путём хранения изображений на сервере GAE или любом другом. Для загрузки картинок из сети в Corona SDK есть функция display.loadRemoteImage()
или более низкоуровневая network.download()
.
По нажатию на слайд вызывается функция gotoPenguin()
, которая получает номер (не Id
) пингвина в массиве (table) всех полученных пингвинов. Эта функция производит переход на заключительную сцену penguin.lua
, передавая этой сцене тот же самый аргумент.
function scene:createScene(event)
local group = self.view
local background = app.newImage('images/background.jpg', {g = group, w = 384, h = 640, x = _CX, y = _CY})
self.backButton = app.newButton{g = group, x = _L + 10, y = _T + 10, w = 48, h = 32, rp = 'TopLeft',
text = 'Back',
fontSize = 14,
onRelease = function()
storyboard.gotoScene('scenes.choose', {effect = 'slideRight', time = app.duration})
end}
local ind = event.params
local p = app.api.penguins[ind]
local visitsLabel = app.newText{g = group, x = _CX, y = _T + 20, text = 'Visits: ' .. p.visit_count, size = 18, color = 'white'}
local fishLabel = app.newText{g = group, x = _CX, y = _T + 40, text = 'Fish: ' .. p.fish_count, size = 18, color = 'white'}
local bellyrubsLabel = app.newText{g = group, x = _CX, y = _T + 60, text = 'Belly rubs: ' .. p.bellyrub_count, size = 18, color = 'white'}
local penguin = app.newImage('images/penguins/' .. p.id .. '.png', {g = group, w = 200, h = 256, x = _CX, y = _CY - 25})
app.newButton{g = group, x = _CX - 80, y = _B - 50, w = 128, h = 48,
text = 'Fish',
fontSize = 14,
onRelease = function()
local fish = app.newImage('images/fish.png', {g = group, x = penguin.x, y = penguin.y + 200, w = 512, h = 188})
fish.alpha = 0.8
transition.to(fish, {time = 400, alpha = 1, y = penguin.y, xScale = 0.1, yScale = 0.1, transition = easing.outExpo, onComplete = function(obj)
transition.to(fish, {time = 400, alpha = 0, onComplete = function(obj)
display.remove(obj)
end})
end})
app.api:sendFish(p.id)
p.fish_count = p.fish_count + 1
fishLabel:setText('Fish: ' .. p.fish_count)
end}
app.newButton{g = group, x = _CX + 80, y = _B - 50, w = 128, h = 48,
text = 'Belly rub',
fontSize = 14,
onRelease = function()
local hand = app.newImage('images/hand.png', {g = group, x = penguin.x - 40, y = penguin.y + 30, w = 80, h = 80, rp = 'TopLeft'})
transition.to(hand, {time = 1200, x = penguin.x + 40, transition = easing.swing3(easing.outQuad), onComplete = function(obj)
display.remove(obj)
end})
app.api:sendBellyrub(p.id)
p.bellyrub_count = p.bellyrub_count + 1
bellyrubsLabel:setText('Belly rubs: ' .. p.bellyrub_count)
end}
app.api:sendVisit(p.id)
p.visit_count = p.visit_count + 1
visitsLabel:setText('Visits: ' .. p.visit_count)
end
В
penguin.lua
происходит загрузка фонового изображения, изображения выбранного пингвина, отображение нескольких текстовых меток и двух кнопок-действий. При нажатии на них происходит анимация действия и отправка запроса на сервер через функции app.api:sendFish()
и app.api:sendBellyrub()
. А app.api:sendVisit()
вызывается сразу после создания сцены. После вызова каждой из этих функций обновляются соответствующие текстовые метки, даже если нет интернета. Это можно исправить, введя проверку на получение ответа от сервера по каждому запросу и предоставляя callback-функции.Наконец, вся работа с сервером осуществляется в файле lib/api.lua
.
local _M = {}
local app = require('lib.app')
_M.hostname = 'http://ift.tt/1fhXIXH'
function _M:getPenguins(callback)
local url = '/penguins#' .. math.random(1000, 9999)
network.request(self.hostname .. url , 'GET', function (event)
if not event.isError then
local response = json.decode(event.response)
if response then
self.penguins = response
callback()
end
end
end)
end
function _M:sendVisit(id)
local url = '/stat/visit'
local request = {body = 'id=' .. id}
network.request(self.hostname .. url , 'POST', function (event)
if event.isError then
app.alert('Network error')
end
end, request)
end
function _M:sendFish(id)
local url = '/stat/fish'
local request = {body = 'id=' .. id}
network.request(self.hostname .. url , 'POST', function (event)
if event.isError then
app.alert('Network error')
end
end, request)
end
function _M:sendBellyrub(id)
local url = '/stat/bellyrub'
local request = {body = 'id=' .. id}
network.request(self.hostname .. url , 'POST', function (event)
if event.isError then
app.alert('Network error')
end
end, request)
end
return _M
Как можно было догадаться, работа с сервером производится простыми POST запросами. В случае
getPenguins()
, ответ от сервера конвертируется из JSON формата в массив (table) фукцией json.decode()
и помещается в поле (property) модуля.Как видите, посылать POST запросы и реагировать на их ответы в Corona SDK очень просто. Соответственно очень простая вышла и сама интеграция с Google App Engine. Я не расписываю что делает каждая строчка, надеюсь синтаксис интуитивно понятен.
8. Ссылки
Исходники лежат у меня на GitHub:
Можно установить клиентскую часть на Android 2.3.3+, вот APK (mirror).
Либо скачивайте Corona SDK, скачивайте исходники с GitHub и запускайте в Corona Simulator.
Либо запускайте прямо в браузере HTML5 версию. Однако в этом режиме ещё пока нет поддержки сети, поэтому все показатели по нулям. Вдобавок ещё текст неточно отрисовывается.
Спасибо M0sTH8 за помощь в написании статьи.
Подписывайтесь на мой твиттер @SergeyLerg
На этом всё. Спасибо за внимание!
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий