前面幾篇給大家系統講解的有關xmpp openfire smack asmack相關的技術和使用,大家如果有所遺忘可以參考
基於xmpp openfire smack開發之openfire介紹和部署[1]
基於xmpp openfire smack開發之smack類庫介紹和使用[2]
基於xmpp openfire smack開發之Android客戶端開發[3]
順便也一起回顧下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過於龐大繁複,許多對我們來說都是沒什麼用的,甚至要砍掉改造,能不能有精簡的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秒左右,查看系統控制檯打印:
查看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
轉載請標明出處http://blog.csdn.net/shimiso
歡迎有識之士加入我們的技術交流羣:173711587