---------------------------------------------------(一)androidpn-server服务端启动过程的理解分析---------------------------------------------------
在Androidpn的底层主要采用的mina和openfire两大框架,其中mina主要为底层数据传输的Socket框架。下面简单说明mina的框架。
Apache Mina Server 是一个网络通信应用框架,也就是说,它主要是对基于TCP/IP、UDP/IP协议栈的通信框架(然,也可以提供JAVA 对象的序列化服务、虚拟机管道通信服务等),Mina 可以帮助我们快速开发高性能、高扩展性的网络通信应用,Mina 提供了事件驱动、异步(Mina 的异步IO 默认使用的是JAVA NIO 作为底层支持)操作的编程模型。Mina 主要有1.x 和2.x 两个分支,这里我们讲解最新版本2.0,如果你使用的是Mina 1.x,那么可能会有一些功能并不适用。学习本文档,需要你已掌握JAVA IO、JAVA NIO、JAVASocket、JAVA 线程及并发库(java.util.concurrent.*)的知识。Mina 同时提供了网络通信的Server 端、Client 端的封装,无论是哪端,Mina 在整个网通通信结构中都处于如下的位置:
可见Mina 的API 将真正的网络通信与我们的应用程序隔离开来,你只需要关心你要发送、接收的数据以及你的业务逻辑即可。同样的,无论是哪端,Mina 的执行流程如下所示:
(1.) IoService:这个接口在一个线程上负责套接字的建立,拥有自己的Selector,监听是否有连接被建立。
(2.) IoProcessor:这个接口在另一个线程上负责检查是否有数据在通道上读写,也就是说它也拥有自己的Selector,这是与我们使用JAVA NIO 编码时的一个不同之处,通常在JAVA NIO 编码中,我们都是使用一个Selector,也就是不区分IoService与IoProcessor 两个功能接口。另外,IoProcessor 负责调用注册在IoService 上的过滤器,并在过滤器链之后调用IoHandler。
(3.) IoFilter:这个接口定义一组拦截器,这些拦截器可以包括日志输出、黑名单过滤、数据的编码(write 方向)与解码(read 方向)等功能,其中数据的encode 与decode是最为重要的、也是你在使用Mina 时最主要关注的地方。
(4.) IoHandler:这个接口负责编写业务逻辑,也就是接收、发送数据的地方。
Mina自带的过滤器(部分)
由上面的Mina的执行的过程可以知道针对使用Mina的第三方来说主要的业务主要在IoHandler中。
androidpn-server的spring配置。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd"> <!-- =============================================================== --> <!-- Resources --> <!-- =============================================================== --> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:jdbc.properties</value> </list> </property> </bean> <!-- =============================================================== --> <!-- Data Source --> <!-- =============================================================== --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbcDriverClassName}" /> <property name="url" value="${jdbcUrl}" /> <property name="username" value="${jdbcUsername}" /> <property name="password" value="${jdbcPassword}" /> <property name="maxActive" value="${jdbcMaxActive}" /> <property name="maxIdle" value="${jdbcMaxIdle}" /> <property name="maxWait" value="${jdbcMaxWait}" /> <property name="defaultAutoCommit" value="true" /> </bean> <!-- =============================================================== --> <!-- Hibernate --> <!-- =============================================================== --> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:hibernate.cfg.xml" /> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <!-- =============================================================== --> <!-- Data Access Objects --> <!-- =============================================================== --> <bean id="userDao" class="org.androidpn.server.dao.hibernate.UserDaoHibernate"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <!-- =============================================================== --> <!-- Services --> <!-- =============================================================== --> <bean id="userService" class="org.androidpn.server.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao" /> </bean> <!-- =============================================================== --> <!-- SSL --> <!-- =============================================================== --> <!-- <bean id="tlsContextFactory" class="org.androidpn.server.ssl2.ResourceBasedTLSContextFactory"> <constructor-arg value="classpath:bogus_mina_tls.cert" /> <property name="password" value="boguspw" /> <property name="trustManagerFactory"> <bean class="org.androidpn.server.ssl2.BogusTrustManagerFactory" /> </property> </bean> --> <!-- =============================================================== --> <!-- MINA --> <!-- =============================================================== --> <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"> <property name="customEditors"> <map> <entry key="java.net.SocketAddress"> <bean class="org.apache.mina.integration.beans.InetSocketAddressEditor" /> </entry> </map> </property> </bean> <bean id="xmppHandler" class="org.androidpn.server.xmpp.net.XmppIoHandler" /> <bean id="filterChainBuilder" class="org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder"> <property name="filters"> <map> <entry key="executor"> <bean class="org.apache.mina.filter.executor.ExecutorFilter" /> </entry> <entry key="codec"> <bean class="org.apache.mina.filter.codec.ProtocolCodecFilter"> <constructor-arg> <bean class="org.androidpn.server.xmpp.codec.XmppCodecFactory" /> </constructor-arg> </bean> </entry> <!-- <entry key="logging"> <bean class="org.apache.mina.filter.logging.LoggingFilter" /> </entry> --> </map> </property> </bean> <bean id="ioAcceptor" class="org.apache.mina.transport.socket.nio.NioSocketAcceptor" init-method="bind" destroy-method="unbind"> <property name="defaultLocalAddress" value=":5222" /> <property name="handler" ref="xmppHandler" /> <property name="filterChainBuilder" ref="filterChainBuilder" /> <property name="reuseAddress" value="true" /> </bean> </beans>
通过Mina的原理我们研究androidpn的运行流程不能看出,如下:
androidpn-server执行过程如下:
1.spring初始化并启动过程,调用NioSocketAcceptor。
2。NioSocketAcceptor开始执行调用IoProcessor.
3.IoProcessor开始调用FilterChain。FilterChain调用相关的IoFilter的。其中ProtocolCodecFilter的过滤器调用了org.androidpn.server.xmpp.codec.XmppCodecFactory进行编码。
4.XmppIoHandler实现自IoHanlder并调用通过openfire 的XMLLightweightParser解析相关的业务逻辑。
5.根据解析的信息调用xmpp并处理相关信息。
---------------------------------------------------(二)androidpn-server服务端几个类说明---------------------------------------------------
AndroidPN(Android Push Notification) 是一个基于XMPP协议的Java开源推送通知实现,它包含了完整的客户端和服务端。
AndroidPN基于Openfire下的一些开源项目构建。
AndroidPN服务器包含两个部分,
一个是侦听在5222端口上的XMPP服务,负责与客户端的XMPPConnection类进行通信,作用是用户注册和身份认证,并发送推送通知消息。
另外一部分是Web服务器,采用一个轻量级的HTTP服务器,负责接收用户的Web请求。
最上层包含四个组成部分,分别是SessionManager,Auth Manager,PresenceManager以及Notification Manager。
SessionManager负责管理客户端与服务器之间的会话。
Auth Manager负责客户端用户认证管理。
Presence Manager负责管理客户端用户的登录状态。
NotificationManager负责实现服务器向客户端推送消息功能。
IQHandler消息处理器的类:
IQHandler:消息处理器抽象类。
IQAuthHandler:权限协议的消息处理类,消息的类型为:jabber:iq:auth
IQRegisterHandler:用户注册的消息处理类,消息类型为: jabber:iq:register
IQRosterHandler:用户消息交互类,消息类型为:jabber:iq:roster
PresenceUpdateHandler:用户状态展现变化处理类。内部调用,不具有类型。
NotificationManager源代码:
- public class NotificationManager {
-
- private static final String NOTIFICATION_NAMESPACE = "androidpn:iq:notification";
-
- private final Log log = LogFactory.getLog(getClass());
-
- private SessionManager sessionManager;
-
-
-
-
- public NotificationManager() {
- sessionManager = SessionManager.getInstance();
- }
-
-
-
-
-
-
-
-
-
- public void sendBroadcast(String apiKey, String title, String message,
- String uri) {
- log.debug("sendBroadcast()...");
- IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
- for (ClientSession session : sessionManager.getSessions()) {
- if (session.getPresence().isAvailable()) {
- notificationIQ.setTo(session.getAddress());
- session.deliver(notificationIQ);
- }
- }
- }
-
-
-
-
-
-
-
-
-
- public void sendNotifcationToUser(String apiKey, String username,
- String title, String message, String uri) {
- log.debug("sendNotifcationToUser()...");
- IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
- ClientSession session = sessionManager.getSession(username);
- if (session != null) {
- if (session.getPresence().isAvailable()) {
- notificationIQ.setTo(session.getAddress());
- session.deliver(notificationIQ);
- }
- }
- }
-
-
-
-
- private IQ createNotificationIQ(String apiKey, String title,
- String message, String uri) {
- Random random = new Random();
- String id = Integer.toHexString(random.nextInt());
-
-
- Element notification = DocumentHelper.createElement(QName.get(
- "notification", NOTIFICATION_NAMESPACE));
- notification.addElement("id").setText(id);
- notification.addElement("apiKey").setText(apiKey);
- notification.addElement("title").setText(title);
- notification.addElement("message").setText(message);
- notification.addElement("uri").setText(uri);
-
- IQ iq = new IQ();
- iq.setType(IQ.Type.set);
- iq.setChildElement(notification);
-
- return iq;
- }
- }
其中:
发布订阅式的发送消息调用方法:
/**
* Broadcasts a newly created notification message to all connected users.
*
* @param apiKey the API key
* @param title the title
* @param message the message details
* @param uri the uri
*/
public void sendBroadcast(String apiKey, String title, String message,
String uri);
点对点的发送消息调用方法:
/**
* Sends a newly created notification message to the specific user.
*
* @param apiKey the API key
* @param title the title
* @param message the message details
* @param uri the uri
*/
public void sendNotifcationToUser(String apiKey, String username,
String title, String message, String uri);
创建发送消息的方法:
/**
* Creates a new notification IQ and returns it.
*/
private IQ createNotificationIQ(String apiKey, String title,
String message, String uri);
---------------------------------------------------(三)androidpn-client客户端几个类说明---------------------------------------------------
在androidpn的客户端几个重要的类:
ServiceManager:管理消息服务和加载相关的配置。
ConnectivityReceiver:处理网络状态的广播。
NotificationReceiver:处理服务端发送的推送消息。
NotificationService:后台服务用户响应服务端的消息。需要在AndroidManifest.xml.注册。
NotificationSettingsActivity:推送信息设置页面。
PersistentConnectionListener:监控连接关闭和重连事件的监听。
PhoneStateChangeListener:监听手机状态的事件监听类。
ReconnectionThread:重连的线程类。
Notifier:客户端发送通知的类。
NotificationIQ:消息的数据包。
ServiceManager中获取属性信息的方法:
- private Properties loadProperties() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Properties props = new Properties();
- try {
- int id = context.getResources().getIdentifier("androidpn", "raw",
- context.getPackageName());
- props.load(context.getResources().openRawResource(id));
- } catch (Exception e) {
- Log.e(LOGTAG, "Could not find the properties file.", e);
-
- }
- return props;
- }
SharedPreferences的使用:
- sharedPrefs = context.getSharedPreferences(
- Constants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
- Editor editor = sharedPrefs.edit();
- editor.putString(Constants.API_KEY, apiKey);
- editor.putString(Constants.VERSION, version);
- editor.putString(Constants.XMPP_HOST, xmppHost);
- editor.putInt(Constants.XMPP_PORT, Integer.parseInt(xmppPort));
- editor.putString(Constants.CALLBACK_ACTIVITY_PACKAGE_NAME,
- callbackActivityPackageName);
- editor.putString(Constants.CALLBACK_ACTIVITY_CLASS_NAME,
- callbackActivityClassName);
- editor.commit();
获取手机的设备id:
- TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
-
-
-
-
- deviceId = telephonyManager.getDeviceId();
Notifier中发送通知的方法:
-
- Notification notification = new Notification();
- notification.icon = getNotificationIcon();
- notification.defaults = Notification.DEFAULT_LIGHTS;
- if (isNotificationSoundEnabled()) {
- notification.defaults |= Notification.DEFAULT_SOUND;
- }
- if (isNotificationVibrateEnabled()) {
- notification.defaults |= Notification.DEFAULT_VIBRATE;
- }
- notification.flags |= Notification.FLAG_AUTO_CANCEL;
- notification.when = System.currentTimeMillis();
- notification.tickerText = message;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Intent intent = new Intent(context,
- NotificationDetailsActivity.class);
- intent.putExtra(Constants.NOTIFICATION_ID, notificationId);
- intent.putExtra(Constants.NOTIFICATION_API_KEY, apiKey);
- intent.putExtra(Constants.NOTIFICATION_TITLE, title);
- intent.putExtra(Constants.NOTIFICATION_MESSAGE, message);
- intent.putExtra(Constants.NOTIFICATION_URI, uri);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- PendingIntent contentIntent = PendingIntent.getActivity(context, 0,
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- notification.setLatestEventInfo(context, title, message,
- contentIntent);
- notificationManager.notify(random.nextInt(), notification);
androidpn用户名和密码来源:
XmppManager的注册任务(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));
---------------------------------------------------(四)androidpn-client 常见BUG解决方法---------------------------------------------------
Androidpn用的比较广泛,但Androidpn还不成熟,存在一些BUG。
目前比较困扰大家的BUG主要有:
1.当服务端连续发送多条通知时,客户端都是显示同一条通知内容。
2.服务端重启,客户端也需要重启建立连接。
最后还有一个问题当我们服务器端重启的时候,客户端就无法在连接到服务器了,除非把android后台的服务
关掉,然后重启才行.在XmmpManager中加上如下红色代码就可:
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服务器
runTask();
taskList.add(runnable);
}
}
Log.d(LOGTAG, "addTask(runnable)... done");
}
由于没有经验,有BUG也只能继续GOOGLE,最终发现
http://jclick.iteye.com/blog/1289383
这个TOMCAT版本比较符合项目的需求。在此还是要感谢这位大神,真是造福广大人民群众。在这个版本中主要修复了第二个问题,相信很多朋友都已经看过了这篇BLOG ,但是大神在回复中写错了类名XMPPWriter,所有导致大家并不知道问题是怎么解决的, 其实是修改了org.androidpn.client.XmppManager,org.jivesoftware.smack.PacketWriter(smack源码)这2个类,这个将他2个版本的代码比较一下就可以看出来了,废话了那么多,在此说一下大神解决此问题的方法。代码在
http://phonepush.sinaapp.com/forum.php?mod=viewthread&tid=5&extra=page%3D1
篇帖子中已经贴出来了。在org.androidpn.client.XmppManager的LoginTask方法中加了一行代码getConnection().startKeepAliveThread(xmppManager);跟踪进去发现是开启了一个线程发送心跳,当发送失败时捕获异常,客户端每隔一段时间进行重连。
org.jivesoftware.smack.PacketWriter的run方法
- catch (SocketException e) {
- Log.e("PacketReader", e.toString());
- connection.disconnect();
- xmppManager.startReconnectionThread();
- } catch (IOException e) {
- e.printStackTrace();
- }
这样就达到了当与服务端失去连接时,客户端能够进行重新连接的效果。后来群里有朋友说在LoginTask方法中加入
getConnection().startKeepAliveThread(xmppManager); 编译就报错,那是因为他用的是第一个版本 ,所有请先下载第二个版本,第二个版本带大神精简过smack源码。 其实心跳机制在官方的asmack中就已经存在,并且在建立XmppConnection的时候就已经启动,但是遗憾的是asmack的开发人员并没有进行异常之后的重连
所有才出现这个困扰大家的问题。
然后是第二个问题,我们刚才下载的这个版本并没有处理这个BUG,其实很简单,群里的高手经解决,就是将org.androidpn.client.Notifier中的notify方法的
- PendingIntent contentIntent = PendingIntent.getActivity(context, 0,intent, PendingIntent.FLAG_UPDATE_CURRENT);
复制代码
改成
- PendingIntent contentIntent = PendingIntent.getActivity(context, random.nextInt(),
- intent, PendingIntent.FLAG_UPDATE_CURRENT);
复制代码
好了,这2个问题基本上就解决了,本人也只是在此将前辈们的经验写一下,方便大家集中修正BUG。其实在碰到框架BUG时,看看框架的源码还是有帮助,可能会有很多不解,但我觉得多多少少还是能看出一点东西来。以后大家碰到什么问题也可以提出来,大家一起研究讨论,集思广益,总比一个人瞎想的强,有什么好的想法也可以拿出来分享。再次谢谢各位前辈。
在网上androidpn上的BUG基本都解决了,也多亏牛人们顶力相助,灰常感谢啊。在这里要说的问题是手机锁屏后,客户端心跳包不再发送了。由于android也接触不是很久,对一些系统的机制不太了解,经过多次测试与分析,才发现了是由于锁屏后CPU处于睡眠状态,线程都被挂起,所以在服务器端设定的闲置时间内收不到心跳包,强制移除用户下线。
OK问题已经找到了就好办多了,既然是被挂起了我们就只有让心跳一直在跑了,不啰嗦了。既而在网上有找到两种方法,第一种是让系统不睡眠,第二种则是使用AlarmManager来做我们的操作,在这里我是用的第二种方案来解决我们的问题的。但这样可能有点费电,暂时只能这样解决了了,不知道大家有木有更好点的解决办法能说出来大家一起研究研究。
1).
-
- public void acquireWakeLock()
- {
- if (null == mWakeLock)
- {
- PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "NotificationService");
- if (null != mWakeLock)
- {
- mWakeLock.acquire();
- }
- }
- }
-
-
- public void releaseWakeLock()
- {
- if (null != mWakeLock)
- {
- mWakeLock.release();
- mWakeLock = null;
- }
- }
-
- public void acquireWakeLock()
- {
- if (null == mWakeLock)
- {
- PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "NotificationService");
- if (null != mWakeLock)
- {
- mWakeLock.acquire();
- }
- }
- }
-
-
- public void releaseWakeLock()
- {
- if (null != mWakeLock)
- {
- mWakeLock.release();
- mWakeLock = null;
- }
- }
2).
- public void registerAlarmManager(){
- am = (AlarmManager)getSystemService(ALARM_SERVICE);
- pi = PendingIntent.getBroadcast(this, 0, new Intent(this, HeartActionBroadCast.class), Intent.FLAG_ACTIVITY_NEW_TASK);
- long now = System.currentTimeMillis();
- am.setInexactRepeating(AlarmManager.RTC_WAKEUP, now, 20000, pi);
- }
---------------------------------------------------(五)Androidpn-server的Mina编码和解码解析过程---------------------------------------------------
在许多网络应用中可能针对传输的数据进行加密操作,接收到数据之后进行解码操作。
在mina中提供许多加密和解密的解析方式:
1.带一定前缀的字符串的解析方式。
2.序列化对象的字符串解析方式。
3.分隔符方式的字符串解析方式。
在mina中提供相关的filterchain支持相关的操作。
Mina的源代码如下:
- package org.apache.mina.filter.codec;
-
- import org.apache.mina.core.session.IoSession;
-
-
-
-
-
-
-
-
-
-
-
- public interface ProtocolCodecFactory {
-
-
-
-
- ProtocolEncoder getEncoder(IoSession session) throws Exception;
-
-
-
-
-
- ProtocolDecoder getDecoder(IoSession session) throws Exception;
- }
加密处理类:
- package org.apache.mina.filter.codec;
-
- import org.apache.mina.core.buffer.IoBuffer;
- import org.apache.mina.core.session.IoSession;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public interface ProtocolEncoder {
-
-
-
-
-
-
-
-
-
-
- void encode(IoSession session, Object message, ProtocolEncoderOutput out)
- throws Exception;
-
-
-
-
-
-
- void dispose(IoSession session) throws Exception;
- }
解密类如下:
- package org.apache.mina.filter.codec;
-
- import org.apache.mina.core.buffer.IoBuffer;
- import org.apache.mina.core.session.IoSession;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public interface ProtocolDecoder {
-
-
-
-
-
-
-
-
- void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out)
- throws Exception;
-
-
-
-
-
-
-
-
-
-
- void finishDecode(IoSession session, ProtocolDecoderOutput out)
- throws Exception;
-
-
-
-
-
-
- void dispose(IoSession session) throws Exception;
- }
在androidpn中的没有采取任何加密方式,但是提供相关的类org.androidpn.server.xmpp.codec,如果需要可以针对传输的数据进行加密和解密工作。
- package org.androidpn.server.xmpp.codec;
-
- import org.apache.mina.core.session.IoSession;
- import org.apache.mina.filter.codec.ProtocolCodecFactory;
- import org.apache.mina.filter.codec.ProtocolDecoder;
- import org.apache.mina.filter.codec.ProtocolEncoder;
-
-
-
-
-
-
- public class XmppCodecFactory implements ProtocolCodecFactory {
-
- private final XmppEncoder encoder;
-
- private final XmppDecoder decoder;
-
-
-
-
- public XmppCodecFactory() {
- encoder = new XmppEncoder();
- decoder = new XmppDecoder();
- }
-
-
-
-
- public ProtocolEncoder getEncoder(IoSession session) throws Exception {
- return encoder;
- }
-
-
-
-
- public ProtocolDecoder getDecoder(IoSession session) throws Exception {
- return decoder;
- }
-
- }
androidpn的解密类:
- package org.androidpn.server.xmpp.codec;
-
- import org.androidpn.server.xmpp.net.XmppIoHandler;
- import org.apache.mina.core.buffer.IoBuffer;
- import org.apache.mina.core.session.IoSession;
- import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
- import org.apache.mina.filter.codec.ProtocolDecoderOutput;
- import org.jivesoftware.openfire.nio.XMLLightweightParser;
-
-
-
-
-
-
- public class XmppDecoder extends CumulativeProtocolDecoder {
-
-
-
- @Override
- public boolean doDecode(IoSession session, IoBuffer in,
- ProtocolDecoderOutput out) throws Exception {
-
-
- XMLLightweightParser parser = (XMLLightweightParser) session
- .getAttribute(XmppIoHandler.XML_PARSER);
- parser.read(in);
-
- if (parser.areThereMsgs()) {
- for (String stanza : parser.getMsgs()) {
- out.write(stanza);
- }
- }
- return !in.hasRemaining();
- }
-
- }
androidpn的加密类:
- package org.androidpn.server.xmpp.codec;
-
- import org.apache.mina.core.session.IoSession;
- import org.apache.mina.filter.codec.ProtocolEncoder;
- import org.apache.mina.filter.codec.ProtocolEncoderOutput;
-
-
-
-
-
-
- public class XmppEncoder implements ProtocolEncoder {
-
-
-
- public void encode(IoSession session, Object message,
- ProtocolEncoderOutput out) throws Exception {
-
- }
-
- public void dispose(IoSession session) throws Exception {
-
- }
-
- }
最终将编码解析器转换为过滤器使用,具体配置如下:
- <bean class="org.apache.mina.filter.codec.ProtocolCodecFilter">
- <constructor-arg>
- <bean class="org.androidpn.server.xmpp.codec.XmppCodecFactory" />
- </constructor-arg>
- </bean>
---------------------(六)Androidpn-server的添加其他xmpp相关的协议(如查看好友列表等)----------------
曾经有一个同学,在网上问我,如果想androidpn添加额外的xmpp协议的方法在怎么加呢?我当时很迷惑,后来经过一翻仔细研究androidpn发现,其实每一种处理xmpp协议方法,必须有一个Handler实现。具体可以参考org.androidpn.server.xmpp.handler中。
针对每一个Handler对应的xml拥有不同的命名空间,每一个命名空间在xmpp中都有定义,因为传输的xml的格式是一定的。
例如:
IQAuthHandler:命名空间 String NAMESPACE = "jabber:iq:auth";
IQRegisterHandler:命名空间 String NAMESPACE = "jabber:iq:register";
IQRosterHandler:命名空间 String NAMESPACE = "jabber:iq:roster";
同时我们从Handler的实现可以看到每一个handler最好实现对应IQHandler类,但是为辅助类型的Handler那么可以不用,例如androidpn的状态更新处理器类PresenceUpdateHandler,不必要发送相关的消息到客户端。
所以如果要实现xmpp中如查看好友,用户分组等通信协议,那么你可能要实现相关的Handler并使用xmpp协议规定的相关的命名空间。
在androidpn中主要的业务处理类XmppIoHandler可以看出最终消息解析之后分发到IQRouter中。IQRouter用于处理消息的响应的消息。
IQRouter的源代码如下:
由以上的源代码可以看出,IQRouter在加载时候将各种处理器添加到回话管理器中,当消息分发到IQRouter中时候,根据命名空间的不同使用不同的处理处置即可。
---------------------------------------------------(七)androidpn 中业务类XmppIoHandler实现分析---------------------------------------------------
在androidpn中主要采用Mina进行网络通讯,其中Mina中IoHandler用来处理主要的业务逻辑。
Mina 中源代码如下:
- package org.apache.mina.core.service;
-
- import java.io.IOException;
-
- import org.apache.mina.core.session.IdleStatus;
- import org.apache.mina.core.session.IoSession;
-
-
-
-
-
-
-
-
- public interface IoHandler {
-
-
-
-
-
-
-
- void sessionCreated(IoSession session) throws Exception;
-
-
-
-
-
-
-
- void sessionOpened(IoSession session) throws Exception;
-
-
-
-
- void sessionClosed(IoSession session) throws Exception;
-
-
-
-
-
-
- void sessionIdle(IoSession session, IdleStatus status) throws Exception;
-
-
-
-
-
-
- void exceptionCaught(IoSession session, Throwable cause) throws Exception;
-
-
-
-
- void messageReceived(IoSession session, Object message) throws Exception;
-
-
-
-
-
- void messageSent(IoSession session, Object message) throws Exception;
- }
Mina中IoHandler可供处理的事件回调
sessionCreate(IoSession)
IoSession对象被创建时的回调,一般用于进行会话初始化操作。注意:与sessionOpened(IoSession)不同,IoSession对象的创建并不意味着对应底层TCP连接的建立,而仅仅代表字面意思:一个IoSession对象被创建出来了。
sessionOpened(IoSession)
IoSession对象被打开时回调。在TCP中,该事件是在TCP连接建立时触发,一般可用于发起连接建立的握手、认证等操作。
sessionIdle(IoSession,IdleStatus)
IoSession对象超时时回调。当一个IoSession对象在指定的超时时常内没有读写事件发生,就会触发该事件,一般可用于通知服务器断开长时间闲置的连接等处理。具体的超时设置可由 IoService.setWriteIdleTime(int) ,IoService.setReadIdleTime(int) ,IoService.setBothIdleTime(int)设置。
messageReceived(IoSession,Object)
当接收到IoSession对Client发送的数据时回调。
messageSent(IoSession,Object)
当发送给IoSession对Client的数据发送成功时回调。
exceptionCaught(IoSession,Throwable)
当会话过程中出现异常时回调,通常用于错误处理。
session.write(Object)方法是一个异步方法,对该方法的调用并不会阻塞,而是向Mina投递一个异步的写操作,并返回一个可用于对已投递异步写操作进行控制的WriteFuture对象。例如:调用WriteFuture的await()或awaitUninterruptibly(),可由同步等待该异步操作的完成。
在I/O处理器中实现业务逻辑的时候,对于简单的情况,一般只需要在messageReceived中对传入的消息进行处理。如果需要写回数据到对等体,用IoSession.write()即可。
另外的情况,client和server的通信协议比较复杂,client是有状态变迁的,这时可用Mina提供的状态机实现,可使用IO处理器的实现更加简单。
androidpn中XmppIoHandler源代码:
- package org.androidpn.server.xmpp.net;
-
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
-
- import org.androidpn.server.xmpp.XmppServer;
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.apache.mina.core.service.IoHandler;
- import org.apache.mina.core.session.IdleStatus;
- import org.apache.mina.core.session.IoSession;
- import org.dom4j.io.XMPPPacketReader;
- import org.jivesoftware.openfire.net.MXParser;
- import org.jivesoftware.openfire.nio.XMLLightweightParser;
- import org.xmlpull.v1.XmlPullParserException;
- import org.xmlpull.v1.XmlPullParserFactory;
-
-
-
-
-
-
-
- public class XmppIoHandler implements IoHandler {
-
- private static final Log log = LogFactory.getLog(XmppIoHandler.class);
-
- public static final String XML_PARSER = "XML_PARSER";
-
- private static final String CONNECTION = "CONNECTION";
-
- private static final String STANZA_HANDLER = "STANZA_HANDLER";
-
- private String serverName;
-
- private static Map<Integer, XMPPPacketReader> parsers = new ConcurrentHashMap<Integer, XMPPPacketReader>();
-
- private static XmlPullParserFactory factory = null;
-
- static {
- try {
- factory = XmlPullParserFactory.newInstance(
- MXParser.class.getName(), null);
- factory.setNamespaceAware(true);
- } catch (XmlPullParserException e) {
- log.error("Error creating a parser factory", e);
- }
- }
-
-
-
-
- protected XmppIoHandler() {
- serverName = XmppServer.getInstance().getServerName();
- }
-
-
-
-
- public void sessionCreated(IoSession session) throws Exception {
- log.debug("sessionCreated()...");
- }
-
-
-
-
- public void sessionOpened(IoSession session) throws Exception {
- log.debug("sessionOpened()...");
- log.debug("remoteAddress=" + session.getRemoteAddress());
-
- XMLLightweightParser parser = new XMLLightweightParser("UTF-8");
- session.setAttribute(XML_PARSER, parser);
-
- Connection connection = new Connection(session);
- session.setAttribute(CONNECTION, connection);
- session.setAttribute(STANZA_HANDLER, new StanzaHandler(serverName,
- connection));
- }
-
-
-
-
- public void sessionClosed(IoSession session) throws Exception {
- log.debug("sessionClosed()...");
- Connection connection = (Connection) session.getAttribute(CONNECTION);
- connection.close();
- }
-
-
-
-
- public void sessionIdle(IoSession session, IdleStatus status)
- throws Exception {
- log.debug("sessionIdle()...");
- Connection connection = (Connection) session.getAttribute(CONNECTION);
- if (log.isDebugEnabled()) {
- log.debug("Closing connection that has been idle: " + connection);
- }
- connection.close();
- }
-
-
-
-
- public void exceptionCaught(IoSession session, Throwable cause)
- throws Exception {
- log.debug("exceptionCaught()...");
- log.error(cause);
- }
-
-
-
-
- public void messageReceived(IoSession session, Object message)
- throws Exception {
- log.debug("messageReceived()...");
- log.debug("RCVD: " + message);
-
-
- StanzaHandler handler = (StanzaHandler) session
- .getAttribute(STANZA_HANDLER);
-
-
- int hashCode = Thread.currentThread().hashCode();
- XMPPPacketReader parser = parsers.get(hashCode);
- if (parser == null) {
- parser = new XMPPPacketReader();
- parser.setXPPFactory(factory);
- parsers.put(hashCode, parser);
- }
-
-
- try {
- handler.process((String) message, parser);
- } catch (Exception e) {
- log.error(
- "Closing connection due to error while processing message: "
- + message, e);
- Connection connection = (Connection) session
- .getAttribute(CONNECTION);
- connection.close();
- }
- }
-
-
-
-
- public void messageSent(IoSession session, Object message) throws Exception {
- log.debug("messageSent()...");
- }
-
- }
XmppIoHandler在加载的时候创建相关的xml解析工厂。
sessionOpened:在连接打开时候创建相关的xml的解析器和Handler处理器。
sessionClosed:关闭相关的连接。
sessionIdle:关闭相关的连接。
messageReceived:获取相关的xml解析器和handler处理器处理相关的消息。
最近正在做一个项目,要用到Android的Push技术。目前对于Android的推送技术,用的比较多的还是AndroidPn。由于要对Push的服务器端,进行压力测试。当然,不可能真找几千台手机来测试。所以只能通过PC端模拟AndroidPN的用户端,每个线程代表一个AndroidPN的客户端。
闲话少说,要想在PC端模拟AndroidPN的客户端,不了解
源码是不行的。
Google一下,大致可以找到相关源码的解析。本文是在相关基础上,添加些自己的见解。
Androidpn包含有server和client两个包,server部分可以作为服务器单独运行,也可以嵌入到web项目的servlet中,在tomcat环境中与web项目的其他部分交互。
Server部分的主要包结构如下:
其中org.androidpn.server.dao,org.androidpn.server.model和org.androidpn.server.service为使用hibernate链接
数据库并实现简单的用户登录认证,开发中可以用我们自己的认证模块替换。剩下的包就是推送的主体实现。
接下来逐个包来看:
1.util包中的类用来加载resources中的配置文件,在配置文件中可指定监听端口和ssl证书目录等属性。
2.org.androidpn.server.xmpp包里面定义了一些异常类型,主要是包含有入口类XmppServer,这个类用来启动和停止server程序。
3.org.androidpn.server.xmpp.auth包里面是认证的一些类,我们自己的认证模块可以在这里与androidpn进行结合。
4.org.androidpn.server.xmpp.codec是XMPP协议的XML文件解析包,server收到和发送的消息都要通过这个包来进行xmpp协议编码和解码。
5.org.androidpn.server.xmpp.handler包主要是对消息的处理,我们可以针对不同的消息类型定义自己的handler,
6.org.androidpn.server.xmpp.net包负责维护与client之间的持久连接,并实现了一些传输方式供发送xmpp消息时使用。
7.org.androidpn.server.xmpp.presence里面只包含PresenceManager类,用来维护client的在线状态。
8.org.androidpn.server.xmpp.push包里面的NotificationManager类包含有向client发送消息的接口。
9.org.androidpn.server.xmpp.router包负责将收到的信息包发送到相应的handler进行处理,是一个路由包。
10.org.androidpn.server.xmpp.session包定义了用来表示持久链接的session,每个session包含一条连接的状态信息。
11.org.androidpn.server.xmpp.ssl是对连接进行ssl认证的工具包。(目前服务器使用的是NonSASLAuthentication认证)
跟XMPP协议有关的类:
IQ Presence Message 分别表示XMPP中的<iq>节,<presence>节,<message>节
Packet表示XMPP节的抽象类,IQ Presence Message均是Packet的子类
Element表示XML节中的元素
如果对XMPP协议不太了解,可以先大致了解下XMPP协议中各种xml节的含义。
server发送消息的整个流程主要是:
1. NotificationManager的push接口被调用。
2.使用SessionManager在当前session集合中查找相应的client链接。
3.定义自己的XMPP消息格式并组装。
4.通过相应session,向client发送消息。
在这个流程中我们需要修改的是步骤3,也就是需要定义和组装自己的xmpp消息,以便于将适当的信息传到客户端并便于客户端解析。一个简单的消息组装例子如下:
1: private IQ createCustomizeIQ(String apiKey, String title,
2: String message, String uri) {
3: Random random = new Random();
4: String id = Integer.toHexString(random.nextInt());
5: // String id = String.valueOf(System.currentTimeMillis());
6:
7: Element notification = DocumentHelper.createElement(QName.get(
8: "notification", NOTIFICATION_NAMESPACE));
9: notification.addElement("id").setText(id);
10: notification.addElement("title").setText(title);
11: notification.addElement("message").setText(message);
12: notification.addElement("uri").setText(uri);
13: //自定义IQ的属性
14: notification.addElement("属性名").setText(属性);
15: IQ iq = new IQ();
16: iq.setType(IQ.Type.set);
17: iq.setChildElement(notification);
18:
19: return iq;
20: }
要注意的是在创建element的时候,传入的namespace要和client解析使用的namespace相匹配。
server端接收和处理消息的流程是:
1.connection收到packet,使用tsc.push.server.xmpp.codec解码。
2.router根据packet的namespace等信息,将packet路由到相应的handler。
3.handler进行处理。
相应的router和handler类在androidpn中都有例子可以参考,这里就不贴代码了。开发中只要根据client发送消息的格式,定义自己的router和handler类,然后在PacketRouter中注册router,在IQHandler中注册handler即可。
补充:
PacketRouter注册router的具体步骤
1.即在PacketRouter中添加成员变量,类型为自己定义的XXXRouter(自定义消息
路由器)
2.在PackRouter的构造函数里初始化XXXRouter
3.根据不同的消息类型调用相应的路由器
4.在PacketRouter中添加XXXRouter的路由方法route(XXX xxx)
注:XXX(自定义XMPP节)是Packet的子类
IQHandler注册handler的具体步骤
IQHandler处理信息<iq>节的抽象类,注册IQHandler就是继承IQHandler,重写其中的handleIQ(IQ)方法返回
应答的<iq>节.
IQHandler的process(IQ)即是处理各种IQ,在实际过程中是IQHandler handler = new IQXXXHandler()。在调用
handler.process()就会调用子类的handleIQ(IQ)方法
Client部分的主要包结构如下:
Client这边包含有消息的收发,解析以及持久连接的发起,重连等功能呢,十分强大,我们开发时完全不用管底层的连接,也不用担心断线,可以专注于业务部分的开发。
同时,代码结构也很简单。去除android的Service和BroadCast类以及一些工具类和常量类不谈:
1.NotificationIQ,NotificationIQProvider,NotificationPacketListener三个类负责对收到的Notification格式的消息进行解析和处理,
2.XmppManager是主控制器,NotificationService通过这个类,在后台维护androidpn连接。
3.PersistentConnectionListener,PhoneStateChangeListener,ReconnectionThread.java三个类则负责监听手机的状态并进行断线重连。
我们自定义消息时需要定义3个类:在***IQ中定义消息的实体,在***IQProvider中将消息转化为***IQ实体,在***PacketListener中对实体进行处理,具体的实现可参考NotificationIQ,NotificationIQProvider,NotificationPacketListener三个类。在定义这些类之后,还需要在XmppManager中将这3个类中注册到connection中,代码如下:
//ConnectTask类在XmppManager类里
1: Log.i(LOGTAG, "XMPP connected successfully");
2: // packet provider
3: ProviderManager.getInstance().addIQProvider("notification",
4: "androidpn:iq:notification",new NotificationIQProvider());
//LoginTask类在XmppManager类里
1: // packet filter
2: PacketFilter packetFilter = new PacketTypeFilter(NotificationIQ.class);
3: // packet listener
4: PacketListener packetListener = xmppManager.getNotificationPacketListener();
5: connection.addPacketListener(packetListener, packetFilter);
需要注意的是,注册***IQProvider时,传入的namespace需要和服务端组装消息时使用的namespace一致,才能正确的收到。
以上红色部分,是自己在看这篇文章时,所添加上去的,算是对这篇文章的一点补充…如果不对的地方,请大家指正。