Социальная сеть на Android за несколько выходных — часть II (сервер)

Краткое содержание первой части В ответ на непрекращающийся бум мобильных социальных приложений, мы с друзьями решили собраться в мини-хакатон и написать очередную социальную сеть на Android с целью очертить круг общих вопросов и предложить скелет, из которого каждый сможет сделать что-то новое и оригинальное. В первой части мы рассмотрели интерфейс клиента, сетевые запросы, граф друзей и обработку изображений.В этой статье мы вкратце расскажем про загрузку фотографий в облачное хранилище, доставку push-уведомлений и очереди асинхронных задач на сервере.Содержание ВведениеРегистрацияСинхронизация контактовЗагрузка фотографийPush-уведомленияОчереди асинхронных задачЗаключениеВведение Серверная часть приложения выполняет функции по регистрации пользователей, синхронизации списка контактов и управлению списком друзей, загрузке и пост-обработке фотографий, управлению и выдаче комментариев/лайков, отправке push уведомлений. Рассмотрим эти вопросы более детально.Регистрация При регистрации от пользователя требуется указать имя и номер телефона, а также опционно выбрать аватар. Т.к. идентификация пользователей происходит по контактной книге, то важным аспектом является верификация указанного телефона, поэтому мы добавили смс-верификацию. Выбрать свой сервис для отправки смс вы можете из данной статьи.Синхронизация контактов Для построения графа друзей на сервере ведется учет контакт-листов пользователей и сопоставление его с телефонными номерами пользователей, указанными при регистрации. Все контакт-листы хранятся в хэшированном виде. Телефонные номера должны быть приведены к нормальной форме, для чего используется библиотека libphonenumber от Google.Код 1. Пример нормализации в libphonenumber 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.Сопоставление телефонов происходит в одном из двух случаев:

При регистрации нового пользователя, его телефон сравнивается с уже существующими контакт-листами Также при каждом запуске приложения контакт-лист повторно отправляется на сервер для выявления новых контактов Для описанного сценария на БД сервера создается две таблицы — одна для самого контакт-листа и вторая для списка подтвержденных друзей (сам граф друзей). Такая схема позволяет изменять существующие контакты, не нарушая сформированные ранее ребра графа друзей.Код 2. Схема БД — contacts и friends db/schema.rb 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 image Перед загрузкой клиент запрашивает у сервера временную публичную ссылку с правами записи, выполняет загрузку по этой ссылке напрямую на S3, после чего сообщает на сервер об успешной загрузке. Для работы с AWS S3 мы использовали aws-sdk gem. Перед работой необходимо завести аккаунт в AWS Web Services (на момент разработки была возможность завести бесплатный тестовый аккаунт на 5GB и 20,000 запросов) и получить пару ключей ACCESS_KEY/SECRET_ACCESS_KEYКод 3. Запрос публичной ссылки в aws-sdk lib/s3.rb 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 и сохраняет обратно на облачном хранилище. Миниатюры используются для облегчения трафика на мобильном устройстве при просмотре изображений в ленте.Код 4. Пример создания миниатюр в rmagick lib/uploader.rb 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 отправляется на наш сервер приложения и впоследствии используется при доставке уведомлений.

Рис 2. Последовательность регистрации нового PushID image Код 5. Пример регистрации нового PushID (клиент) class MainActivityHandler 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() { @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 запросы, но допускает отправку уведомления сразу нескольким адресатам.Рис 3. Последовательность доставки push-уведомлений image Код 6. Пример отправки push-уведомлений (HTTP) lib/push.rb require 'net/http' class PushSender def self.perform (id, event, msg) user = User.where (id: id).first http = Net: HTTP.new ('android.googleapis.com', 80) request = Net: HTTP: Post.new ('/gcm/send', {'Content-Type' => 'application/json', 'Authorization' => 'key=' + APP_CONFIG['google_api_key']}) data = {: registration_ids => [user.pushid], : data => {: event => event, : msg => msg}} request.body = data.to_json response = http.request (request) end end Очереди асинхронных задач Чтобы ускорить взаимодействие с клиентом, некоторые задачи на сервере выполняются в фоновом режиме. В частности это отправка Push уведомлений, а также масштабирование изображений. Для таких задач мы выбрали resque gem. Список решений по обработке очередей и краткое описание можно изучить по ссылке. Мы выбрали resque за его простоту установки и конфигурации, поддержку персистентности с помощью БД redis, наличие минималистского веб-интерфейса. После запуска rails сервера необходимо отдельно запустить обработчик очередей resque следующим способом: QUEUE=* rake environment resque: work После этого постановка новых задач в очередь осуществляется следующим способом (На примере отправки push-уведомлений)Код 7. Пример постановки задачи в очередь app/controllers/image_controller.rb #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 разработчик и у вас есть интерес поучаствовать в таком формате офлайн-встреч в Москве или же удаленно, то пишите нам по любым каналам связи.Это мы image P.S. Хочется отметить, что написание новой социальной сети не является сложной задачей и при наличии желания доступно даже начинающему разработчику Android. Вместо собственного бэкэнда можно использовать готовые решения от Google Apps Engine или Heroku. Намного большую сложность представляет проработка концепции, операционная поддержка и масштабирование сети в связи с ростом числа пользователей. Возможно мы рассмотрим эти вопросы в будущих статьях.githubAndroid клиентСервер на ruby on rails

Всем удачи и хорошей недели!

© Habrahabr.ru