前段时间用了轮询的方法实现了app从服务端获取数据
现在采用mqtt协议使用长连接,让服务端推送数据,减少流量和耗电,原理就是用安卓的原生socket协议,客户端几分钟发一两个字节保证链路畅通,让各经过点的NAT记录保持住。
网上有的是老的代码,安卓4.0要求socket协议的处理要放在线程中,放在service中都无效,connet的时候会有Exception
所以找到饿了paho项目中的这个版本,mqtt客户端jar调用sdk:
org.eclipse.paho.client.mqttv3.jar
package com.zeeeitch.healthinfo_tongzhi;
import android.app.*;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.provider.Settings.Secure;
import android.util.Log;
import com.zeeeitch.gxs.SIMCardInfo;
import com.zeeeitch.utils.android.DataUtil;
import org.eclipse.paho.client.mqttv3.*;//paho软件包http://www.eclipse.org/proposals/technology.paho/
import org.eclipse.paho.client.mqttv3.internal.MemoryPersistence;
import org.peng.util.HttpUtil;
import java.io.File;
import java.util.Locale;
public class MqttService extends Service implements MqttCallback
{
public static final String DEBUG_TAG = "MqttService"; // Debug TAG
private static final String MQTT_THREAD_NAME = "MqttService[" + DEBUG_TAG + "]"; // Handler Thread ID
private static final String MQTT_BROKER = "xxx.xxx.edu.cn"; // Broker URL or IP Address
private static final int MQTT_PORT = 5222; // Broker Port
//三个质量级别,我的项目用的1
public static final int MQTT_QOS_0 = 0; // QOS Level 0 ( Delivery Once no confirmation )
public static final int MQTT_QOS_1 = 1; // QOS Level 1 ( Delevery at least Once with confirmation )
public static final int MQTT_QOS_2 = 2; // QOS Level 2 ( Delivery only once with confirmation with handshake )
private static final int MQTT_KEEP_ALIVE = 80 * 1000; // KeepAlive Interval ,每次尝试断线重连,4次是已连接的情况下keepalive
private final int maxReconnectMultipler = 40;//最大重连时间除以MQTT_KEEP_ALIVE的倍数
private final int mKeepAliveInterval = 4 * 60;//告诉服务器的间隔 4分钟发送心跳包,保证长连接,每次只有10几个字节
private int mKeepRecMultipler = 0; // 重连与keepalive的倍数
private int keepAliveTimes = -2;// keepalive函数运行了几次,到4就复0,负数越大,启动后延迟越久重连
private int reconnectTriedTimes = 0;//记录重连尝试了几次
private int keepAliveRunTimesInReconnect = 0;//记录在重连阶段,keepalive函数运行了几次
private long lastKeepAliveRun;
// private Thread mKeepAliveWakeUpThread = null;
private static final String MQTT_KEEP_ALIVE_TOPIC_FORAMT = "/ka/%s"; // Topic format for KeepAlives
private static final byte[] MQTT_KEEP_ALIVE_MESSAGE = {0}; // Keep Alive message to send
private static final int MQTT_KEEP_ALIVE_QOS = MQTT_QOS_1; // Default Keepalive QOS
private static final boolean MQTT_CLEAN_SESSION = false; // Start a clean session?
private static final String MQTT_URL_FORMAT = "tcp://%s:%d"; // URL Format normally don't change
private static final String ACTION_START = DEBUG_TAG + ".START"; // Action to start
private static final String ACTION_STOP = DEBUG_TAG + ".STOP"; // Action to stop
private static final String ACTION_KEEPALIVE = DEBUG_TAG + ".KEEPALIVE"; // Action to keep alive used by alarm manager
private static final String ACTION_RECONNECT = DEBUG_TAG + ".RECONNECT"; // Action to reconnect
private static final String ACTION_RESUBSCRIBE = DEBUG_TAG + ".RESUBSCRIB"; // Action to reconnect
private static String mKeepalivealarm = "TCAlert.app.alarm";
private String mTopic = "Jxs/formal/%s";//可注册多个topic,不同topic分别推送/响应
private String mTopic2 = "Jxs/formal/notifyrepeat/%s";
private String Imei ="";
//mqtt从报警改到通知后 Boradcas_action_tongzhi 就是通知发给报警的广播
private String Boradcas_action_tongzhi ="jxs.app.boradcas.action.tongzhi";
private String Boradcas_action_gongzi ="jxs.app.boradcas.action.gongzi";
private static final int MQTT_TOPIC_SUBSCRIBE_QOS = MQTT_QOS_1; // Default Keepalive QOS
// private boolean mSubTestTopic = true;
// private boolean mSubTestTopic = false;
private static final String DEVICE_ID_FORMAT = "andr_%s"; // Device ID Format, add any prefix you'd like
// Note: There is a 23 character limit you will get
// An NPE if you go over that limit
private boolean mStarted = false; // Is the Client started?
private String mDeviceId; // Device ID, Secure.ANDROID_ID
private Handler mConnHandler; // Seperate Handler thread for networking
private MqttDefaultFilePersistence mDataStore; // Defaults to FileStore
private MemoryPersistence mMemStore; // On Fail reverts to MemoryStore
private MqttConnectOptions mOpts; // Connection Options
private MqttTopic mKeepAliveTopic; // Instance Variable for Keepalive topic
private MqttClient mClient; // Mqtt Client
private AlarmManager mAlarmManager; // Alarm manager to perform repeating tasks
private ConnectivityManager mConnectivityManager; // To check for connectivity changes
private Context mContext = this;
private String exangeFile = "exchange.txt";//给另外两个应用,提供是否有新消息的数据,存储
/**
* Start MQTT Client
* 外界调用这个函数,启动
*
* @return void
*/
public static void actionStart(Context ctx)
{
Intent i = new Intent(ctx, MqttService.class);
i.setAction(ACTION_START);
ctx.startService(i);
}
/**
* Stop MQTT Client
* 外界调用这个函数,停止
*
* @return void
*/
public static void actionStop(Context ctx)
{
Intent i = new Intent(ctx, MqttService.class);
i.setAction(ACTION_STOP);
ctx.startService(i);
}
/**
* Send a KeepAlive Message
* 外界调用这个函数,保持连接
*
* @return void
*/
public static void actionKeepalive(Context ctx)
{
Intent i = new Intent(ctx, MqttService.class);
i.setAction(ACTION_KEEPALIVE);
ctx.startService(i);
}
/**
* Initalizes the DeviceId and most instance variables
* Including the Connection Handler, Datastore, Alarm Manager
* and ConnectivityManager.
*/
@Override
public void onCreate()
{
super.onCreate();
mDeviceId = String.format(DEVICE_ID_FORMAT, Secure.getString(getContentResolver(), Secure.ANDROID_ID));
HandlerThread thread = new HandlerThread(MQTT_THREAD_NAME);
thread.start();
mConnHandler = new Handler(thread.getLooper());
lastKeepAliveRun = System.currentTimeMillis() - mKeepAliveInterval * 2;
try
{
mDataStore = new MqttDefaultFilePersistence(getCacheDir().getAbsolutePath());
}
catch (MqttPersistenceException e)
{
e.printStackTrace();
mDataStore = null;
mMemStore = new MemoryPersistence();
}
mKeepRecMultipler = mKeepAliveInterval / (MQTT_KEEP_ALIVE / 1000);
//控制keepAliveTimes不要太小,不然第一次keepalive就会太久,失败了
keepAliveTimes = Math.max(keepAliveTimes, (int) (-Math.floor((mKeepAliveInterval * 1.5 - 3) / (MQTT_KEEP_ALIVE / 1000))));
mOpts = new MqttConnectOptions();
mOpts.setCleanSession(MQTT_CLEAN_SESSION);
mOpts.setKeepAliveInterval(mKeepAliveInterval);
// Do not set keep alive interval on mOpts we keep track of it with alarm's
mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
startKeepAlives();
mConnectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
crun.setInstanceForLock(this);
registerBoradcastReceiver();
SIMCardInfo sim = new SIMCardInfo(MqttService.this);
if (sim.getIMEI() != null && sim.getIMEI().length() > 5){
Imei=sim.getIMEI();
//多个应用的mqtt,mdevice要保值不一样,让服务器区别
mDeviceId = mDeviceId.substring(0, mDeviceId.length() - 6) + sim.getIMEI().substring(sim.getIMEI().length() - 6);
}
}
/**
* 关闭定时广播线程
*/
@Override
public void onDestroy()
{
stopKeepAlives();
}
/**
* Service onStartCommand
* Handles the action passed via the Intent
* 响应actionxxx函数的消息
*
* @return START_REDELIVER_INTENT
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
super.onStartCommand(intent, flags, startId);
String action = intent.getAction();
Log.i(DEBUG_TAG, "Received action of " + action);
if (action == null)
{
Log.i(DEBUG_TAG, "Starting service with no action\n Probably from a crash");
}
else
{
if (action.equals(ACTION_START))
{
Log.i(DEBUG_TAG, "Received ACTION_START");
start();
}
else if (action.equals(ACTION_KEEPALIVE))
{
keepAlive();
}
else if (action.equals(ACTION_RECONNECT))
{
if (isNetworkAvailable())
{
reconnectIfNecessary();
}
}
}
return START_REDELIVER_INTENT;
}
/**
* Attempts connect to the Mqtt Broker
* and listen for Connectivity changes
* via ConnectivityManager.CONNECTVITIY_ACTION BroadcastReceiver
* 启动函数,内部使用
*/
private synchronized void start()
{
if (mStarted)
{
Log.i(DEBUG_TAG, "Attempt to start while already started");
return;
}
connect();
}
private class ConnectRunnable implements Runnable
{
private MqttService instanceForLock = null;
public void setInstanceForLock(MqttService instanceForLock)
{
this.instanceForLock = instanceForLock;
}
public void run()
{
synchronized (instanceForLock)
{
try
{
if (isConnected()) return;
mClient.connect(mOpts);
Log.i(DEBUG_TAG, "mClient.connect(mOpts)");
try
{
mClient.subscribe(String.format(Locale.US, mTopic, Imei), MQTT_TOPIC_SUBSCRIBE_QOS);
mClient.subscribe(String.format(Locale.US, mTopic2, Imei), MQTT_TOPIC_SUBSCRIBE_QOS);
mClient.setCallback(MqttService.this);
mStarted = true;
Log.i(DEBUG_TAG, "Successfully connected and subscribed starting keep alives");
mKeepAliveTopic = null;
reconnectTriedTimes = 0;
keepAliveRunTimesInReconnect = 0;
keepAliveTimes = 0;
}
catch (Exception e)
{
mKeepAliveTopic = null;
e.printStackTrace();
try
{
reconnectTriedTimes++;
mClient.disconnect();
}
catch (Exception ex)
{
}
}
}
catch (Exception e)
{
mClient = null;
mKeepAliveTopic = null;
reconnectTriedTimes++;
e.printStackTrace();
}
}
}
}
private ConnectRunnable crun = new ConnectRunnable();
/**
* Connects to the broker with the appropriate datastore
* 实际的连接函数,订阅topic消息,收到消息会在messageArrived函数回调
*/
private synchronized void connect()
{
String url = String.format(Locale.US, MQTT_URL_FORMAT, MQTT_BROKER, MQTT_PORT);
Log.i(DEBUG_TAG, "Connecting with URL: " + url);
try
{
if (mDataStore != null)
{
Log.i(DEBUG_TAG, "Connecting with DataStore");
mClient = new MqttClient(url, mDeviceId, mDataStore);
}
else
{
Log.i(DEBUG_TAG, "Connecting with MemStore");
mClient = new MqttClient(url, mDeviceId, mMemStore);
}
}
catch (MqttException e)
{
e.printStackTrace();
return;
}
catch (Exception e)
{
e.printStackTrace();
return;
}
mConnHandler.post(crun);
}
/**
* Schedules keep alives via a PendingIntent
* in the Alarm Manager
* 启动“保持连接”
*/
private void startKeepAlives()
{
Intent i = new Intent();
i.setClass(this, MqttService.class);
i.setAction(ACTION_KEEPALIVE);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + MQTT_KEEP_ALIVE, MQTT_KEEP_ALIVE, pi);
}
/**
* Cancels the Pending Intent
* in the alarm manager
*/
private void stopKeepAlives()
{
Intent i = new Intent();
i.setClass(this, MqttService.class);
i.setAction(ACTION_KEEPALIVE);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
mAlarmManager.cancel(pi);
}
/**
* 通过线程循环发送广播,广播响应时调用keepalive
*/
public void registerBoradcastReceiver()
{
IntentFilter myIntentFilter = new IntentFilter();
myIntentFilter.addAction(mKeepalivealarm);
//注册广播
registerReceiver(mBroadcastReceiver, myIntentFilter);
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
String action = intent.getAction();
if (action.equals(mKeepalivealarm))
{
keepAlive();
}
}
};
/**
* Publishes a KeepALive to the topic
* in the broker
* 重连,失败后重新连接,zeeeitch ;增加keepalive的功能,没有连接,那么在keepalive实际做的就是连接;永远不会停止keepalive
*/
private synchronized void keepAlive()
{
// Log.i(DEBUG_TAG, "entering keepAlive()");
// if (reconnectTriedTimes == 0) keepAliveRunTimesInReconnect = 0;
Log.i(DEBUG_TAG, "lastKeepAliveRun " + lastKeepAliveRun);
Log.i(DEBUG_TAG, "mKeepRecMultipler " + mKeepRecMultipler);
Log.i(DEBUG_TAG, "keepAliveRunTimesInReconnect " + keepAliveRunTimesInReconnect);
Log.i(DEBUG_TAG, "keepAliveTimes " + keepAliveTimes);
keepAliveTimes++;
keepAliveTimes = (keepAliveTimes >= 0 ? (keepAliveTimes % mKeepRecMultipler) : keepAliveTimes);
//小米手机,alarm间隔不能少于5分钟,如果发现,取消掉mKeepRecMultipler机制
if (System.currentTimeMillis() - lastKeepAliveRun > mKeepAliveInterval * 1000) keepAliveTimes = 0;
lastKeepAliveRun = System.currentTimeMillis();
boolean noErrOccurred = true;
if (isConnected())
{
if (keepAliveTimes == 0)
{
try
{
sendKeepAlive();
}
catch (MqttConnectivityException ex)
{
ex.printStackTrace();
noErrOccurred = false;
}
catch (MqttPersistenceException ex)
{
ex.printStackTrace();
noErrOccurred = false;
}
catch (Exception ex)
{
ex.printStackTrace();
noErrOccurred = false;
}
}
}
else noErrOccurred = false;
if (!noErrOccurred)
{
mKeepAliveTopic = null;
keepAliveRunTimesInReconnect++;
Log.i(DEBUG_TAG, "keepAliveRunTimesInReconnect ++ ");
//做到在网络很差的时候,重连间隔越来越长 , =x*x/9,二次函数 ,但最长也不超过maxReconnectMultiple
double nextTry = 1.0 * reconnectTriedTimes * reconnectTriedTimes / 9.0;
long nextTry2 = Math.min(Math.max(1, Math.round(nextTry)), maxReconnectMultipler);
if (keepAliveTimes >= 0 && keepAliveRunTimesInReconnect / nextTry2 >= 1)
{
connect();
keepAliveRunTimesInReconnect = 0;
}
}
}
/**
* Checkes the current connectivity
* and reconnects if it is required.
* 实际的重连函数
*/
private synchronized void reconnectIfNecessary()
{
if (mStarted && !isConnected())
{
connect();
}
}
/**
* Query's the NetworkInfo via ConnectivityManager
* to return the current connected state
*
* @return boolean true if we are connected false otherwise
*/
private boolean isNetworkAvailable()
{
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
return (info == null) ? false : info.isConnected();
}
/**
* Verifies the client State with our local connected state
*
* @return true if its a match we are connected false if we aren't connected
*/
private boolean isConnected()
{
if (mStarted && mClient != null && !mClient.isConnected())
{
Log.w(DEBUG_TAG, "Mismatch between what we think is connected and what is connected");
}
if (mClient != null)
{
if ((mStarted && mClient.isConnected()))
{
return true;
}
else return false;
}
return false;
}
/**
* Receiver that listens for connectivity chanes
* via ConnectivityManager
*/
private final BroadcastReceiver mConnectivityReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
Log.i(DEBUG_TAG, "Connectivity Changed...");
if (isNetworkAvailable())
{
reconnectIfNecessary();
}
}
};
/**
* Sends a Keep Alive message to the specified topic
* 每隔MQTT_KEEP_ALIVE时间发送保持连接数据包,发送内容MQTT_KEEP_ALIVE_MESSAGE,数据量少
*
* @return MqttDeliveryToken specified token you can choose to wait for completion
*/
private synchronized MqttDeliveryToken sendKeepAlive() throws MqttConnectivityException, MqttPersistenceException, MqttException
{
if (!isConnected()) throw new MqttConnectivityException();
if (mKeepAliveTopic == null)
{
mKeepAliveTopic = mClient.getTopic(String.format(Locale.US, MQTT_KEEP_ALIVE_TOPIC_FORAMT, mDeviceId));
}
Log.i(DEBUG_TAG, "Sending Keepalive to " + MQTT_BROKER);
MqttMessage message = new MqttMessage(MQTT_KEEP_ALIVE_MESSAGE);
message.setQos(MQTT_KEEP_ALIVE_QOS);
return mKeepAliveTopic.publish(message);
}
@Override
public IBinder onBind(Intent arg0)
{
return null;
}
/**
* Connectivity Lost from broker
* 连接丢失的回调函数
*/
public synchronized void connectionLost(Throwable arg0)
{
Log.i(DEBUG_TAG, "connectionLost()");
mClient = null;
mKeepAliveTopic = null;
if (isNetworkAvailable())
{
reconnectIfNecessary();
}
}
/**
* Publish Message Completion
* 发送数据完成的回调函数,本应用没有使用发送功能
*/
public void deliveryComplete(MqttDeliveryToken arg0)
{
}
//收到推送后处理具体业务消息
private void checkNewDataFor3Apps()
{
// Log.i("", "run service");
SIMCardInfo sim = new SIMCardInfo(MqttService.this);
if (sim.getIMEI() != null && sim.getIMEI().length() > 5)
{
String urlStr = HttpUtil.BASE_URL + "jxgb/xxxx?imei=" + sim.getIMEI();
// 先看看有没有新数据
String webRespStatus = HttpUtil.queryStringForPost(urlStr);
if (!(DataUtil.getString(webRespStatus, "hasNew") == null || !DataUtil.getString(webRespStatus, "hasNew").equals("1")))
{
urlStr = HttpUtil.BASE_URL + "jxgb/xxxx?imei=" + sim.getIMEI();
// 查询返回结果
String webResp = HttpUtil.queryStringForPost(urlStr);
if (!(DataUtil.getString(webResp, "nodata") != null && DataUtil.getString(webResp, "nodata").equals("1")))
{
// Log.i("","havedata");
int count = DataUtil.getInt(webResp, "count");
int i;
for (i = 0; i < count; i++)
{
if (DataUtil.getInt(webResp, "read" + i).equals(0) && !DataUtil.getInt(webResp, "type" + i).equals(3)) //通知
{
Log.i("", "send notify");
Notification messageNotification = new Notification(R.drawable.ic_launcher, "aaa", System.currentTimeMillis());
messageNotification.tickerText = "有新通知";
// messageNotification.defaults = Notification.DEFAULT_SOUND;
//启动时接收,用getident
Intent notificationIntent;
notificationIntent = new Intent(mContext, MyActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
CharSequence contentTitle = "有新通知";
CharSequence contentText = DataUtil.getString(webResp, "msg" + i); // 通知栏内容
messageNotification.setLatestEventInfo(mContext, contentTitle, contentText, contentIntent);
messageNotification.flags |= Notification.FLAG_AUTO_CANCEL;
messageNotification.sound = Uri.parse("android.resource://com.zeeeitch.healthinfo_tongzhi/raw/notification");// 播放自定义的铃声
NotificationManager messageNotificatioManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
messageNotificatioManager.notify(0, messageNotification);
//标记到服务器:已读
urlStr = HttpUtil.BASE_URL + "jxgb/xxxxx2?id=" + DataUtil.getLong(webResp, "id" + i);
HttpUtil.queryStringForPost(urlStr);
}
}
}
}
//其他具体业务应用:
// 看看"超范围"有没有新数据
if (DataUtil.getString(webRespStatus, "hasNew3") == null || !DataUtil.getString(webRespStatus, "hasNew3").equals("1"))
{;}
else
{
Intent mIntent = new Intent(Boradcas_action_tongzhi);
//发送广播
sendBroadcast(mIntent);
Log.i("","sending brocast");
}
// 看看"工资"有没有新数据
if (DataUtil.getString(webRespStatus, "hasNewSalary") == null || !DataUtil.getString(webRespStatus, "hasNewSalary").equals("1"))
{;}
else
{
Intent mIntent = new Intent(Boradcas_action_gongzi);
//发送广播
sendBroadcast(mIntent);
Log.i("","sending brocast");
}
}
}
/**
* 收到有通知未读的,重复检测发来的,直接发Notify
*/
private void notifyForRepeat()
{
Notification messageNotification = new Notification(R.drawable.ic_launcher, "aaa", System.currentTimeMillis());
messageNotification.tickerText = "有新通知";
// messageNotification.defaults = Notification.DEFAULT_SOUND;
//启动时接收,用getident
Intent notificationIntent;
notificationIntent = new Intent(mContext, MyActivity.class);
PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
CharSequence contentTitle = "有新通知";
// CharSequence contentText = DataUtil.getString(webResp, "msg" + i); // 通知栏内容
messageNotification.setLatestEventInfo(mContext, contentTitle, "有未读的新通知", contentIntent);
messageNotification.flags |= Notification.FLAG_AUTO_CANCEL;
// messageNotification.sound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); // 系统默认铃声
messageNotification.sound = Uri.parse("android.resource://com.zeeeitch.healthinfo_tongzhi/raw/notification");// 播放自定义的铃声
// messageNotification.sound = Uri.parse("file:///android_assets/com.zeeeitch.healthinfo_tongzhi/raw/notification.wav");// 播放自定义的铃声
NotificationManager messageNotificatioManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
messageNotificatioManager.notify(0, messageNotification);
}
/**
* Received Message from broker
* 收到消息回调函数
*/
public void messageArrived(MqttTopic topic, MqttMessage message)
{
try
{
Log.i(DEBUG_TAG, " Topic:\t" + topic.getName() +
" Message:\t" + new String(message.getPayload()) +
" QoS:\t" + message.getQos());
// sendNotification(new String(message.getPayload()));
checkNewDataFor3Apps();
if(topic.getName().substring(0,23).equals("Jxs/formal/notifyrepeat"))
notifyForRepeat();
}
catch (Exception e)
{
// e.printStackTrace();
}
}
/**
* MqttConnectivityException Exception class
*/
private class MqttConnectivityException extends Exception
{
private static final long serialVersionUID = -7385866796799469420L;
}
}