Long Polling для Android

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

Буду рад услышать критику и предложения.

Ссылки

  1. https://github.com/jonasasx/LongPolling — собственно результат работы
  2. https://github.com/wandenberg/nginx-push-stream-module — nxing модуль
  3. https://github.com/loopj/android-async-http — библиотека асинхронных http запросов для Android
  • +12
  • 15,7k
  • 6
Поделиться публикацией

Комментарии 6

    +2
    Под нагрузкой эта штука ведёт себя нестабильно (segfault).
    Поэтому:
    1) как самый первый франт используем ТОЛЬКО нормальный стоковый nginx
    2) nginx собранный с этим модулем (и с минимумом остальных) ставим ЗА первый.

    И наконец, делайте имена каналов подлиннее (можно перебором подобрать и увести данные). \

    PS опыт использования донного модуля — полгода. Пока всего 5к параллельных сессий в пике.
      +1
      Ну и закрыть (http авторизацией, например) служебный интерфейс публикации и получения статистики.
        0
        А cowboy не пробовали?
        0
        Почему бы не использовать SocketIO? У него есть режим Long polling. И велосипедов уже написано наверняка парочка.
          0
          Используем на сервере
          github.com/Atmosphere/atmosphere + Jetty
          (она поддерживает сразу и вебсокеты и longpolling)
          на веб клиенте jquery.atmosphere.js
          на Android клиенте de.tavendo.autobahn.WebSocket
            0
            Переместите, пожалуйста, статью в хаб «Разработка под Android»

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое