Android端服务器推送技术原理分析及XMPP简单的使用(转)
xmpp协议起源于著名的Linux即时通讯服务服务器jabber,有时候我们会把xmpp协议也叫jabber协议,其实这是不规范的,xmpp是个协议,而jabber是个服务器,因为jabber开源,设计精良,安全,稳定,跨语言,跨平台,封装开发简便,越来越多人开始使用它,并且逐步完善,不久它便形成了一个强大的标准化体系,Google GTalk、Pidgin、PSI、Spark、Pandion、MSN、Yahoo、ICQ..诸如此类一些软件在这个强大的标准体系下实现了互联.那么XMPP到底是什么意思,用通俗的话讲它和基于xml格式的一些协议原理差不多,只不过是个针对服务器的软件协议罢了。
那么在java领域是否存在一个类似jabber那么强大开源稳定的也完美支持xmpp协议的服务器呢?答案有的,那便是openfire,openfire是纯java开发的基于XMPP的协议,目前最终版本锁定在了2011年openfire 3.7,它一共有linux windows mac 三个版本,安装也非常简单,openfire这个服务器是个开放式的平台,它内部集成的服务包括即时通讯服务,会议室服务,用户安全验证和管理服务,搜索服务,组织机构服务,会话服务,这几大服务都有相应的管理类和对外接口,它的二次开发和扩展都是在插件基础上直接嫁接进去的,早期有很多第三方为他做了插件,有语音服务,red5视频服务,邮件服务等等,语音和视频在openfire上一直是个鸡肋,没有非常好的解决方案,而做这些插件的大部分都停止更新,大家如果选用openfire做视频和语音还要慎重!抛开这些插件,openfire在IM及时通讯上还是相当强大稳定的,不少公司拿它来做二次开发!但即便如此openfire的二次开发成本还是比较高昂的,笔者曾经成功费了九牛二虎之力将源码环境搭建起来,并成功将它与我们JAVAEE 经典架构SSH成功组装,用openfire的桌面客户端spark软件和android开源xmpp客户端Beam软件,web端聊天软件Claros Chat享受了一把在自己服务器上“随时随地聊天”,不过这些都是实验阶段,距离成熟可用还很远!研究技术可以这么勾兑尝试,真的给人用可不能这么随意,我们还是要挖掘真正对我们有用的价值!
有关openfire的知识点大家可以参考我的另一篇文章http://blog.csdn.net/shimiso/article/details/8816558
openfire过于庞大繁复,许多对我们来说都是没什么用的,甚至要砍掉改造,能不能有精简的xmpp服务器呢?答案是有的,androidpn,笔者认真比对过openfire和androidpn的源码,最后惊奇的发现,原来它就是从openfire里面庖丁解牛出来的一部分,做这件事的人非常的了不起,为我们省了很大力气,在此感谢他的开源和共享精神,那么androidpn分离出来的是消息推送服务,简言之就是从服务端向android客户端推送消息的服务,因为openfire的源码架构是在jetty基础上建立的,它的启动和部署方式和我们传统的服务器tomcat和weblogic等有点区别,所以androidpn也有jetty的影子,在和我们传统架构组合的时候还要再把它和jetty拆开, androidpn的搭建和使用网上的教程很多,大家可以发现大部分千篇一律,出现一个OK界面就没了,堂而皇之的写上原创,有的只是改了下hello world,如此糊弄,实在难为所用!
androidpn消息推送采用的是apache的mina框架做的,服务端和客户端两边都有监听,也就是我们所说的socket编程,有人说socket编程有什么难的,就那么回事,其实不然,我们平时写的socket聊天都只是在局域网的,但是要穿透路由和防火墙,让信息安全及时的传送到另一个网关的局域网电脑中,就不是一件简单的活了,其中涉及到在nat上打洞,还有线程,断网重连,安全加密等等,那么androidpn配合mina相当于把这些活都干了,那么我们要的干活就相对比较精细了,第一学习mina的安装配置的规则,第二学习xmpp协议组装和解析的规则,第三学习androidpn推和收消息的核心代码,如此三点我们便能灵活驾驭住androidpn出现再大的问题自己也能动手去调了。
在和spring整合的时候大家要注意不要让mina服务启动2次,笔者整合时候无意发现在linux64位系统,weblogic上启动时候总是报5222已经被占用,反复查看代码发现mina在随web容器启动过一次5222端口后,xmppserver类中的start方法中ClassPathXmlApplicationContext类又加载了一次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">
- <context:component-scan base-package="org.androidpn.server.*" /><!-- 自动装配 -->
- <!-- =============================================================== -->
- <!-- 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>
- <!-- sessionFactory -->
- <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="txManager"
- class="org.springframework.orm.hibernate3.HibernateTransactionManager">
- <property name="sessionFactory" ref="sessionFactory" />
- <property name="dataSource" ref="dataSource" />
- </bean>
- <!-- 采用注解来管理事务-->
- <tx:annotation-driven transaction-manager="txManager" />
- <!-- spring hibernate工具类模板 -->
- <bean id="hibernateTemplate"
- class="org.springframework.orm.hibernate3.HibernateTemplate">
- <property name="sessionFactory" ref="sessionFactory"></property>
- </bean>
- <!-- spring jdbc 工具类模板 -->
- <bean id="jdbcTemplate"
- class="org.springframework.jdbc.core.JdbcTemplate">
- <property name="dataSource">
- <ref bean="dataSource" />
- </property>
- </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" scope="singleton">
- <property name="defaultLocalAddress" value=":5222" />
- <property name="handler" ref="xmppHandler" />
- <property name="filterChainBuilder" ref="filterChainBuilder" />
- <property name="reuseAddress" value="true" />
- </bean>
- <bean id="serviceLocator" class="org.androidpn.server.service.ServiceLocator" scope="singleton" />
- <!-- Services-->
- <bean id="userService" class="org.androidpn.server.service.impl.UserServiceImpl"/>
- <bean id="notificationService" class="org.androidpn.server.service.impl.NotificationServiceImpl"/>
- </beans>
配置serviceLocator是为了保证spring容器只能由一个上下文,也就是spring容器只被启动一次,我们将BeanFactory交给了serviceLocator,这样一来有什么好处呢?
控制层,服务层,数据库操作层都受spring管理,在他们中去跟spring要资源,一定是要什么有什么想怎么拿就怎么拿,都很方便,但是如果想在没有被spring所管理的类中去拿spring的资源,动作就不那么优雅了,有人建议用ClassPath加载器初始化spring工厂来获取资源,问题就处在这里,这种做法必定会产生2个spring上下文,一个是web容器所启动的,一个是java类加载器所启动的,我们的MINA服务器也就被启动了2次,其实资源被重复多次实例化除了影响性能外,对程序影响可能并不大,但是MINA被启动2次,肯定会出问题的。为保证spring只有一个上下文,我们将容器上下文交给了serviceLocator,脱离spring管控的环境可以面向serviceLocator来调度spring中的资源操作MINA服务器。
- package org.androidpn.server.service;
- import org.springframework.beans.BeansException;
- import org.springframework.beans.factory.BeanFactory;
- import org.springframework.beans.factory.BeanFactoryAware;
- public class ServiceLocator implements BeanFactoryAware {
- private static BeanFactory beanFactory = null;
- private static ServiceLocator servlocator = null;
- public static String USER_SERVICE = "userService";
- public static String NOTIFICATION_SERVICE = "notificationService";
- public void setBeanFactory(BeanFactory factory) throws BeansException {
- this.beanFactory = factory;
- }
- public BeanFactory getBeanFactory() {
- return beanFactory;
- }
- public static ServiceLocator getInstance() {
- if (servlocator == null)
- servlocator = (ServiceLocator) beanFactory.getBean("serviceLocator");
- return servlocator;
- }
- /**
- * 根据提供的bean名称得到相应的服务类
- *
- * @param servName
- * bean名称
- */
- public static Object getService(String servName) {
- return beanFactory.getBean(servName);
- }
- /**
- * 根据提供的bean名称得到对应于指定类型的服务类
- *
- * @param servName
- * bean名称
- * @param clazz
- * 返回的bean类型,若类型不匹配,将抛出异常
- */
- public static Object getService(String servName, Class clazz) {
- return beanFactory.getBean(servName, clazz);
- }
- /**
- * Obtains the user service.
- *
- * @return the user service
- */
- public static UserService getUserService() {
- return (UserService) getService(USER_SERVICE);
- }
- public static NotificationService getNotificationService() {
- return (NotificationService) getService(NOTIFICATION_SERVICE);
- }
- }
在config.properties中还要特别注意xmpp.resourceName必须跟客户端中XmppManager的private static final String XMPP_RESOURCE_NAME = "AndroidpnClient";保持一致,否则连不上服务器,还xmpp.session.maxInactiveInterval=-1表示永不中断,如果设定了时间超过这个时间范围没有任何活动就会自动断开,这里的时间单位全部是毫秒。
- apiKey=1234567890
- xmpp.ssl.storeType=JKS
- xmpp.ssl.keystore=conf/security/keystore
- xmpp.ssl.keypass=changeit
- xmpp.ssl.truststore=conf/security/truststore
- xmpp.ssl.trustpass=changeit
- xmpp.resourceName=AndroidpnClient
- ##Added by ken
- username=admin
- password=admin
- #资源名称
- resource_name=AndroidpnClient
- #校验超时时间间隔
- xmpp.session.checkTimeoutInterval=10000
- #Session timeout最大非活动时间间隔
- xmpp.session.maxInactiveInterval=1000000
在androidpn.properties中端口和IP不要写错,有人喜欢写localhost,在手机上是无法识别的,必须写绝对IP地址。
apiey=1234567890
xmppHost=192.168.1.78
xmppPort=5222
运行结果如下:
离线消息也支持,先给离线用户发个消息,效果如下:
在数据库中我们看到有一条离线消息是发给用户4aa50dde313f4b63907c2430bf00b413,status为0标记为离线
这时我们再上线,大约等待20秒左右,查看系统控制台打印:
离线消息也支持,先给离线用户发个消息,效果如下:
在数据库中我们看到有一条离线消息是发给用户4aa50dde313f4b63907c2430bf00b413,status为0标记为离线
这时我们再上线,大约等待20秒左右,查看系统控制台打印:
查看android端看看用户4aa50dde313f4b63907c2430bf00b413上线情况:
这时候数据库记录发生了变化,status变成了2,表示已经接收,用户点击OK的时候,它又变成了3表示已经查看
离线消息的原理相对比较简单,当系统给指定用户发送消息时候,会首先判断用户是够在线,如果在线就直接发送,如果没有在线就暂时标记保存,等用户上线时候先查离线消息然后弹出,其实整个项目都是开源的,可能唯一的难点就是对MINA和XMPP协议的不了解,再加上本身对socket和多线程的畏惧,如果这些全部都掌握,驾驭好这套源码还是很有信心的,了解其基本原理以后,我们就可以放心的做更多的扩展。
网上现在也有不少androidpn版本,五花八门什么都有,里面到底有没问题,改了什么没改什么都不知道,基本上已经追溯不到原创到底是谁了,索性就只能从国外的一个网站上下了一个比较可靠的版本自己动手去量身改造,终于出了一个比较稳定版本。对于消息提醒来说,它仅仅是个notification,许多人非要把业务数据也做进去,更有夸张好几兆的xml数据就这么硬塞提醒过去,这种做法本身就背离了设计的初衷,非要把跑车当牛车使能不出问题吗?其实业务数据还是用http拉比较好,xmpp及时的前提是用资源消耗作为代价的,我们能适度就适度用,用好用稳就行!
项目源码下载: Androidpn威力加强版(4月17日更新)
搭建步骤:
1.android端找到res/raw/androidpn.properties文件修改服务器ip地址,不要写localhost,写绝对ip地址
2.服务端找到resources/jdbc.properties 在mysql中新建一个数据库apn,并将连接指向该库,设置用户名和密码,库表会随服务启动的时候自动创建
3.先启动服务,再打开android客户端,点击连接即可
参阅文献
Openfirehttp://www.igniterealtime.org/
push-notificationhttp://www.push-notification.org/
Claros chathttp://www.claros.org/
androidpnsourceforgehttp://sourceforge.net/projects/androidpn/
android消息推送解决方案http://www.cnblogs.com/hanyonglu/archive/2012/03/04/2378971.html
xmpp协议实现原理介绍 http://www.cnblogs.com/hanyonglu/archive/2012/03/04/2378956.html