Краткое содержание первой части
В ответ на непрекращающийся бум мобильных социальных приложений, мы с друзьями решили собраться в мини-хакатон и написать очередную социальную сеть на 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 |
Перед загрузкой клиент запрашивает у сервера временную публичную ссылку с правами записи, выполняет загрузку по этой ссылке напрямую на 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
Код 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-уведомлений
Код 6. Пример отправки push-уведомлений (HTTP) |
lib/push.rb
|
Очереди асинхронных задач
Чтобы ускорить взаимодействие с клиентом, некоторые задачи на сервере выполняются в фоновом режиме. В частности это отправка Push уведомлений, а также масштабирование изображений. Для таких задач мы выбрали resque gem. Список решений по обработке очередей и краткое описание можно изучить по ссылке. Мы выбрали resque за его простоту установки и конфигурации, поддержку персистентности с помощью БД redis, наличие минималистского веб-интерфейса. После запуска rails сервера необходимо отдельно запустить обработчик очередей resque следующим способом:
QUEUE=* rake environment resque:work
После этого постановка новых задач в очередь осуществляется следующим способом (На примере отправки push-уведомлений)
Код 7. Пример постановки задачи в очередь
app/controllers/image_controller.rb
lib/uploader.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 разработчик и у вас есть интерес поучаствовать в таком формате офлайн-встреч в Москве или же удаленно, то пишите нам по любым каналам связи.
Это мы
P.S. Хочется отметить, что написание новой социальной сети не является сложной задачей и при наличии желания доступно даже начинающему разработчику Android. Вместо собственного бэкэнда можно использовать готовые решения от Google Apps Engine или Heroku. Намного большую сложность представляет проработка концепции, операционная поддержка и масштабирование сети в связи с ростом числа пользователей. Возможно мы рассмотрим эти вопросы в будущих статьях.
github
Android клиент
Сервер на ruby on rails
Всем удачи и хорошей недели!