Прочитав статью, стал внедрять в web проекты Long Polling. На nginx крутится серверная часть, на javascript клиенты слушают каналы. Прежде всего это было очень полезно для личных сообщений на сайте.
Потом в поддержку web проектов стали разрабатываться приложения под Android. Встал вопрос: как реализовать многопользовательский проект, в котором равнозначно участвовали бы как браузерные клиенты, так и мобильные приложения. Так как Long Polling уже был внедрён в браузерные версии, решено было написать java модуль и для Android.
Задачи написать полностью аналог js библиотеке не было, поэтому я начал писать модуль под частный, но наиболее популярный, случай.
Итак начну с сервера.
Используется nginx-push-stream-module
В настройках nginx:
В конфиге описаны три директивы: для отправки, приёма сообщений, а так же для получения статистики.
Сообщения публикуются сервером. Вот пример функции на php:
Здесь тоже всё просто: передаём канал(ы) и сообщение, которое хотим отправить. Ввиду того, что обычный plain текст никому не интересен, и существует замечательный формат json, отправлять можно сразу объекты.
Долго обдумывалась концепция формирования названия каналов. Нужно предусмотре��ь возможность отправления разнотипных сообщений всем клиентам, или только одному, или нескольким, но отфильтрованных по какому то признаку.
В итоге был разработан следующий формат, состоящий из трёх параметров:
Если мы хотим отправить сообщение всем пользователям, то используем канал:
если пользователю с id=777:
если изменилась стоимость заказа с id=777 в общем для всех списке, то:
Получилось очень гибко.
Хотя впоследствии, немного поразмыслив, пришёл к выводу, что клиентов лучше не нагружать прослушкой всех каналов, а перенести эту нагрузку на сервер, который будет формировать и отправлять сообщение каждому пользователю.
А для разделения типов сообщения можно использовать параметр, например act:
Про серверную часть вроде всё. Приступим к java!
Класс я разместил на gihub.
В своей библиотеке я использовал библиотеку android-async-http, осуществляющую удобные асинхронные http запросы. В примере я добавил скомпилированный jar файл.
Интерфейс у класса достаточно простой.
Для начала надо создать callback объект, в методы которого будут приходить ответы. Так как мы используем в сообщениях преимущественно объекты, то в качестве callback класса был выбран JsonHttpResponseHandler:
В этом примере слушаются сообщения о появлении нового заказа, удалении заказа, изменении атрибутов пользователя и новом личном сообщении.
Дальше инициализируем LongPolling объект (допустим мы это делаем в Activity):
Если Long Polling нам нужен только в Activity, то необходимо прописать:
Если же сообщения нужно принимать во всём приложении (а это как правило), то объект можно инициализировать в классе приложения (Application) или в сервисе (Service).
Тогда сразу после инициализации нужно начать прослушивание
И, дабы не угнетать аккумулятор пользователя, надо зарегистрировать BroadcastReceiver на события появления/исчезновения подключения к и��тернет:
AndroidManifest.xml
и InternetStateReceiver
Ну и в качестве плюшки, было б интересно посмотреть статистику реального времени, благо мы вовремя предусмотрели это.
Допустим, нас интересует количество пользователей онлайн.
Для этого мы получаем XML/json информацию по url:
и видим следующее:
В теге subscribers мы видим количество слушателей каждого канала. Но так как в данном случае у каждого пользователя свой канал, на PHP составим список пользователей онлайн:
Вот мы и получили id пользователей в сети. Но есть одно НО. В момент, когда клиент переподключается к серверу, в статистике его не будет, это необходимо учитывать.
Ну вот, вроде, обо всём рассказал. Теперь можно рассылать сообщения пользователям и в браузер, где их примет JavaScript, и в приложение на Android, используя один и тот же отправщик. И вдобавок можем выводить, к примеру на сайте, точное количество пользователей онлайн.
Буду рад услышать критику и предложения.
Потом в поддержку web проектов стали разрабатываться приложения под Android. Встал вопрос: как реализовать многопользовательский проект, в котором равнозначно участвовали бы как браузерные клиенты, так и мобильные приложения. Так как Long Polling уже был внедрён в браузерные версии, решено было написать java модуль и для Android.
Задачи написать полностью аналог js библиотеке не было, поэтому я начал писать модуль под частный, но наиболее популярный, случай.
Серверная часть
Итак начну с сервера.
Nginx
Используется nginx-push-stream-module
В настройках nginx:
# Директива для сбора статистики location /channels-stats { push_stream_channels_statistics; push_stream_channels_path $arg_id; } # Директива для публикации сообщений location /pub { push_stream_publisher admin; push_stream_channels_path $arg_id; push_stream_store_messages on; # Сохранять пропущенные сообщения, чтобы доставить их, когда клиент начнёт слушать канал } # Директива для прослушивания сообщений location ~ ^/lp/(.*) { push_stream_subscriber long-polling; push_stream_channels_path $1; push_stream_message_template "{\"id\":~id~,\"channel\":\"~channel~\",\"tag\":\"~tag~\",\"time\":\"~time~\",\"text\":~text~}"; push_stream_longpolling_connection_ttl 30s; }
В конфиге описаны три директивы: для отправки, приёма сообщений, а так же для получения статистики.
PHP
Сообщения публикуются сервером. Вот пример функции на php:
/* * $cids - ID канала, либо массив, у которого каждый элемент - ID канала * $text - сообщение, которое необходимо отправить */ public static function push($cids, $text) { $text = json_encode($text); $c = curl_init(); $url = 'http://example.com/pub?id='; curl_setopt($c, CURLOPT_RETURNTRANSFER, true); curl_setopt($c, CURLOPT_POST, true); $results = array(); if (!is_array($cids)) { $cids = array($cids); } $cids = array_unique($cids); foreach ($cids as $v) { curl_setopt($c, CURLOPT_URL, $url . $v); curl_setopt($c, CURLOPT_POSTFIELDS, $text); $results[] = curl_exec($c); } curl_close($c); }
Здесь тоже всё просто: передаём канал(ы) и сообщение, которое хотим отправить. Ввиду того, что обычный plain текст никому не интересен, и существует замечательный формат json, отправлять можно сразу объекты.
Долго обдумывалась концепция формирования названия каналов. Нужно предусмотре��ь возможность отправления разнотипных сообщений всем клиентам, или только одному, или нескольким, но отфильтрованных по какому то признаку.
В итоге был разработан следующий формат, состоящий из трёх параметров:
[id пользователя]_[название сервиса]_[id сервиса]
Если мы хотим отправить сообщение всем пользователям, то используем канал:
0_main_0
если пользователю с id=777:
777_main_0
если изменилась стоимость заказа с id=777 в общем для всех списке, то:
0_orderPriceChanged_777
Получилось очень гибко.
Хотя впоследствии, немного поразмыслив, пришёл к выводу, что клиентов лучше не нагружать прослушкой всех каналов, а перенести эту нагрузку на сервер, который будет формировать и отправлять сообщение каждому пользователю.
А для разделения типов сообщения можно использовать параметр, например act:
const ACT_NEW_MESSAGE = 1; LongPolling::push($uid."_main_0", array( "act" => ACT_NEW_MESSAGE, "content" => "Hello, user ".$uid."!", ));
Клиентская часть
Про серверную часть вроде всё. Приступим к java!
Класс я разместил на gihub.
В своей библиотеке я использовал библиотеку android-async-http, осуществляющую удобные асинхронные http запросы. В примере я добавил скомпилированный jar файл.
Интерфейс у класса достаточно простой.
Для начала надо создать callback объект, в методы которого будут приходить ответы. Так как мы используем в сообщениях преимущественно объекты, то в качестве callback класса был выбран JsonHttpResponseHandler:
private final static int ACT_NEW_ORDER = 1; private final static int ACT_DEL_ORDER = 2; private final static int ACT_ATTRIBUTES_CHANGED = 3; private final static int ACT_MESSAGE = 4; private final JsonHttpResponseHandler handler = new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { try { JSONObject json = response.getJSONObject("text"); switch (json.getInt("act")) { case ACT_NEW_ORDER: ... break; case ACT_DEL_ORDER: ... break; case ACT_ATTRIBUTES_CHANGED: ... break; case ACT_MESSAGE: ... break; default: break; } } catch (JSONException e) { e.printStackTrace(); } } };
В этом примере слушаются сообщения о появлении нового заказа, удалении заказа, изменении атрибутов пользователя и новом личном сообщении.
Дальше инициализируем LongPolling объект (допустим мы это делаем в Activity):
private LongPolling lp; private int uid = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_balance); lp = new LongPolling(getApplicationContext(), "http://example.com/lp/", Integer.toString(uid) + "_main_0", handler); }
Если Long Polling нам нужен только в Activity, то необходимо прописать:
public void onResume() { super.onResume(); lp.connect(); } public void onPause() { super.onPause(); lp.disconnect(); }
Если же сообщения нужно принимать во всём приложении (а это как правило), то объект можно инициализировать в классе приложения (Application) или в сервисе (Service).
Тогда сразу после инициализации нужно начать прослушивание
lp = new LongPolling(getApplicationContext(), "http://example.com/lp/", Integer.toString(uid) + "_main_0", handler); lp.connect();
И, дабы не угнетать аккумулятор пользователя, надо зарегистрировать BroadcastReceiver на события появления/исчезновения подключения к и��тернет:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> ... <application> ... <receiver android:name="com.app.example.receivers.InternetStateReceiver" > <intent-filter> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> <action android:name="android.net.wifi.supplicant.CONNECTION_CHANGE" /> </intent-filter> </receiver> </application> </manifest>
и InternetStateReceiver
public class InternetStateReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); final android.net.NetworkInfo wifi = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); final android.net.NetworkInfo mobile = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); if (wifi != null && wifi.isAvailable() || mobile != null && mobile.isAvailable()) { application.getInstance().lp.connect(); } else { application.getInstance().lp.disconnect(); } } }
Статистика
Ну и в качестве плюшки, было б интересно посмотреть статистику реального времени, благо мы вовремя предусмотрели это.
Допустим, нас интересует количество пользователей онлайн.
Для этого мы получаем XML/json информацию по url:
http://example.com/channels-stats?id=ALL
и видим следующее:
<?xml version="1.0" encoding="UTF-8" ?> <root> <hostname>example.com</hostname> <time>2014-03-22T00:03:37</time> <channels>2</channels> <wildcard_channels>0</wildcard_channels> <uptime>818530</uptime> <infos> <channel> <name>4_main_0</name> <published_messages>0</published_messages> <stored_messages>0</stored_messages> <subscribers>1</subscribers> </channel> <channel> <name>23_main_0</name> <published_messages>0</published_messages> <stored_messages>0</stored_messages> <subscribers>1</subscribers> </channel> </infos> </root>
В теге subscribers мы видим количество слушателей каждого канала. Но так как в данном случае у каждого пользователя свой канал, на PHP составим список пользователей онлайн:
const STATISTICS_URL = 'http://example.com/channels-stats?id=ALL'; public static function getOnlineIds() { $str = file_get_contents(self::STATISTICS_URL); if (!$str) return; $json = json_decode($str); if (empty($json -> infos)) return; $ids = array(); foreach ($json->infos as $v) { if ($v -> subscribers > 0 && substr_count($v -> channel, '_main_0') > 0) { $ids[] = str_replace('_main_0', '', $v -> channel); } } return $ids; }
Вот мы и получили id пользователей в сети. Но есть одно НО. В момент, когда клиент переподключается к серверу, в статистике его не будет, это необходимо учитывать.
Заключение
Ну вот, вроде, обо всём рассказал. Теперь можно рассылать сообщения пользователям и в браузер, где их примет JavaScript, и в приложение на Android, используя один и тот же отправщик. И вдобавок можем выводить, к примеру на сайте, точное количество пользователей онлайн.
Буду рад услышать критику и предложения.
Ссылки
- https://github.com/jonasasx/LongPolling — собственно результат работы
- https://github.com/wandenberg/nginx-push-stream-module — nxing модуль
- https://github.com/loopj/android-async-http — библиотека асинхронных http запросов для Android
