即時通訊軟體openfire+spark+smack

yangxi_001發表於2014-03-06
  • 開發你自己的XMPP IM - [J2EE]


這幾天查國內外的資料,發現國內關於這方面間的軟體資料太少了,就想在這裡寫幾篇關於此類IM 軟體開發的文章。不過別看東西小,涉及的模組可不少。

所以我基本上分為三篇文章來介紹此類軟體的開發:

第一篇是關於XMPP 協議是啥,IM 是啥以及一個比較有名的開源實現,該開源實現包括三個部分(Spark、Smack和Openfire);

第二篇講如何開發基於Spark 的客戶端IM 外掛部分;

第三篇講如何開發基於Openfire 伺服器端的外掛部分。

好了,進入正題吧。

 

什麼是XMPP?
Extensible Messaging and Presence Protocol,簡單的來講,它就是一個傳送接收處理訊息的協議,但是這個協議傳送的訊息,既不是二進位制的東東也不是字串,而是XML。正是因為使用了XML作為訊息傳遞的中介,Extensible 才談的上,不是麼?嘿嘿。再詳盡的東西,我也就不多介紹了,大家可以去百度百科裡檢視下。

什麼是IM ?

Instant Messenger,及時通訊軟體,就是大家使用的QQ、MSN Messenger和Gtalk等等。其中Gtalk 就是基於XMPP 協議的一個實現,其他的則不是。當前IM 幾乎作為每個上網者必然使用的工具,在國外的大型企業中有一些企業級的IM應用,但是其商業價值還沒完全發揮出來。設想既然XMPP 協議是一個公開的協議,那麼每個企業都可以利用它來開發適合本身企業工作,提高自身生產效率的IM;甚至,你還可以在網路遊戲中整合這種通訊軟體,不但讓你可以邊遊戲邊聊天,也可以開發出適合遊戲本身的IM 應用,比如說一些遊戲關鍵場景提醒功能,團隊語音交流等等都可以基於IM來實現。說了這麼多,就是一個意思,其商業價值遠遠比你想的高!

Spark Smack 和 Openfire

開源界總是有許多有趣的東東,這三個合起來就是一個完整的XMPP IM 實現。包括伺服器端——Openfire,客戶端——Spark,XMPP 傳輸協議的實現——Smack(記住,XMPP是一個協議,協議是需要實現的,Smack起到的就是這樣的一個作用)。三者都是基於Java 語言的實現,因此對於熟悉Java 的開發者來說不是很難


Spark 提供了客戶端一個基本的實現,並提出了一個很好的外掛架構,這對於開發者來說不能不說是一個福音。我強烈建議基於外掛方式來實現你新增加的功能,而不是去改它的原始碼,這樣有利於你專案架構,把原始專案的影響降到最低,文章以後的部分也是基於這種外掛體系進行開發的

Openfire 是基於XMPP 協議的IM 的伺服器端的一個實現,雖然當兩個使用者連線後,可以通過點對點的方式來傳送訊息,但是使用者還是需要連線到伺服器來獲取一些連線資訊和通訊資訊的,所以伺服器端是必須要實現的。Openfire 也提供了一些基本功能,但真的很基本的!慶幸的是,它也提供外掛的擴充套件,像Spark 一樣,我同樣強烈建議使用外掛擴充套件的方式來增加新的功能,而不是修改人家的原始碼。

Smack 是一個XMPP 協議的Java 實現,提供一套可擴充套件的API,不過有些時候,你還是不得不使用自己定製傳送的XML 檔案內容的方式來實現自己的功能

下圖展示了三者之間的關係:

 

 

從圖上可以瞭解到,client 端和server端都可以通過外掛的方式來進行擴充套件,smack是二者傳遞資料的媒介。

 

  • 開發你自己的XMPP IM 續 - Spark 外掛開發 - [J2EE]


繼續3月18日介紹基於XMPP IM開發的那篇Blog,今天主要總結一下如何基於Spark 的外掛架構來新增客戶端的功能,這裡列舉出一個獲取伺服器端群組資訊的實際例子,實現後的效果如下圖所示:

 

Spark 是一個基於XMPP 協議,用Java 實現的IM 客戶端。它提供了一些API,可以採用外掛機制進行擴充套件,上圖中,“部門”部分就是使用外掛機制擴充套件出來的新功能。要想實現你的擴充套件,首先要了解 Spark API的架構,其中最關鍵的是要了解它的工廠類,這些工廠類可以獲得Spark 提供的諸如XMPPConnection、ChatContainer 等例項,從而你可以實現獲取伺服器的資訊,與另外的Client 通訊等功能。最核心的類是SparkManager,這個類是一系列工廠類的工廠類(呵呵,還真拗口)。它的getChatManager()、getSessionManager ()、getMainWindow() 、getConnection() 等方法分別可以獲得聊天管理器、會話管理器、主視窗、與伺服器的連線等等非常有用的例項。基本上可以說SparkManager 是你與Spark 打交道的銜介面。其實,每一個Manager 都使用了單例模式,你也可以不通過SparkManager 來獲取它們,但筆者建議你從單一的入口著手,這樣有利於程式碼的開發和維護。

接下來描述一下外掛的開發流程:
1、建立外掛配置檔案 plugin.xml
2、實現你自己的Plugin 類的實現(如果你需要實現自己規定格式的XML 傳送、接收和處理,那麼你需要在這裡註冊你的IQProvider,關於IQProvider 你可以查詢Smack API,簡單的來講是處理你自定義的IQ 處理器。)
3、打包你的外掛(Spark 有自己的打包機制,我研究了半天才發現其中的玄機,後面介紹)
4、部署你的外掛(其實3、4兩步可以糅合在一起,當然要利用Ant 啦)

好滴,下面結合一個實際的例子講述上面的四個步驟
1、plugin.xml

<plugin>
    <name>Enterprise IM Client</name>
    <version>1.0</version>
    <author>Phoenix</author>
    <homePage>http://phoenixtoday.blogbus.com</homePage>
    <email>phoenixtoday@gmail.com</email>
    <description>Enterprise Client Plug-in</description>
    <!-- 關鍵是這裡,這裡要定義你的Plugin 類 -->
    <class>com.im.plugin.IMPlugin</class>
    <!-- 這裡定義你使用的Spark 最低版本 -->
    <minSparkVersion>2.5.0</minSparkVersion>
    <os>Windows</os>
</plugin>

這是一個 plugin.xml 檔案的內容,外掛體系會自動呼叫你在此檔案中定義的Plugin 類,從而完成你自己擴充套件的功能。最關鍵的部分我用紅色標識出來了,要宣告你的外掛擴充套件類,採用完整的名稱空間方式(包括包名),其餘的部分結合我的註釋,大家應該都能理解,就不做詳細的描述了。要注意的是plugin.xml 檔案要放在專案的根目錄下,這是嚴格規定好的。

2、Plugin 類的實現
你的類首先要實現Spark 提供的Plugin 介面,然後實現它的一些方法。其中最主要的是實現initialize() 發放,在這裡註冊你的的IQProvider

ProviderManager providerManager = ProviderManager.getInstance();
providerManager.addIQProvider("groups", "com:im:group", //1
                new GroupTreeIQProvider());
System.out.println("註冊GroupTree IQ 提供者");
requestGroupTree();

上述的程式碼,就在該類就是我實現的IMPlugin.initialize() 方法中的一小段,大概的含義是,先獲取ProviderManager(這個貌似不能從SparkManager 直接獲取),然後註冊一個GroupTreeIQProvider(自己建立的)這是一個IQProvider 的具體實現,它用於解析像下面這樣的一個XML 檔案:

<?xml version="1.0" encoding="UTF-8"?>
<iq type='result' 
to='domain@server.com' from='phoenixtoday@gmail.com' id='request_1'>
    <groups xmlns='com:im:group'>
        <group>
             <groupId>1</groupId>
             <name>西安交通大學</name>
             <upGroup>ROOT</upGroup>
             <isLeaf>0</isLeaf>
             <description>xjtu</description>
             <user>
                 <userGroupId>1</userGroupId>
                 <userName>phoenix_test</userName>
                 <role>normal</role>
             </user>
        </group>
        <group>
             <groupId>2</groupId>
             <name>電信學院</name>
             <upGroup>1</upGroup>
             <isLeaf>1</isLeaf>
             <description>xjtu info</description>
        </group>
    </groups>
</iq>

可以看到,在註冊 IQProvider 的時候(程式碼中標註的1部分),需要你提供名稱和名稱空間,我的XML 檔案中的iq 下的第一個子節點是<groups> 所以我的名稱就寫“groups”,名稱空間對應於groups 節點的xmlns(XML Name Space)所以是“com:im:group”,其實IQProvider 中最關鍵的方法是parseIQ(XmlPullParser parser) 該方法就是解析XML,完成你的功能,並返回一個相應的IQ 例項(這裡可以把IQ 看做一個回饋的Model 類)。說到底實現基於XMPP 協議的IM 就是解析XML 檔案,而這正是客戶端的IQProvider 和伺服器端的IQHandler(下一篇文章會涉及到)所做的事情。

3、打包你的外掛
現在該有的功能都實現了,那麼就是打包了。這最好利用Ant 來完成,因為每次你都要打包,要部署,如果純手動的話,那也太不敏捷了,大大影響開發效率。

<?xml version="1.0" encoding="UTF-8"?>
<project name="IM" default="release" basedir=".">
    <property name="src.dir" value="src" />
    <property name="dest.dir" value="bin" />
    <property name="lib.dir" value="lib" />
    <property name="im.path"
        value="E:/workspace/europa/spark_new/doc/spark/target/build" />
    <target name="clean">
        <!--  
            <delete dir="${dest.dir}" />
            
            <delete dir="${lib.dir}" />
        -->
    </target>
    <target name="init" depends="clean">
        <!--  
            <mkdir dir="${dest.dir}" />
            
            <mkdir dir="${lib.dir}" />
        -->
    </target>
    <target name="build" depends="init">
        <!--
            <javac srcdir="${src.dir}" destdir="${dest.dir}" />
        -->
    </target>
    <!-- 最重要的是這裡,打兩次包 -->
    <target name="jar" depends="build">
        <jar jarfile="${lib.dir}/eim.jar" basedir="${dest.dir}" />
        <jar jarfile="${im.path}/plugins/eim.jar">
            <fileset dir=".">
                <include name="lib/*.jar" />
            </fileset>
            <fileset dir=".">
                <include name="plugin.xml" />
            </fileset>
        </jar>
    </target>
    <target name="release" depends="jar">
        <!--  
            <exec executable="cmd.exe"
            failonerror="true">
            <arg line="/c e:"/>
            <arg line="/c cd workspace/europa/spark_new/doc/spark/target/build/bin"/>
            <arg line="/c startup.bat"/>
            </exec>
        -->
    </target>
</project>

這是我的這個專案的 build.xml 檔案中的內容。因為Eclipse 其實幫我自動完成了編譯的任務,所以我也就省去了這寫編譯的步驟,最重要的是大家要看到“jar” 部分,Spark 打包的神祕之處也就在此,打兩次包首先把你的專案打包到本專案lib 資料夾下,比如說你的專案目錄是MyPlugin 那麼,你就將你的類打包到MyPlugin/lib 目錄下,然後再次的打包,將所有的lib 資料夾下的內容打包起來,記得這次要包含plugin.xml。也就是說,最後Spark 外掛體系會讀取你的專案下的lib 資料夾下的內容。這裡我也有個疑問,我本來想每次打包後自動執行bat 檔案,啟動外掛,看看效果,為啥死都呼叫不了呢,那段程式碼在最後面,註釋掉了,誰能幫我解決,我請他吃飯滴!

4、最後就是釋出了

其實我的釋出很簡單,就是將這個打包好的jar 檔案拷到Spark 本身的plugins 目錄下,每次啟動Spark 的時候,它會自動呼叫自定義的外掛的。我這裡用Ant 第二次jar 的時候,就自動拷貝過去了,這裡用的是絕對路徑,所以你不能直接拷貝就用滴呦(是不是很醜陋呀,這段Ant 程式碼)。

基本上客戶端的實現原理就是這樣的,只是有些地方需要特別注意,還有就是應該利用像Ant 這樣的工具大大簡化開發步驟,加快開發效率。還有就是,我建議你在開發自己的外掛的時候,多利用MVC 模式,尤其是在IQProvider 解析後,生成的部分可以例項化Model,然後你可以編寫自己的Manager 進行這些Model 的處理。多寫Log,當然Log4j 貌似不太起作用,那就System.out.println() 吧,哈哈!今天就寫到這裡啦,偶有點累啦。

 

  • 開發你自己的XMPP IM 續 - Openfire 外掛開發 - [J2EE]


繼續上一篇的內容,本篇文章介紹開發Openfire 的外掛

這篇文章拖了很久了,呵呵,真是千呼萬喚始出來呀。Openfire 伺服器端是支援外掛開發的,開發過程可能會涉及到資料庫的操作,本篇文章專注於Openfire 外掛的部分,對伺服器端涉及到資料庫的開發只做簡單介紹。

Openfire 是一個用Java 實現的XMPP 伺服器,客戶端可以通過IQ 的方式與其進行通訊(其實就是XML),客戶端和伺服器之間的通訊是依靠底層Smack 庫提供的各種功能來完成的。其實利用外掛方式來擴充套件Openfire 伺服器端主要有兩種擴充套件方式,一種是對伺服器控制檯頁面進行擴充套件(不是本文的主要內容),其實就是遵循Openfire 頁面的佈局方式,進行相應的頁面擴充套件和功能擴充套件;另一種是對通訊功能進行擴充套件。本文主要針對後者進行具體的描述

本篇文章的結構如下:

1、建立plugin.xml(這是整個外掛最關鍵的文件)
2、建立伺服器外掛例項(實現Plugin 介面的一個類還有一批IQHandler)
3、打包外掛(Openfire 外掛也有自己的打包方式)和部署外掛

好滴,實刀實槍的來動手做吧

1、建立plugin.xml

初次開發Openfire 和Spark 外掛的時候,很容易把二者搞混,千萬記得,這裡是Openfire 的plugin.xml 不是第二篇文章說的那個啦!

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
    <!-- Main plugin class  這裡是最重要滴-->
    <class>com.im.server.plugin.GroupTreePlugin</class>

    <!-- Plugin meta-data -->
    <name>GroupTreePlugin</name>
    <description>This is the group plugin.</description>
    <author>Phoenix</author>

    <version>1.0</version>
    <date>14/03/2008</date>
    <url>http://localhost:9001/openfire/plugins.jsp</url>
    <minServerVersion>3.4.1</minServerVersion>
    <licenseType>gpl</licenseType>

    <!-- Admin console entries -->
    <adminconsole>
        <!-- More on this below -->
    </adminconsole>
</plugin>

最重要的那一行我已經標記出來啦,就是你這個外掛的初始化和垃圾清理類,例子中是在com.im.server.plugin 包中的GroupTreePlugin 類,下文會對這個類進行詳細描述。其餘的都是描述資訊,只要你提供了正確的描述資訊,一般都不會出錯。建議初次開發者,在寫完plugin.xml 檔案後,寫一個簡單的Plugin 例項,並列印出一些資訊,如果重新啟動Openfire 資訊成功顯示,恭喜你,你已經邁出一大步了!

2、實現Plugin 類和IQHandler

Plugin 類主要起到的作用是初始化和釋放資源,在初始化的過程中,最重要的的註冊一批IQHandler,IQHander 的作用有點類似於Spark 中的IQProvider,其實就是解析XML 檔案之後,生成一些有用的例項,以供處理。下面分別給出一個Plugin 類的例項和IQProvider 的例項

GroupTreePlugin 類

/**
 * 伺服器端外掛類
 * 
 * @author Phoenix
 * 
 * Mar 14, 2008 11:03:11 AM
 * 
 * version 0.1
 */
public class GroupTreePlugin implements Plugin
{
    private XMPPServer server;

    /*
     * (non-Javadoc)
     * 
     * @see org.jivesoftware.openfire.container.Plugin#destroyPlugin()
     */
    public void destroyPlugin()
    {

    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jivesoftware.openfire.container.Plugin#initializePlugin(org.jivesoftware.openfire.container.PluginManager,
     *      java.io.File)
     */
    public void initializePlugin(PluginManager manager, File pluginDirectory)
    {
        PluginLog.trace("註冊群組樹IQ處理器");
        server = XMPPServer.getInstance();
        
        server.getIQRouter().addHandler(new GroupTreeIQHander()); //1
        server.getIQRouter().addHandler(new UserInfoIQHandler());
        server.getIQRouter().addHandler(new DelUserIQHandler());
        server.getIQRouter().addHandler(new CreateUserIQHandler());
        server.getIQRouter().addHandler(new AddGroupUserIQHandler());
        server.getIQRouter().addHandler(new SetRoleIQHandler());

    }

}

上例所示,在初始化中先找到IQRouter,然後通過IQRouter 註冊一批IQHandler,這些IQHander 會自動監聽相應名稱空間的IQ,然後進行處理;由於這個Plugin 不需要做資源釋放的工作,所以在destroyPlugin() 方法中沒有任何內容。具體的IQHander 類如下

GroupTreeIQHander

/**
 * 處理客戶端發來的IQ,並回送結果IQ
 * 
 * @author Phoenix
 * 
 * Mar 14, 2008 4:55:33 PM
 * 
 * version 0.1
 */
public class GroupTreeIQHander extends IQHandler
{

    private static final String MODULE_NAME = "group tree handler";

    private static final String NAME_SPACE = "com:im:group";

    private IQHandlerInfo info;

    public GroupTreeIQHander()
    {
        super(MODULE_NAME);
        info = new IQHandlerInfo("gruops", NAME_SPACE);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jivesoftware.openfire.handler.IQHandler#getInfo()
     */
    @Override
    public IQHandlerInfo getInfo()
    {
        return info;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.jivesoftware.openfire.handler.IQHandler#handleIQ(org.xmpp.packet.IQ)
     */
    @Override
    public IQ handleIQ(IQ packet) throws UnauthorizedException
    {
        IQ reply = IQ.createResultIQ(packet);
        Element groups = packet.getChildElement();//1
        
        if (!IQ.Type.get.equals(packet.getType()))
        {
            System.out.println("非法的請求型別");
            reply.setChildElement(groups.createCopy());
            reply.setError(PacketError.Condition.bad_request);
            return reply;
        }
        
        String userName = StringUtils.substringBefore(packet.getFrom().toString(),"@");

        GroupManager.getInstance().initElement(groups,userName);
        
        reply.setChildElement(groups.createCopy());//2

        System.out.println("返回的最終XML" + reply.toXML());

        return reply;
    }

}

可以看到主要有兩個方法,一個是getInfo() 這個方法的目的是提供要解析的名稱空間,在本例中,這個IQHandler 對每個名稱空間為"com:im:group" 的例項進行處理;還有一個最重要的方法:handleIQ() 該方法對包含指定名稱空間的XML 進行解析,然後返回一個解析好的IQ。其實我認為,這個IQHandler 和IQ 的關係就是Controller 和Model 的關係(如果你瞭解MVC 的話,那麼你一定知道我再說什麼),只不過這裡並沒有指定什麼View,你完全可以把IQ 當成Model 類進行理解。在這裡,我用了GroupManager 進行了XML 的處理,因為我返回的IQ 內容中要從資料庫讀取所有群組資訊,所以轉交給GroupManager 進行處理,你完全可以在這個方法中進行具體的XML 處理,在這裡,解析和建立新的XML 主要用到的是JDOM(如果你對Java 解析XML 有所瞭解,那真的太好了!)。程式//1 處主要是獲取建立返回的IQ,並獲取原來IQ 的子元素(用於建立我們返回的IQ);程式//2 處很關鍵,如果你不呼叫createCopy 方法,程式會出錯(程式會死鎖還是什麼,忘記咧,不好以西)。

這就是程式的主體部分,我在這裡有一個建議,能不用Openfire 原始的程式函式,就不要用它們。我的提取資料庫方式都是自己寫的Bean,這樣有利於你自己對程式的掌控,其實更有利於快速開發(這世道不是啥都講究敏捷麼,哇哈哈)

3、打包外掛

打包依然遵循二次打包的原則(如果你不瞭解啥叫要二次打包,請看上一篇)
這是我的ant 檔案,由於Eclipse 幫我做了build 等很多工作,實際我的ant 工作就是在打包,並放入外掛目錄下的plugin 資料夾下

<?xml version="1.0" encoding="UTF-8"?>
<project name="IM" default="release" basedir=".">

    <property name="openfire.path"
        value="E:/workspace/europa/openfire_src/target/openfire" />
    <property name="classes.dir" value="classes" />
    <property name="lib.dir" value="lib" />

    <target name="jar">
        <jar jarfile="${lib.dir}/grouptreeplugin.jar" basedir="${classes.dir}" >
            <fileset dir=".">
                <include name="*.jar"/>
            </fileset>
        </jar>
        <jar jarfile="${openfire.path}/plugins/groupTreePlugin.jar">
            <fileset dir=".">
                <include name="lib/*.jar" />
                <include name="plugin.xml" />
                <include name="logo_small.gif" />
                <include name="logo_large.gif" />
                <include name="readme.html" />
                <include name="changelog.html" />
                <include name="build.xml" />
            </fileset>
        </jar>

    </target>

    <target name="release" depends="jar">
    </target>

</project>

好了,至此XMPP+Spark+Openfire 的外掛開發三部曲徹底結束了,希望你們對這個開發流程有了系統的瞭解。

相關文章