📜 ⬆️ ⬇️

Long Polling for Android

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

There was no task to write a complete analog of the js library, so I started writing a module for a particular, but most popular, case.

Server part


So let's start with the server.

Nginx

Nginx-push-stream-module is used
')
In the 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; } 

The config describes three directives: for sending, receiving messages, as well as for receiving statistics.

Php

Messages are published by the server. Here is an example of the 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 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 immediately.

The concept of forming the name of the channels was long thought over. It is necessary to provide the possibility of sending different types of messages to all clients, or only one or several, but filtered by some characteristic.

As a result, the following format was developed, consisting of three parameters:
 [id ]_[ ]_[id ] 

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

if the user with id = 777:
 777_main_0 

if the cost of the order has changed with id = 777 in general for all the list, 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 the server, which will form 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 part like everything. Get down to java!
I posted a class on gihub .
In my library, I used the android-async-http library, which implements convenient asynchronous http requests. In the example, I added a compiled jar file.

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

In this example, you hear messages about the appearance of a new order, deleting an order, changing user attributes, and a new personal message.

Next we initialize the LongPolling object (let's say we do it in the 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 need Long Polling only in Activity, then it is necessary to register:
 public void onResume() { super.onResume(); lp.connect(); } public void onPause() { super.onPause(); lp.disconnect(); } 

If messages need to be received in the entire application (and this is usually), then the object can be initialized in the application class (Application) or in the service (Service).
Then immediately 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, it is necessary to register the BroadcastReceiver for the events of appearance / disappearance of connection to the Internet:
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> 

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 provided for it in time.
Suppose we are interested in the number of users online.
For this we get the XML / json information by url:
 http://example.com/channels-stats?id=ALL 

and see the following:
 <?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> 

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

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

Conclusion


Well, sort of told everything. Now you can send messages to users in the browser, where they will receive JavaScript, and in the application on Android, using the same sender. And in addition we can display, for example on the site, the exact number of users online.

I will be glad to hear criticism and suggestions.

Links

  1. https://github.com/jonasasx/LongPolling - the actual result of the 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

Source: https://habr.com/ru/post/216713/


All Articles