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

yangxi_001發表於2014-08-06
前面幾篇給大家系統講解的有關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,如此糊弄,實在難為所用!

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配置,導致埠被重複開啟兩次,最後終於發現問題所在:
[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/beanshttp://www.springframework.org/schema/beans/spring-beans-2.5.xsd
                http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-2.5.xsd
                http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-2.5.xsd
                http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-2.5.xsd
                http://www.springframework.org/schema/utilhttp://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;
    }

    /**
     * 根據提供的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);
    }
}[/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 執行結果如下:


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


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


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


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


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


離線訊息的原理相對比較簡單,當系統給指定使用者傳送訊息時候,會首先判斷使用者是夠線上,如果線上就直接傳送,如果沒有線上就暫時標記儲存,等使用者上線時候先查離線訊息然後彈出,其實整個專案都是開源的,可能唯一的難點就是對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客戶端,點選連線即可









參閱文獻
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

相關文章