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

  • Tutorial

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


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

github
Android клиент
Сервер на ruby on rails

Всем удачи и хорошей недели!
  • +11
  • 26.9k
  • 2
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 2

    +1
    Спасибо, полезно.
      0
      Спасибо, мы старались!

    Only users with full accounts can post comments. Log in, please.