Краткое содержание первой части
В ответ на непрекращающийся бум мобильных социальных приложений, мы с друзьями решили собраться в мини-хакатон и написать очередную социальную сеть на Android с целью очертить круг общих вопросов и предложить скелет, из которого каждый сможет сделать что-то новое и оригинальное. В первой части мы рассмотрели интерфейс клиента, сетевые запросы, граф друзей и обработку изображений.
В этой статье мы вкратце расскажем про загрузку фотографий в облачное хранилище, доставку push-уведомлений и очереди асинхронных задач на сервере. Результат работы можно сразу посмотреть на github – android клиент и сервер на ruby on rails.
Содержание
Введение
Регистрация
Синхронизация контактов
Загрузка фотографий
Push-уведомления
Очереди асинхронных задач
Заключение
Введение
Серверная часть приложения выполняет функции по регистрации пользователей, синхронизации списка контактов и управлению списком друзей, загрузке и пост-обработке фотографий, управлению и выдаче комментариев/лайков, отправке push уведомлений. Рассмотрим эти вопросы более детально.
Регистрация
При регистрации от пользователя требуется указать имя и номер телефона, а также опционно выбрать аватар. Т.к. идентификация пользователей происходит по контактной книге, то важным аспектом является верификация указанного телефона, поэтому мы добавили смс-верификацию. Выбрать свой сервис для отправки смс вы можете из данной статьи.
Синхронизация контактов
Для построения графа друзей на сервере ведется учет контакт-листов пользователей и сопоставление его с телефонными номерами пользователей, указанными при регистрации. Все контакт-листы хранятся в хэшированном виде. Телефонные номера должны быть приведены к нормальной форме, для чего используется библиотека libphonenumber от Google.
String strRawPhone = "8-903-1234567"; PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); try { PhoneNumber swissNumberProto = phoneUtil.parse(swissNumberStr, "RU"); } catch (NumberParseException e) { System.err.println("NumberParseException was thrown: " + e.toString()); } System.out.println(phoneUtil.format(swissNumberProto, PhoneNumberFormat.E164)); //Результат: +79031234567
Стоит отметить один нюанс – код страны определяется в формате ISO-3166 относительно устройства пользователя, т.е. даже если в моей контактной книге находятся телефонные номера других стран, то при нормализации этих номеров необходимо использовать код страны «приписки» sim-карты моего устройства — RU.
Сопоставление телефонов происходит в одном из двух случаев:
- При регистрации нового пользователя, его телефон сравнивается с уже существующими контакт-листами
- Также при каждом запуске приложения контакт-лист повторно отправляется на сервер для выявления новых контактов
Для описанного сценария на БД сервера создается две таблицы – одна для самого контакт-листа и вторая для списка подтвержденных друзей (сам граф друзей). Такая схема позволяет изменять существующие контакты, не нарушая сформированные ранее ребра графа друзей.
create_table "contacts", force: true do |t| t.string "public_id" t.string "contact_key" t.datetime "created_at" t.datetime "updated_at" end create_table "friends", force: true do |t| t.string "public_id_src" t.string "public_id_dest" t.integer "status" t.datetime "created_at" t.datetime "updated_at" t.string "contact_key" end
Загрузка фотографий
В качестве хранилища фотографий мы выбрали два варианта – бесплатный аккаунт(free tier) AWS S3 как основной и собственный сервер как запасной (например на случай превышения лимита запросов в бесплатном аккаунте S3).
| Рис 1. Загрузка изображений на AWS S3 |
![]() |
Перед загрузкой клиент запрашивает у сервера временную публичную ссылку с правами записи, выполняет загрузку по этой ссылке напрямую на S3, после чего сообщает на сервер об успешной загрузке. Для работы с AWS S3 мы использовали aws-sdk gem. Перед работой необходимо завести аккаунт в AWS Web Services (на момент разработки была возможность завести бесплатный тестовый аккаунт на 5GB и 20,000 запросов) и получить пару ключей ACCESS_KEY/SECRET_ACCESS_KEY
require 'aws-sdk' class S3Storage ... def self.get_presigned_url(key) s3 = Aws::S3::Resource.new( :access_key_id => APP_CONFIG['s3_access_key_id'], :secret_access_key => APP_CONFIG['s3_secret_access_key'], :region => APP_CONFIG['s3_region']) obj = s3.bucket(APP_CONFIG['s3_bucket']).object(APP_CONFIG['s3_prefix'] + "/" + key) obj.presigned_url(:put, acl: 'public-read', expires_in: 3600) end ...
После того как клиент сообщил об успешной загрузки фотографии наш сервер в асинхронном режиме скачивает её, делает две миниатюры с помощью rmagick gem и сохраняет обратно на облачном хранилище. Миниатюры используются для облегчения трафика на мобильном устройстве при просмотре изображений в ленте.
require 'aws-sdk' require 'open-uri' require 's3' class Uploader @queue = :upload def self.perform(img_id) ... image = Image.where(image_id: img_id).first image_original = Magick::Image.from_blob(open(image.url_original).read).first image_medium = image_original.resize_to_fit(Image::MEDIUM_WIDTH, medium_height) image_medium.write( filepath_medium ){self.quality=100} ... end end
После того, как загруженные фотографии обработаны всем подписчикам рассылается push-уведомление.
Push-уведомления
При загрузке новых фотографий или добавлении комментариев подписчикам пользователя в реальном времени отправляются push-уведомления. Самым популярным и достаточно простым способом доставки push уведомлений в Android является GCM – Google Cloud Messaging. Перед использованием сервиса необходимо зарегистрировать свой проект в консоли разработчика, получить API-ключ и Project Number. API-ключ используется для авторизации сервера приложения при запросах к GCM, он добавляется в заголовок HTTP-запросов.
Со стороны клиента уникальным идентификатором получателя уведомлений является PushID, который получается путём обращения через GoogleCloudMessaging SDK с Android устройства напрямую к серверу GCM, при этом необходимо указать полученный ранее ProjectID. Полученный PushID отправляется на наш сервер приложения и впоследствии используется при доставке уведомлений.
public void registerPushID() { AsyncTask task = new AsyncTask() { @Override protected Object doInBackground(Object[] params) { String strPushID = ""; try { if (gcm == null) { gcm = GoogleCloudMessaging.getInstance(activity); } strPushID = gcm.register(Constants.PUSH_SENDER_ID); Log.d(LOG_TAG, "Received push id = " + strPushID); } catch (IOException ex) { Log.d(LOG_TAG, "Error: " + ex.getMessage()); } return strPushID; } @Override protected void onPostExecute(Object res) { final String strPushID = res != null ? (String) res : ""; if (!strPushID.isEmpty()) { UserProfile profile = new UserProfile(); profile.pushid = strPushID; Log.d(LOG_TAG, "Sending pushId " + strPushID + " to server"); ServerInterface.updateProfileRequest(activity, profile, new Response.Listener<String>() { @Override public void onResponse(String response) { Photobook.getPreferences().strPushRegID = strPushID; Photobook.getPreferences().savePreferences(); Log.d(LOG_TAG, "Delivered pushId to server"); } }, null); } } }; task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }
Соединение между сервером приложения и GCM может быть осуществлено двумя способами – через XMPP и HTTP. Первый вариант является асинхронным (позволяет отправлять несколько сообщений, не дожидаясь подтверждения по предыдущим), а также поддерживает двустороннюю связь upstream/downstream. HTTP поддерживает только синхронные downstream запросы, но допускает отправку уведомления сразу нескольким адресатам.
| Код 6. Пример отправки push-уведомлений (HTTP) |
lib/push.rb
|
Очереди асинхронных задач
Чтобы ускорить взаимодействие с клиентом, некоторые задачи на сервере выполняются в фоновом режиме. В частности это отправка Push уведомлений, а также масштабирование изображений. Для таких задач мы выбрали resque gem. Список решений по обработке очередей и краткое описание можно изучить по ссылке. Мы выбрали resque за его простоту установки и конфигурации, поддержку персистентности с помощью БД redis, наличие минималистского веб-интерфейса. После запуска rails сервера необходимо отдельно запустить обработчик очередей resque следующим способом:
QUEUE=* rake environment resque:work
После этого постановка новых задач в очередь осуществляется следующим способом (На примере отправки push-уведомлений)
#Crop and save uploaded file def create img_id = request.headers['imageid'] ... Resque.enqueue(Uploader, img_id) ... end
lib/uploader.rb
require 'aws-sdk' require 'open-uri' require 's3' class Uploader @queue = :upload def self.perform(img_id) ... author = User.where(id: image.author_id).first if (author != nil) followers = Friend.where(public_id_dest: author.id.to_s, status: Friend::STATUS_FRIEND) followers.each do |follower| data = {:image_id => img_id, :author => JSON.parse(author.profile), :image => image} PushSender.perform(follower.public_id_src, PushSender::EVENT_NEW_IMAGE, data) end end end end
Заключение
Работа над приложением велась без цели извлечения коммерческой выгоды и исключительно ради собственного интереса, а также для укрепления навыков работы в команде. Формат наших встреч был похож на хакатон выходного дня, в каждый день мы пытались реализовать конкретный модуль приложения. Мы будем рады, если у вас есть комментарии или предложения по улучшению проекта, а также планируем продолжать подобные хакатоны, так что если вы начинающий бэкэнд/веб/Android разработчик и у вас есть интерес поучаствовать в таком формате офлайн-встреч в Москве или же удаленно, то пишите нам по любым каналам связи.
P.S. Хочется отметить, что написание новой социальной сети не является сложной задачей и при наличии желания доступно даже начинающему разработчику Android. Вместо собственного бэкэнда можно использовать готовые решения от Google Apps Engine или Heroku. Намного большую сложность представляет проработка концепции, операционная поддержка и масштабирование сети в связи с ростом числа пользователей. Возможно мы рассмотрим эти вопросы в будущих статьях.
Всем удачи и хорошей недели!
ссылка на оригинал статьи http://habrahabr.ru/post/258111/

Добавить комментарий