Long Polling for Android

After reading the article , he began to implement Long Polling projects in web. On nginx, the server part is spinning, on javascript, clients listen to channels. First of all, it was very useful for private messages on the site.
Then, in support of web projects, applications for Android began to be developed. The question arose: how to implement a multi-user project, in which both browser clients and mobile applications would equally participate. Since Long Polling was already implemented in browser versions, it was decided to write a java module for Android as well.

There was no task to write a completely analogue to the js library, so I started writing a module for a special, but most popular, case.

Server side


So, I'll start from the server.

Nginx

Used by nginx-push-stream-module

In nginx settings:

# Директива для сбора статистики
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;
}

Three directives are described in the config: for sending, receiving messages, and also for receiving statistics.

Php

Messages are published by the server. Here is an example function in 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);
}

Here, too, everything is simple: we transmit the channel (s) and the message that we want to send. Due to the fact that plain plain text is not interesting to anyone, and there is a wonderful json format, you can send objects right away.

The concept of forming the name of the channels was considered for a long time. It is necessary to provide for the possibility of sending heterogeneous messages to all clients, or only to one, or to several, but filtered by some criterion.

As a result, the following format was developed, consisting of three parameters:
[id пользователя]_[название сервиса]_[id сервиса]

If we want to send a message to all users, then we use the channel:
0_main_0

if the user with id = 777:
777_main_0

if the order value has changed with id = 777 in the list common to all, then:
0_orderPriceChanged_777

It turned out very flexible.
Although later, after a little reflection, I came to the conclusion that it is better not to load clients with wiretapping of all channels, but to transfer this load to a server that will generate and send a message to each user.
And to separate message types, you can use a parameter, for example, act:
const ACT_NEW_MESSAGE = 1;
LongPolling::push($uid."_main_0", array(
     "act" => ACT_NEW_MESSAGE,
     "content" => "Hello, user ".$uid."!",
));


Client part


About the server side, everything seems to be. Let's get down to java!
The class I posted on gihub .
In my library, I used the android-async-http library , which provides convenient asynchronous http requests. In the example, I added a compiled jar file.

The interface of the class is quite simple.
First you need to create a callback object, the methods of which will receive answers. Since we primarily use objects in messages, we chose JsonHttpResponseHandler as the class’s callback:
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();
		}
	}
};

This example listens for messages about a new order, deleting an order, changing user attributes, and a new private message.

Next, initialize the LongPolling object (let's say we do this in 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);
}

If we only need Long Polling in Activity, then we need to register:
public void onResume() {
	super.onResume();
	lp.connect();
}
public void onPause() {
	super.onPause();
	lp.disconnect();
}

If you need to receive messages throughout the application (and this is usually the case), then the object can be initialized in the application class (Application) or in the service (Service).
Then right after initialization you need to start listening
lp = new LongPolling(getApplicationContext(), "http://example.com/lp/", Integer.toString(uid) + "_main_0", handler);
lp.connect();

And in order not to depress the user's battery, you need to register BroadcastReceiver for events of the appearance / disappearance of an Internet connection:
AndroidManifest.xml

    ...
    
        ...
        

and 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();
		}
	}
}


Statistics


Well, as a bun, it would be interesting to see real-time statistics, since we foreseen this in time.
Let's say we are interested in the number of users online.
To do this, we get the XML / json information on the url:
http://example.com/channels-stats?id=ALL

and see the following:
example.com208185304_main_000123_main_0001

In the subscribers tag, we see the number of listeners for each channel. But since in this case each user has his own channel, in PHP we will compile a list of users online:
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;
}

So we got the user id on the network. But there is one BUT. At the moment when the client reconnects to the server, it will not be in the statistics, this must be taken into account.

Conclusion


Well, like, told about everything. Now you can send messages to users in the browser, where JavaScript will accept them, and in the application on Android, using the same sender. And in addition, we can derive, for example on the site, the exact number of users online.

I will be glad to hear criticism and suggestions.

References

  1. https://github.com/jonasasx/LongPolling - actually the result of work
  2. https://github.com/wandenberg/nginx-push-stream-module - nxing module
  3. https://github.com/loopj/android-async-http - library of asynchronous http requests for Android

Also popular now: