基于AndroidPN搭建Android的推送平台
一. 什么是推送消息
自主Google脑补
二. AndroidPN的介绍
AndroidPN是Android平台上一个基于XMPP协议的即时推送消息开源项目
github: https://github.com/dannytiehui/androidpn
注意 - AndroidPN是一个还处Beta版且被搁置的开源项目
三. AndroidPN的Bug及修复
1. 客户端断线及重连
a. 解决方法 - 加入心跳机制
i. 什么是心跳机制
心跳机制就是客户端定时向服务器端发送消息,告诉服务器,'我'还在线还活着
那问题来了:为什么需要在即时通讯中需要加入心跳机制?
原因很简单,那是因为TCP/IP协议是单向通信的,只有当客户端向服务器发送请求-Request时,服务器端才会响应客户端,即Response
ii. 服务器端
1. 修改spring-config.xml文件,添加一个bean
<!-- 服务器端添加对客户端的心跳机制设置 --> <!-- 假设客户端每隔一个分钟向服务器发送一个心跳包,如果服务器在三分钟的时间段都没有收到客户端的心跳包,则认为该客户端处于离线状态 --> <!-- 多出来的10秒是为了让客户端第三次发送心跳包时有足够的时间发送和响应 --> <bean id="getSessionConfig" factory-bean="ioAcceptor" factory-method="getSessionConfig"> <property name="readerIdleTime" value="190"></property> </bean>
iii. 客户端
1. 修改org.jivesoftware.smack包下的PacketWriter.java文件,new出一个向服务器发送心跳数据包的线程,同时对外提供一个开启该线程的方法
/**
* 开启发送心跳数据包线程
*/
public void startHeartBeatThread() {
HearBeatThread hearBeatThread = new HearBeatThread();
hearBeatThread.start();
}
/**
* 向服务器发送心跳数据包的线程类
*/
class HearBeatThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
while (!done) {
try {
writer.write(" ");
writer.flush();
Log.d("TAG", "send heart beat once...");
Thread.sleep(60 * 1000);
} catch (Exception e) {
if (!(done || connection.isSocketClosed())) {
done = true;
// packetReader could be set to null by an concurrent
// disconnect() call.
// Therefore Prevent NPE exceptions by checking
// packetReader.
if (connection.packetReader != null) {
connection.notifyConnectionError(e);
}
}
}
}
}
}
2. 何时何地开启发送心跳数据包线程
1. 为org.jivesoftware.smack包下的XMPPConnection.java文件添加一个方法
/**
* 通过XMPPConnection持有的PacketWriter对象的引用对外提供一个开启该心跳机制线程的方法
*/
public void startHeartBean() {
if(packetWriter != null) {
packetWriter.startHeartBeatThread();
}
}
2. 修改org.androidpn.client包下的XMPPManager.java文件
private class LoginTask implements Runnable {
// ...
// 在XMPPManager轮询Task队列前启动心跳线程
connection.startHeartBean();
xmppManager.runTask();
// ...
}
iv. 客户端断线重连
1. 修改org.androidpn.client包下的ReconnectionThread.java的逻辑错误
public void run() {
try {
// 添加一个 且-&& 判断条件 -> 是否已经登录验证身份
// 如果不添加一个且判断,该run方法一直在执行,不符合重连后停止重连的逻辑
while (!isInterrupted() && !xmppManager.getConnection().isAuthenticated()) {
Log.d(LOGTAG, "Trying to reconnect in " + waiting()
+ " seconds");
Thread.sleep((long) waiting() * 1000L);
xmppManager.connect();
waiting++;
}
} catch (final InterruptedException e) {
xmppManager.getHandler().post(new Runnable() {
public void run() {
xmppManager.getConnectionListener().reconnectionFailed(e);
}
});
}
}
2. 修改org.androidpn.client包下的XMPPManager.java文件,通过重新new出一个ReconnectionThread对象,防止断线重连线程被start()多次的异常问题
public void startReconnectionThread() {
synchronized (reconnection) {
// 优化原因 - 一个线程不能被start两次,否则会抛出异常
// 添加一个 |-或 判断,判断 reconnection是否为空,若为空,则new出一个对象,并且进行重连
if (reconnection == null || !reconnection.isAlive()) {
reconnection = new ReconnectionThread(this);
reconnection.setName("Xmpp Reconnection Thread");
reconnection.start();
}
}
}
2. 任务执行机制缺陷及修复
a. 缺陷 - androidpn 采用的是单任务(任务组)队列执行机制,通过执行位于任务队列头的任务,再次调用runTask()方法来轮询队列;但是,androidpn的任务执行机制没有充分考虑到遇到异常后的任务队列调用的情况,仅仅是通过在理想状态下回调runTask(),最终导致如下情况:任务只添加没有被执行
b. Fix
i. 简单粗暴的处理
private void addTask(Runnable runnable) {
Log.d(LOGTAG, "addTask(runnable)...");
taskTracker.increase();
synchronized (taskList) {
if (taskList.isEmpty() && !running) {
running = true;
futureTask = taskSubmitter.submit(runnable);
if (futureTask == null) {
taskTracker.decrease();
}
} else {
// 解决服务器端重启后,客户端不能成功连接androidpn服务器
// 这种解决方法太粗暴,没有从本质上解决 单例任务 机制的执行问题
// 问题的原因是:客户端在请求连接时过于理想化,
// 认为在有网络的情况下,就可以与服务器端连接上,
// 然而请求不一定成功,导致没有执行remove()-Task,
// 导致任务队列只添加任务没有执行任务的尴尬情况
runTask();
}
}
Log.d(LOGTAG, "addTask(runnable)... done");
}
ii. 本质上解决
1. 在XMPPManager.java文件添加一个移除任务方法 - 假设连接失败,那么这个任务组中的注册和登录方法也就没有被执行的意义了,应当移除
/**
* 删除任务
*
* @param dropCount
*/
private void dropTask(int dropCount) {
synchronized (taskList) {
if (taskList.size() >= dropCount) {
for (int i = 0; i < dropCount; ++i) {
taskList.remove(0);
taskTracker.decrease();
}
}
}
}
2. 修XMPPManager中的ConnectTask连接线程
/**
* A runnable task to connect the server.
*/
private class ConnectTask implements Runnable {
final XmppManager xmppManager;
private ConnectTask() {
this.xmppManager = XmppManager.this;
}
public void run() {
Log.i(LOGTAG, "ConnectTask.run()...");
if (!xmppManager.isConnected()) {
// Create the configuration for this new connection
ConnectionConfiguration connConfig = new ConnectionConfiguration(
xmppHost, xmppPort);
// connConfig.setSecurityMode(SecurityMode.disabled);
connConfig.setSecurityMode(SecurityMode.required);
connConfig.setSASLAuthenticationEnabled(false);
connConfig.setCompressionEnabled(false);
XMPPConnection connection = new XMPPConnection(connConfig);
xmppManager.setConnection(connection);
try {
// Connect to the server
connection.connect();
Log.i(LOGTAG, "XMPP connected successfully");
// packet provider
ProviderManager.getInstance().addIQProvider("notification",
"androidpn:iq:notification",
new NotificationIQProvider());
// 2. 添加
xmppManager.runTask();
} catch (XMPPException e) {
Log.e(LOGTAG, "XMPP connection failed", e);
// 3.1. 连接异常,则将连接请求任务(登录、注册)drop掉
xmppManager.dropTask(2);
// 3.2. 保证任务队列继续执行
xmppManager.runTask();
// 3.3. 尝试重新进行连接
xmppManager.startReconnectionThread();
}
// 1. 错误的 - 注释掉
// xmppManager.runTask();
} else {
Log.i(LOGTAG, "XMPP connected already");
xmppManager.runTask();
}
}
}
3. 修XMPPManager中的RegisterTask连接线程
private class RegisterTask implements Runnable {
final XmppManager xmppManager;
// 添加一个注册任务是否成功标志位
boolean isRegisterSucceed = false;
// 添加一个是否丢弃任务标志位 - 由于网络延迟等情况,服务器端响应时间超过10s,防止响应监听中重新runTask
boolean hasDropTask = false;
private RegisterTask() {
xmppManager = XmppManager.this;
}
public void run() {
Log.i(LOGTAG, "RegisterTask.run()...");
if (!xmppManager.isRegistered()) {
final String newUsername = newRandomUUID();
final String newPassword = newRandomUUID();
Registration registration = new Registration();
PacketFilter packetFilter = new AndFilter(new PacketIDFilter(
registration.getPacketID()), new PacketTypeFilter(
IQ.class));
PacketListener packetListener = new PacketListener() {
public void processPacket(Packet packet) {
// 3
synchronized (xmppManager) {
Log.d("RegisterTask.PacketListener",
"processPacket().....");
Log.d("RegisterTask.PacketListener", "packet="
+ packet.toXML());
if (packet instanceof IQ) {
IQ response = (IQ) packet;
if (response.getType() == IQ.Type.ERROR) {
if (!response.getError().toString()
.contains("409")) {
Log.e(LOGTAG,
"Unknown error while registering XMPP account! "
+ response.getError()
.getCondition());
}
} else if (response.getType() == IQ.Type.RESULT) {
xmppManager.setUsername(newUsername);
xmppManager.setPassword(newPassword);
Log.d(LOGTAG, "username=" + newUsername);
Log.d(LOGTAG, "password=" + newPassword);
Editor editor = sharedPrefs.edit();
editor.putString(Constants.XMPP_USERNAME,
newUsername);
editor.putString(Constants.XMPP_PASSWORD,
newPassword);
editor.commit();
//4. 表明客户端注册成功,修改注册标志位
isRegisterSucceed = true;
Log.i(LOGTAG,
"Account registered successfully");
// 8
if (!hasDropTask) {
xmppManager.runTask();
}
}
}
}
}
};
connection.addPacketListener(packetListener, packetFilter);
registration.setType(IQ.Type.SET);
// registration.setTo(xmppHost);
// Map<String, String> attributes = new HashMap<String,
// String>();
// attributes.put("username", rUsername);
// attributes.put("password", rPassword);
// registration.setAttributes(attributes);
registration.addAttribute("username", newUsername);
registration.addAttribute("password", newPassword);
connection.sendPacket(registration);
// 1. 将线程休眠等待服务器响应
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 如果服务器没有响应,即表示本次请求错误
// 5. 同时要注意添加一个同步锁,防止出现服务器恰好在10s响应,而客户端却认为服务器没有响应,将接下来的注册任务drop掉
synchronized (xmppManager) {
// 2. 客户端认为10s内服务器没有响应,则重新执行注册请求任务
// 6
if (!isRegisterSucceed) {
xmppManager.dropTask(1);
xmppManager.runTask();
xmppManager.startReconnectionThread();
// 7. 防止再次dropTask
hasDropTask = true;
}
}
} else {
Log.i(LOGTAG, "Account registered already");
xmppManager.runTask();
}
}
}
4. 修XMPPManager中的LoginTask连接线程
private class LoginTask implements Runnable {
final XmppManager xmppManager;
private LoginTask() {
this.xmppManager = XmppManager.this;
}
public void run() {
Log.i(LOGTAG, "LoginTask.run()...");
if (!xmppManager.isAuthenticated()) {
Log.d(LOGTAG, "username=" + username);
Log.d(LOGTAG, "password=" + password);
try {
xmppManager.getConnection().login(
xmppManager.getUsername(),
xmppManager.getPassword(), XMPP_RESOURCE_NAME);
Log.d(LOGTAG, "Loggedn in successfully");
// connection listener
if (xmppManager.getConnectionListener() != null) {
xmppManager.getConnection().addConnectionListener(
xmppManager.getConnectionListener());
}
// packet filter
PacketFilter packetFilter = new PacketTypeFilter(
NotificationIQ.class);
// packet listener
PacketListener packetListener = xmppManager
.getNotificationPacketListener();
connection.addPacketListener(packetListener, packetFilter);
// 启动心跳线程
connection.startHeartBean();
// 调用地点不对 - 如果有Exception,代码将会跳转至catch语句,导致runTask()没有被执行,这将会破坏掉单任务队列的执行机制
// xmppManager.runTask();
} catch (XMPPException e) {
Log.e(LOGTAG, "LoginTask.run()... xmpp error");
Log.e(LOGTAG, "Failed to login to xmpp server. Caused by: "
+ e.getMessage());
String INVALID_CREDENTIALS_ERROR_CODE = "401";
String errorMessage = e.getMessage();
if (errorMessage != null
&& errorMessage
.contains(INVALID_CREDENTIALS_ERROR_CODE)) {
xmppManager.reregisterAccount();
return;
}
xmppManager.startReconnectionThread();
} catch (Exception e) {
Log.e(LOGTAG, "LoginTask.run()... other error");
Log.e(LOGTAG, "Failed to login to xmpp server. Caused by: "
+ e.getMessage());
xmppManager.startReconnectionThread();
} finally {
// 添加final后调用runTask()
xmppManager.runTask();
}
} else {
Log.i(LOGTAG, "Logged in already");
xmppManager.runTask();
}
}
}

浙公网安备 33010602011771号