天天看點

Android消息推送技術原理分析和實踐

前面幾篇給大家系統講解的有關xmpp openfire smack asmack相關的技術和使用,大家如果有所遺忘可以參考

http://blog.csdn.net/shimiso/article/details/8816558     基于xmpp openfire smack開發之openfire介紹和部署[1] http://blog.csdn.net/shimiso/article/details/8816540     基于xmpp openfire smack開發之smack類庫介紹和使用[2] http://blog.csdn.net/shimiso/article/details/11225873   基于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,如此糊弄,實在難為所用!

Android消息推送技術原理分析和實踐

androidpn消息推送采用的是apache的mina架構做的,服務端和用戶端兩邊都有監聽,也就是我們所說的socket程式設計,有人說socket程式設計有什麼難的,就那麼回事,其實不然,我們平時寫的socket聊天都隻是在區域網路的,但是要穿透路由和防火牆,讓資訊安全及時的傳送到另一個網關的區域網路電腦中,就不是一件簡單的活了,其中涉及到在nat上打洞,還有線程,斷網重連,安全加密等等,那麼androidpn配合mina相當于把這些活都幹了,那麼我們要的幹活就相對比較精細了,第一學習mina的安裝配置的規則,第二學習xmpp協定組裝和解析的規則,第三學習androidpn推和收消息的核心代碼,如此三點我們便能靈活駕馭住androidpn出現再大的問題自己也能動手去調了。

Android消息推送技術原理分析和實踐
Android消息推送技術原理分析和實踐

在和spring整合的時候大家要注意不要讓mina服務啟動2次,筆者整合時候無意發現在linux64位系統,weblogic上啟動時候總是報5222已經被占用,反複檢視代碼發現mina在随web容器啟動過一次5222端口後,xmppserver類中的start方法中ClassPathXmlApplicationContext類又加載了一次spring配置,導緻端口被重複開啟兩次,最後終于發現問題所在:

[mw_shl_code=java,true]<?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>[/mw_shl_code]

配置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伺服器。

[mw_shl_code=java,true]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;

    }

    public static Object getService(String servName) {

        return beanFactory.getBean(servName);

    }

    public static Object getService(String servName, Class clazz) {

        return beanFactory.getBean(servName, clazz);

    }

    public static UserService getUserService() {

        return (UserService) getService(USER_SERVICE);

    }

    public static NotificationService getNotificationService() {

        return (NotificationService) getService(NOTIFICATION_SERVICE);

    }

}[/mw_shl_code]

在config.properties中還要特别注意xmpp.resourceName必須跟用戶端中XmppManager的private static final String XMPP_RESOURCE_NAME = "AndroidpnClient";保持一緻,否則連不上伺服器,還xmpp.session.maxInactiveInterval=-1表示永不中斷,如果設定了時間超過這個時間範圍沒有任何活動就會自動斷開,這裡的時間機關全部是毫秒。

[mw_shl_code=html,true]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[/mw_shl_code]

在androidpn.properties中端口和IP不要寫錯,有人喜歡寫localhost,在手機上是無法識别的,必須寫絕對IP位址。apiey=1234567890xmppHost=192.168.1.78xmppPort=5222 運作結果如下:

Android消息推送技術原理分析和實踐
Android消息推送技術原理分析和實踐

離線消息也支援,先給離線使用者發個消息,效果如下:

Android消息推送技術原理分析和實踐

在資料庫中我們看到有一條離線消息是發給使用者4aa50dde313f4b63907c2430bf00b413,status為0标記為離線

Android消息推送技術原理分析和實踐

這時我們再上線,大約等待20秒左右,檢視系統控制台列印:

Android消息推送技術原理分析和實踐

檢視android端看看使用者4aa50dde313f4b63907c2430bf00b413上線情況:

Android消息推送技術原理分析和實踐

這時候資料庫記錄發生了變化,status變成了2,表示已經接收,使用者點選OK的時候,它又變成了3表示已經檢視

Android消息推送技術原理分析和實踐

離線消息的原理相對比較簡單,當系統給指定使用者發送消息時候,會首先判斷使用者是夠線上,如果線上就直接發送,如果沒有線上就暫時标記儲存,等使用者上線時候先查離線消息然後彈出,其實整個項目都是開源的,可能唯一的難點就是對MINA和XMPP協定的不了解,再加上本身對socket和多線程的畏懼,如果這些全部都掌握,駕馭好這套源碼還是很有信心的,了解其基本原理以後,我們就可以放心的做更多的擴充。         網上現在也有不少androidpn版本,五花八門什麼都有,裡面到底有沒問題,改了什麼沒改什麼都不知道,基本上已經追溯不到原創到底是誰了,索性就隻能從國外的一個網站上下了一個比較可靠的版本自己動手去量身改造,終于出了一個比較穩定版本。對于消息提醒來說,它僅僅是個notification,許多人非要把業務資料也做進去,更有誇張好幾兆的xml資料就這麼硬塞提醒過去,這種做法本身就背離了設計的初衷,非要把跑車當牛車使能不出問題嗎?其實業務資料還是用http拉比較好,xmpp及時的前提是用資源消耗作為代價的,我們能适度就适度用,用好用穩就行!

源碼搭建步驟:

1.android端找到res/raw/androidpn.properties檔案修改伺服器ip位址,不要寫localhost,寫絕對ip位址

2.服務端找到resources/jdbc.properties 在mysql中建立一個資料庫apn,并将連接配接指向該庫,設定使用者名和密碼,庫表會随服務啟動的時候自動建立

3.先啟動服務,再打開android用戶端,點選連接配接即可

參閱文獻

Openfire http://www.igniterealtime.org/

push-notification http://www.push-notification.org/

Claros chat http://www.claros.org/

androidpnsourceforge http://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