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