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

от автора


Краткое содержание первой части

В ответ на непрекращающийся бум мобильных социальных приложений, мы с друзьями решили собраться в мини-хакатон и написать очередную социальную сеть на Android с целью очертить круг общих вопросов и предложить скелет, из которого каждый сможет сделать что-то новое и оригинальное. В первой части мы рассмотрели интерфейс клиента, сетевые запросы, граф друзей и обработку изображений.
В этой статье мы вкратце расскажем про загрузку фотографий в облачное хранилище, доставку push-уведомлений и очереди асинхронных задач на сервере. Результат работы можно сразу посмотреть на githubandroid клиент и сервер на ruby on rails.

Содержание

Введение
Регистрация
Синхронизация контактов
Загрузка фотографий
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<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 запросы, но допускает отправку уведомления сразу нескольким адресатам.

Рис 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. Намного большую сложность представляет проработка концепции, операционная поддержка и масштабирование сети в связи с ростом числа пользователей. Возможно мы рассмотрим эти вопросы в будущих статьях.

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

ссылка на оригинал статьи http://habrahabr.ru/post/258111/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *