Прочитав статью, стал внедрять в 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