即時通訊軟件openfire+spark+smack 收藏
開發(fā)你自己的XMPP IM - [J2EE]
這幾天查國內(nèi)外的資料,發(fā)現(xiàn)國內(nèi)關(guān)于這方面間的軟件資料太少了,就想在這里寫幾篇關(guān)于此類IM 軟件開發(fā)的文章。不過別看東西小,涉及的模塊可不少。
所以我基本上分為三篇文章來介紹此類軟件的開發(fā):
第一篇是關(guān)于XMPP 協(xié)議是啥,IM 是啥以及一個比較有名的開源實現(xiàn),該開源實現(xiàn)包括三個部分(Spark、Smack和Openfire);
第二篇講如何開發(fā)基于Spark 的客戶端IM 插件部分;
第三篇講如何開發(fā)基于Openfire 服務(wù)器端的插件部分。
好了,進(jìn)入正題吧。
什么是XMPP?
Extensible Messaging and Presence Protocol,簡單的來講,它就是一個發(fā)送接收處理消息的協(xié)議,但是這個協(xié)議發(fā)送的消息,既不是二進(jìn)制的東東也不是字符串,而是XML。正是因為使用了XML作為消息傳遞的中介,Extensible 才談的上,不是么?嘿嘿。再詳盡的東西,我也就不多介紹了,大家可以去百度百科里查看下。
什么是IM ?
Instant Messenger,及時通信軟件,就是大家使用的QQ、MSN Messenger和Gtalk等等。其中Gtalk 就是基于XMPP 協(xié)議的一個實現(xiàn),其他的則不是。當(dāng)前IM 幾乎作為每個上網(wǎng)者必然使用的工具,在國外的大型企業(yè)中有一些企業(yè)級的IM應(yīng)用,但是其商業(yè)價值還沒完全發(fā)揮出來。設(shè)想既然XMPP 協(xié)議是一個公開的協(xié)議,那么每個企業(yè)都可以利用它來開發(fā)適合本身企業(yè)工作,提高自身生產(chǎn)效率的IM;甚至,你還可以在網(wǎng)絡(luò)游戲中集成這種通信軟件,不但讓你可以邊游戲邊聊天,也可以開發(fā)出適合游戲本身的IM 應(yīng)用,比如說一些游戲關(guān)鍵場景提醒功能,團(tuán)隊語音交流等等都可以基于IM來實現(xiàn)。說了這么多,就是一個意思,其商業(yè)價值遠(yuǎn)遠(yuǎn)比你想的高!
Spark Smack 和 Openfire
開源界總是有許多有趣的東東,這三個合起來就是一個完整的XMPP IM 實現(xiàn)。包括服務(wù)器端——Openfire,客戶端——Spark,XMPP 傳輸協(xié)議的實現(xiàn)——Smack(記住,XMPP是一個協(xié)議,協(xié)議是需要實現(xiàn)的,Smack起到的就是這樣的一個作用)。三者都是基于Java 語言的實現(xiàn),因此對于熟悉Java 的開發(fā)者來說不是很難
Spark 提供了客戶端一個基本的實現(xiàn),并提出了一個很好的插件架構(gòu),這對于開發(fā)者來說不能不說是一個福音。我強(qiáng)烈建議基于插件方式來實現(xiàn)你新增加的功能,而不是去改它的源代碼,這樣有利于你項目架構(gòu),把原始項目的影響降到最低,文章以后的部分也是基于這種插件體系進(jìn)行開發(fā)的
Openfire 是基于XMPP 協(xié)議的IM 的服務(wù)器端的一個實現(xiàn),雖然當(dāng)兩個用戶連接后,可以通過點對點的方式來發(fā)送消息,但是用戶還是需要連接到服務(wù)器來獲取一些連接信息和通信信息的,所以服務(wù)器端是必須要實現(xiàn)的。Openfire 也提供了一些基本功能,但真的很基本的!慶幸的是,它也提供插件的擴(kuò)展,像Spark 一樣,我同樣強(qiáng)烈建議使用插件擴(kuò)展的方式來增加新的功能,而不是修改人家的源代碼。
Smack 是一個XMPP 協(xié)議的Java 實現(xiàn),提供一套可擴(kuò)展的API,不過有些時候,你還是不得不使用自己定制發(fā)送的XML 文件內(nèi)容的方式來實現(xiàn)自己的功能
下圖展示了三者之間的關(guān)系:
從圖上可以了解到,client 端和server端都可以通過插件的方式來進(jìn)行擴(kuò)展,smack是二者傳遞數(shù)據(jù)的媒介。
開發(fā)你自己的XMPP IM 續(xù) - Spark 插件開發(fā) - [J2EE]
繼續(xù)3月18日介紹基于XMPP IM開發(fā)的那篇Blog,今天主要總結(jié)一下如何基于Spark 的插件架構(gòu)來新增客戶端的功能,這里列舉出一個獲取服務(wù)器端群組信息的實際例子,實現(xiàn)后的效果如下圖所示:
Spark 是一個基于XMPP 協(xié)議,用Java 實現(xiàn)的IM 客戶端。它提供了一些API,可以采用插件機(jī)制進(jìn)行擴(kuò)展,上圖中,“部門”部分就是使用插件機(jī)制擴(kuò)展出來的新功能。要想實現(xiàn)你的擴(kuò)展,首先要了解 Spark API的架構(gòu),其中最關(guān)鍵的是要了解它的工廠類,這些工廠類可以獲得Spark 提供的諸如XMPPConnection、ChatContainer 等實例,從而你可以實現(xiàn)獲取服務(wù)器的信息,與另外的Client 通信等功能。最核心的類是SparkManager,這個類是一系列工廠類的工廠類(呵呵,還真拗口)。它的getChatManager()、getSessionManager ()、getMainWindow() 、getConnection() 等方法分別可以獲得聊天管理器、會話管理器、主窗口、與服務(wù)器的連接等等非常有用的實例。基本上可以說SparkManager 是你與Spark 打交道的銜接口。其實,每一個Manager 都使用了單例模式,你也可以不通過SparkManager 來獲取它們,但筆者建議你從單一的入口著手,這樣有利于代碼的開發(fā)和維護(hù)。
接下來描述一下插件的開發(fā)流程:
1、創(chuàng)建插件配置文件 plugin.xml
2、實現(xiàn)你自己的Plugin 類的實現(xiàn)(如果你需要實現(xiàn)自己規(guī)定格式的XML 發(fā)送、接收和處理,那么你需要在這里注冊你的IQProvider,關(guān)于IQProvider 你可以查詢Smack API,簡單的來講是處理你自定義的IQ 處理器。)
3、打包你的插件(Spark 有自己的打包機(jī)制,我研究了半天才發(fā)現(xiàn)其中的玄機(jī),后面介紹)
4、部署你的插件(其實3、4兩步可以糅合在一起,當(dāng)然要利用Ant 啦)
好滴,下面結(jié)合一個實際的例子講述上面的四個步驟
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>
<!-- 關(guān)鍵是這里,這里要定義你的Plugin 類 -->
<class>com.im.plugin.IMPlugin</class>
<!-- 這里定義你使用的Spark 最低版本 -->
<minSparkVersion>2.5.0</minSparkVersion>
<os>Windows</os>
</plugin>
這是一個 plugin.xml 文件的內(nèi)容,插件體系會自動調(diào)用你在此文件中定義的Plugin 類,從而完成你自己擴(kuò)展的功能。最關(guān)鍵的部分我用紅色標(biāo)識出來了,要聲明你的插件擴(kuò)展類,采用完整的命名空間方式(包括包名),其余的部分結(jié)合我的注釋,大家應(yīng)該都能理解,就不做詳細(xì)的描述了。要注意的是plugin.xml 文件要放在項目的根目錄下,這是嚴(yán)格規(guī)定好的。
2、Plugin 類的實現(xiàn)
你的類首先要實現(xiàn)Spark 提供的Plugin 接口,然后實現(xiàn)它的一些方法。其中最主要的是實現(xiàn)initialize() 發(fā)放,在這里注冊你的的IQProvider
ProviderManager providerManager = ProviderManager.getInstance();
providerManager.addIQProvider("groups", "com:im:group", //1
new GroupTreeIQProvider());
System.out.println("注冊GroupTree IQ 提供者");
requestGroupTree();
上述的代碼,就在該類就是我實現(xiàn)的IMPlugin.initialize() 方法中的一小段,大概的含義是,先獲取ProviderManager(這個貌似不能從SparkManager 直接獲取),然后注冊一個GroupTreeIQProvider(自己創(chuàng)建的)這是一個IQProvider 的具體實現(xiàn),它用于解析像下面這樣的一個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>西安交通大學(xué)</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>電信學(xué)院</name>
<upGroup>1</upGroup>
<isLeaf>1</isLeaf>
<description>xjtu info</description>
</group>
</groups>
</iq>
可以看到,在注冊 IQProvider 的時候(代碼中標(biāo)注的1部分),需要你提供名稱和命名空間,我的XML 文件中的iq 下的第一個子節(jié)點是<groups> 所以我的名稱就寫“groups”,命名空間對應(yīng)于groups 節(jié)點的xmlns(XML Name Space)所以是“com:im:group”,其實IQProvider 中最關(guān)鍵的方法是parseIQ(XmlPullParser parser) 該方法就是解析XML,完成你的功能,并返回一個相應(yīng)的IQ 實例(這里可以把IQ 看做一個回饋的Model 類)。說到底實現(xiàn)基于XMPP 協(xié)議的IM 就是解析XML 文件,而這正是客戶端的IQProvider 和服務(wù)器端的IQHandler(下一篇文章會涉及到)所做的事情。
3、打包你的插件
現(xiàn)在該有的功能都實現(xiàn)了,那么就是打包了。這最好利用Ant 來完成,因為每次你都要打包,要部署,如果純手動的話,那也太不敏捷了,大大影響開發(fā)效率。
<?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 文件中的內(nèi)容。因為Eclipse 其實幫我自動完成了編譯的任務(wù),所以我也就省去了這寫編譯的步驟,最重要的是大家要看到“jar” 部分,Spark 打包的神秘之處也就在此,打兩次包首先把你的項目打包到本項目lib 文件夾下,比如說你的項目目錄是MyPlugin 那么,你就將你的類打包到MyPlugin/lib 目錄下,然后再次的打包,將所有的lib 文件夾下的內(nèi)容打包起來,記得這次要包含plugin.xml。也就是說,最后Spark 插件體系會讀取你的項目下的lib 文件夾下的內(nèi)容。這里我也有個疑問,我本來想每次打包后自動執(zhí)行bat 文件,啟動插件,看看效果,為啥死都調(diào)用不了呢,那段代碼在最后面,注釋掉了,誰能幫我解決,我請他吃飯滴!
4、最后就是發(fā)布了
其實我的發(fā)布很簡單,就是將這個打包好的jar 文件拷到Spark 本身的plugins 目錄下,每次啟動Spark 的時候,它會自動調(diào)用自定義的插件的。我這里用Ant 第二次jar 的時候,就自動拷貝過去了,這里用的是絕對路徑,所以你不能直接拷貝就用滴呦(是不是很丑陋呀,這段Ant 代碼)。
基本上客戶端的實現(xiàn)原理就是這樣的,只是有些地方需要特別注意,還有就是應(yīng)該利用像Ant 這樣的工具大大簡化開發(fā)步驟,加快開發(fā)效率。還有就是,我建議你在開發(fā)自己的插件的時候,多利用MVC 模式,尤其是在IQProvider 解析后,生成的部分可以實例化Model,然后你可以編寫自己的Manager 進(jìn)行這些Model 的處理。多寫Log,當(dāng)然Log4j 貌似不太起作用,那就System.out.println() 吧,哈哈!今天就寫到這里啦,偶有點累啦。
開發(fā)你自己的XMPP IM 續(xù) - Openfire 插件開發(fā) - [J2EE]
繼續(xù)上一篇的內(nèi)容,本篇文章介紹開發(fā)Openfire 的插件
這篇文章拖了很久了,呵呵,真是千呼萬喚始出來呀。Openfire 服務(wù)器端是支持插件開發(fā)的,開發(fā)過程可能會涉及到數(shù)據(jù)庫的操作,本篇文章專注于Openfire 插件的部分,對服務(wù)器端涉及到數(shù)據(jù)庫的開發(fā)只做簡單介紹。
Openfire 是一個用Java 實現(xiàn)的XMPP 服務(wù)器,客戶端可以通過IQ 的方式與其進(jìn)行通信(其實就是XML),客戶端和服務(wù)器之間的通信是依靠底層Smack 庫提供的各種功能來完成的。其實利用插件方式來擴(kuò)展Openfire 服務(wù)器端主要有兩種擴(kuò)展方式,一種是對服務(wù)器控制臺頁面進(jìn)行擴(kuò)展(不是本文的主要內(nèi)容),其實就是遵循Openfire 頁面的布局方式,進(jìn)行相應(yīng)的頁面擴(kuò)展和功能擴(kuò)展;另一種是對通信功能進(jìn)行擴(kuò)展。本文主要針對后者進(jìn)行具體的描述
本篇文章的結(jié)構(gòu)如下:
1、創(chuàng)建plugin.xml(這是整個插件最關(guān)鍵的文檔)
2、創(chuàng)建服務(wù)器插件實例(實現(xiàn)Plugin 接口的一個類還有一批IQHandler)
3、打包插件(Openfire 插件也有自己的打包方式)和部署插件
好滴,實刀實槍的來動手做吧
1、創(chuàng)建plugin.xml
初次開發(fā)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>
最重要的那一行我已經(jīng)標(biāo)記出來啦,就是你這個插件的初始化和垃圾清理類,例子中是在com.im.server.plugin 包中的GroupTreePlugin 類,下文會對這個類進(jìn)行詳細(xì)描述。其余的都是描述信息,只要你提供了正確的描述信息,一般都不會出錯。建議初次開發(fā)者,在寫完plugin.xml 文件后,寫一個簡單的Plugin 實例,并打印出一些信息,如果重新啟動Openfire 信息成功顯示,恭喜你,你已經(jīng)邁出一大步了!
2、實現(xiàn)Plugin 類和IQHandler
Plugin 類主要起到的作用是初始化和釋放資源,在初始化的過程中,最重要的的注冊一批IQHandler,IQHander 的作用有點類似于Spark 中的IQProvider,其實就是解析XML 文件之后,生成一些有用的實例,以供處理。下面分別給出一個Plugin 類的實例和IQProvider 的實例
GroupTreePlugin 類
/**
* 服務(wù)器端插件類
*
* @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 會自動監(jiān)聽相應(yīng)命名空間的IQ,然后進(jìn)行處理;由于這個Plugin 不需要做資源釋放的工作,所以在destroyPlugin() 方法中沒有任何內(nèi)容。具體的IQHander 類如下
GroupTreeIQHander
/**
* 處理客戶端發(fā)來的IQ,并回送結(jié)果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" 的實例進(jìn)行處理;還有一個最重要的方法:handleIQ() 該方法對包含指定命名空間的XML 進(jìn)行解析,然后返回一個解析好的IQ。其實我認(rèn)為,這個IQHandler 和IQ 的關(guān)系就是Controller 和Model 的關(guān)系(如果你了解MVC 的話,那么你一定知道我再說什么),只不過這里并沒有指定什么View,你完全可以把IQ 當(dāng)成Model 類進(jìn)行理解。在這里,我用了GroupManager 進(jìn)行了XML 的處理,因為我返回的IQ 內(nèi)容中要從數(shù)據(jù)庫讀取所有群組信息,所以轉(zhuǎn)交給GroupManager 進(jìn)行處理,你完全可以在這個方法中進(jìn)行具體的XML 處理,在這里,解析和創(chuàng)建新的XML 主要用到的是JDOM(如果你對Java 解析XML 有所了解,那真的太好了?。?。程序//1 處主要是獲取創(chuàng)建返回的IQ,并獲取原來IQ 的子元素(用于創(chuàng)建我們返回的IQ);程序//2 處很關(guān)鍵,如果你不調(diào)用createCopy 方法,程序會出錯(程序會死鎖還是什么,忘記咧,不好以西)。
這就是程序的主體部分,我在這里有一個建議,能不用Openfire 原始的程序函數(shù),就不要用它們。我的提取數(shù)據(jù)庫方式都是自己寫的Bean,這樣有利于你自己對程序的掌控,其實更有利于快速開發(fā)(這世道不是啥都講究敏捷么,哇哈哈)
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 的插件開發(fā)三部曲徹底結(jié)束了,希望你們對這個開發(fā)流程有了系統(tǒng)的了解。