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

	}
}

  

posted @ 2015-12-09 23:23  mattle  阅读(326)  评论(0编辑  收藏  举报