spark client IM

SparkWeb 是由 Jive 軟件公司創建的基於Web的XMPP客戶端,採用 ActionScript 3 編寫,使用 Adobe 的 Flex API 。支持個人頭像裝扮 Avatars,vcards,多用戶聊天以及其他更多的XMPP的特性。


基於開源jabber(XMPP)架設內部即時通訊服務的解決方案


Spark client:::http://www.igniterealtime.org/projects/spark/index.jsp


Spark 是一個基於 XMPP 的 Java 即時通訊客戶端,非 Apache Spark,有興趣的可以繼續往下瞅。

前言

前兩個月的時間裏,有幸接觸到即時通訊領域的一些內容,認識了基於 XMPP 的開源 RTC Server Openfire以及開源的 XMPP 客戶端Spark。由於項目的需求,採用 Openfire + Spark 的方案來完成即時通訊,然後對 Spark 進行了必要的社會主義改造。

Openfire

由於該方面的應用大多侷限在企業內部,相關的資料不大好找,期間也遇到了不少的坑,好在經過艱辛的爬坑過程,大部分問題都得以解決。因此也希望以博文的形式給正在研究 Spark 的苦逼娃一點幫助。

這裏把重點放在 Spark 方面,主要是對 Spark 官方自帶的 Fastpath 插件進行了二次開發、漢化等工作,項目中還涉及到 Fastpath 的 Openfire 插件、以及 Fastpath 的 Web Client,這裏就不展開了。

環境搭建

獲取源代碼

Sarpk 官方正式版最新的是 2.6.3,於 2011 年發佈的,官網上也放出了一些開發預覽版SPARK NIGHTLY BUILDS。我們可以從 Spark 的官方 SVN http://svn.igniterealtime.org/svn/repos/spark/trunk 獲取 Spark 的最新開發代碼。代碼的下載速度較慢,慢慢等吧。下載完成後,讓我們看看具體的內容:

  • build裏面是也用來打包 Spark 的,構建工具是 ant。可以將 Spark 打包成各種平臺下的客戶端。

  • documentation裏面是附帶的文檔,裏面有 Spark 客戶端插件開發的一些指引文檔。

  • src裏面則是項目的源代碼,包括 Spark 主體以及其他官方插件的源代碼等。

新建項目

這裏以 MyEclipse IDE 爲例,新建一個 Spark 二次開發的項目。

  • MyEclipse 10.5

  • JDK 1.7.0_45

  • Windows 7 SP1

在 MyEclipse 中新建一個 Java 項目 SparkDemo,JRE 指定爲 1.7+,下拉欄裏沒有的,去 Configure JREs 裏添加,這裏最好選擇使用 Java 7,因爲在 Build 時,默認要求 JRE 最低版本是 Java 7,後面也會提到。
New Java Project

然後Finish完成項目創建。

接着將下載下來的 Spark 源代碼拖進項目的目錄下,這個時候會看到src目錄下會有很多報錯提示,沒關係。進入項目的 Build Path 設置裏,將src目錄從 Source 欄中移除。然後將 Spark 主體源碼src/java以及 Fastpath 插件源碼src/plugins/fastpath/src/java加入到 Source 欄中。如果自己開發 Spark 插件或者改造其他插件,設置類似。

Source Configuration

引入 lib

然後引入項目所需的庫文件也就是 jar 包。
包括 Spark 主體程序需要的庫文件:

  • build/lib

  • build/lib/dist

  • build/lib/merge

以及插件需要的庫文件:

  • src/plugins/fastpath/build/lib

  • src/plugins/fastpath/build/lib/dist

引入完畢後,SparkDemo 項目的錯誤提示就會自動去除了。

Build and Run

Run

進入 MyEclipse 的 Run Configurations,新建一個 Java Application 的 launch configuration:

  • Main:Main Class 設置爲org.jivesoftware.launcher.Startup

  • JRE:確認 JRE 版本爲 1.7+

  • Classpath:將 Spark 的 resources 以及插件的 resources 文件夾加入到 User Entries 中 (選擇User Entries, 點擊Advanced, 選擇Add Folders)

    User Entries Configuration

    • src/resources

    • src/plugins/fastpath/src/resources

  • Arguments:VM arguments 中加入

    • -Djava.library.path=build/lib/dist/windows 引入平臺運行環境,根據當前開發的運行環境進行選擇,如 win32 win64 Linux。按照自身情況引入相應的 dll 或者 so 等。必須添加。沒有的話,windows 平臺下會拋出com.lti.civil.CaptureException異常

    • -Dplugin=src/plugins/fastpath/plugin.xml 引入相應的插件配置 xml。

    • -Ddebug.mode=true 開啓 Debug 模式,按需添加

    • -Dsmack.debugEnabled=true 開啓 Smack Debug 模式,按需添加。添加後,在 Spark 啓動後,同時啓動 Smack 分析界面,可以用來記錄分析 Spark 通信過程的消息包。

      1
      2
      3
      4
      -Djava.library.path=build/lib/dist/windows-Dplugin=src/plugins/fastpath/plugin.xml-Ddebug.mode=true-Dsmack.debugEnabled=true

設置完畢後,我們就可以按照該 Run config 進行 Run 或者 Debug 了。運行後,就可以看到 Spark 的登錄界面了。

Run Spark

輸入可用的 Openfire 服務器以及用戶名密碼登錄後,即可看到 Spark 的主界面了。Openfire 服務器的搭建及簡單使用詳情請 Google,這裏就不予以說明。然而 Fastpath 插件,這裏沒有顯示出來,原因在後面會提到。

Spark
若開啓了 Smack Debug,還會出現 Smack Debug 窗口。

Smack Debug

運行時,可能會出現bin目錄拒絕訪問的異常,原因是 Spark 自帶的一個插件LanguagePlugin會在試圖在運行目錄下面尋找 spark.jar,但是調試時bin目錄下缺少 spark.jar。該問題在 spark 安裝版本時不會出現,調試時可以直接忽略,或者通過下面的build release生成target\build\lib\spark.jar,然後拷貝至 MyEclipse 的項目bin目錄下面。

Build

再來看看 Spark 客戶端的構建,進入項目的 Build 目錄中,查看 build.xml,裏面定義了各種 build target。可以打開 MyEclipse 的 Ant 窗口,將該 build 文件加入其中。
Ant Window
直接運行默認的 target:release。提示 Build Successful,項目目錄下新增了一個 target 文件夾。進入target/build/bin目錄,然後運行 bat 或者 sh 文件,即可啓動 Spark。

執行 Build 任務時,可能會遇到 Java 版本錯誤、編譯版本錯誤等問題,導致 Build 失敗。留意 Ant Build 文件中 Java Version、Ant Version、Javac Target 要求。

1
2
<fail if="ant.not.ok" message="Must use Ant 1.6.x or higher to build Spark"/><fail if="java.not.ok" message="Must use JDK 1.7.x or higher to build Spark"/>
1
2
3
4
5
<javac destdir="${classes}"
               debug="true"
               source="1.7"
               target="1.7"
            >


在 MyEclipse 中的 External Tool Configuration 中可以配置指定的 JRE 來運行 Ant 任務。

Build 之後,MyEclipse 中的 SparkDemo 項目出現了錯誤提示,該錯誤是 ANT 運行時產生的編譯警告,可以在 Problems 窗口中刪除該部分警告即可。

Delete Compiling Warnings

Build 文件中還定義了其他的 Target,如clean,Build 後運行 Clean 執行清理任務 ; installer.install4j , 配合 Install4j 生成 Spark 的安裝包。可以根據需要進行使用,使用過程中遇到問題可根據 Ant 報錯提示來進行調整。

## Fastpath

### 運行 Fastpath

在前面的運行過程中,並沒有看到 Fastpath 插件的加載,這個是因爲 Fastpath 插件中指定了 Spark 最低版本必須是 2.7.0 :src/plugins/fastpath/plugin.xml

1
2
3
4
5
<plugin>
    ...    <minSparkVersion>2.7.0</minSparkVersion>
    <java>1.7.0</java></plugin>


而我們的 Spark 版本定義仍然是 2.6.3:src/java/org/jivesoftware/resource/default.properties

1
2
3
APPLICATION_NAME = SparkSHORT_NAME = SparkAPPLICATION_VERSION = 2.6.3


我們這裏可以將 Spark 版本改爲 2.7.0。然後再運行程序,就能看見正常加載進 Fastpath 插件了。
Fastpath

有時運行 Spark 後會碰到 Spark 中出現 2 個相同的插件,此時清空 Spark 工作目錄再重新運行即可。Windows 下Win + R輸入 %appdata% 然後確定,進入 AppData 目錄,刪除 Spark 目錄即可。

在 Spark 代碼中,src/java/org/jivesoftware/spark/PluginManager.java 完成加載插件的任務。
loadPlugins方法中:

1
2
3
4
5
// Load extension pluginsloadPublicPlugins();// For development purposes, load the plugin specified by -Dplugin=...String plugin = System.getProperty("plugin");

開發時會以 2 種方式加載插件,就有可能會造成某些插件加載二次。
loadPublicPlugin方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Check for minimum Spark versiontry {
    minVersion = plugin1.selectSingleNode("minSparkVersion").getText();

    String buildNumber = JiveInfo.getVersion();    boolean ok = buildNumber.compareTo(minVersion) >= 0;    if (!ok) {        return null;
    }
}catch (Exception e) {
    Log.error("Unable to load plugin " + name + " due to missing <minSparkVersion>-Tag in plugin.xml.");    return null;
}

可以看到會對插件中定義的 Spark 最低版本進行檢查,另外還有 Java 版本、操作系統類型版本均有相應的匹配驗證。

Fastpath 漢化

Spark 的插件機制支持 i18n 國際化,Fastpath 也默認支持了 5 種語言。在目錄src/plugins/fastpath/src/resources/i18n下可以看到 Fastpath 的國際化文件,我們只需按照規範加入fastpath_i18n_zh_CN.properties就可以完成漢化操作。

消息擴展

Spark 客戶端實現的是 XMPP 的通信協議,構建於Smack API之上。Smack 對於消息 Packet 的靈活擴展也提供了很好的支持,給即時通訊帶來的功能性上的擴展。由於項目中要在在 Fastpath 應用中需要加入圖片內容處理,但是 Fastpath 應用場景是多人聊天,默認不支持發送圖片,因此這裏考慮擴展Message,定義自己的圖片Packet。本文的實現方式不太優雅:將圖片轉成 BASE64 編碼後,以文本格式傳遞消息,然後在接收方對消息進行還原,顯示圖片信息。這種方式只能支持體積較小的圖片。

定義 Packet

實現一個圖片消息類ImageMessage,實現PacketExtension,主要是定義自己的Packet的 root element name、namespace、屬性以及對應 Packet 的 xml 文本。root element name 以及 namespace 名稱可以隨便取,不要與其他默認消息產生衝突就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Overridepublic String getElementName() {    return "cImg";
}@Overridepublic String getNamespace() {    return "xxxx:xmpp:image";
}@Overridepublic String toXML() {
    StringBuilder sb = new StringBuilder();
    
    sb.append("<");
    sb.append(getElementName());
    sb.append(" xmlns=\"");
    sb.append(getNamespace());
    sb.append("\">");
    
    sb.append("<name>").append(name).append("</name>");
    sb.append("<type>").append(type).append("</type>");
    sb.append("<size>").append(size).append("</size>");
    sb.append("<encoded>").append(encoded).append("</encoded>");
    
    sb.append("</");
    sb.append(getElementName());
    sb.append(">");    
    return sb.toString();
}

接着實現一個圖片消息解析類ImageMessageExtensionProvider,實現PacketExtensionProvider,定義如何解析上面的自定義的Packet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Overridepublic PacketExtension parseExtension(XmlPullParser parser)        throws Exception {    
    // customized packet message
    ImageMessage message = new ImageMessage();    
    // parse raw XML stream and populate a message
    boolean done = false;    while (!done) {        int eventType = parser.next();        if (eventType == XmlPullParser.START_TAG) {            if (parser.getName().equals("name")) {
                message.setName(parser.nextText());
            }else if (parser.getName().equals("type")) {
                message.setType(parser.nextText());
            }else if (parser.getName().equals("size")) {
                message.setSize(Long.valueOf(parser.nextText()));
            }else if (parser.getName().equals("encoded")) {
                message.setEncoded(parser.nextText());
            }
        } else if (eventType == XmlPullParser.END_TAG) {            if (parser.getName().equals(message.getElementName())) {
                done = true;
            }
        }
    }    
    return message;
}

註冊自定義 Packet

在代碼中進行自定義 Packet 的註冊,可以選擇在加載 Fastpath 插件時完成這項工作。在src/plugins/fastpath/src/java/org/jivesoftware/fastpath/FastpathPlugin.java中的initialize方法中加入

1
2
ProviderManager.getInstance()
            .addExtensionProvider("cImg", "xxxx:xmpp:image", new ImageMessageExtensionProvider());

解析自定義圖片 Packet

由於 Fastpath 沒有自己處理消息並顯示,用的是 Spark 消息處理模塊。在src/java/org/jivesoftware/spark/ui/TranscriptWindow.java中的insertMessage方法中,能看到如下

1
2
3
4
5
6
7
8
// Check interceptors.for (TranscriptWindowInterceptor interceptor : SparkManager.getChatManager().getTranscriptWindowInterceptors()) {            boolean handled = interceptor.isMessageIntercepted(this, nickname, message);            if (handled) {                // Do nothing.
                return;
            }
}


因此可以通過消息攔截器來進行解析自定義的圖片消息 Packet。

這時我們實現一個TranscriptWindowInterceptor並註冊到ChatManager中即可,這裏直接讓FastpathPlugin.java實現了該接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Overridepublic boolean isMessageIntercepted(TranscriptWindow window, String userid,
        Message message) {
    
    String body = message.getBody();    
    if (ModelUtil.hasLength(body)) {        
        // 按照 root element 以及 namespace 篩選消息
        PacketExtension ext = message.getExtension("cImg", "xxxx:xmpp:image");        
        if (ext != null) {            // 轉換成自定義的圖片消息
            ImageMessage image = (ImageMessage) ext;            
            // 獲取到真正的圖片信息
            ImageIcon icon = new ImageIcon(Base64.decodeBase64(image.getEncoded().getBytes()));
            window.insertIcon(icon);            try {
                window.insertText("\n");
            } catch (BadLocationException e) {
                
            }
            
        }
        
    }    // 繼續處理其他
    return false;
}

初始化插件時,將該TranscriptWindowInterceptor加載進ChatManager當中。

1
SparkManager.getChatManager().addTranscriptWindowInterceptor(this);

這樣當我們的 Spark 加載了 Fastpath 插件後,就可以處理帶有 Image 擴展的消息了。

發送圖片消息

對於消息發送方,該怎樣構建自己的圖片擴展消息併發送出去呢?

構建圖片擴展:

1
2
3
4
5
6
7
8
9
10
// 將圖片文件用 Base64 轉碼byte[] bytes = item.get();
String encode = new String(Base64.encodeBase64(bytes));// 構造消息圖片擴展ImageMessage image = new ImageMessage();
image.setName(name);
image.setType(type);
image.setSize(size);
image.setEncoded(encode);


然後加入到消息中:

1
2
3
4
5
6
7
8
9
final Message chatMessage = new Message();chatMessage.setType(Message.Type.groupchat);chatMessage.setBody(image.getName() + " | " + image.getSize() + "B | " + image.getType());// 添加擴展
chatMessage.addExtension(image);String room = chat.getRoom();chatMessage.setTo(room);chat.sendMessage(chatMessage);

Fastpath 中接收到圖片如下:

Image Message

最後我們來看看搭載圖片的 XMPP 消息包的具體內容:

Image XMPP

在 message body 的後面出現了我們的自定義擴展內容,encoded元素內存放的則是圖片的 Base64 轉碼。

結語

開發過程中,由於參考資料較少,中間花費了不少時間,遇到幾大方面的問題

  • Spark、Openfire 等一系列環境搭建,調試準備

  • 定位需要進行修改的模塊

  • SWING 開發

  • Fastpath 處理圖片消息

  • 利用 install4j 打包 Spark 安裝程序

  • Fastpath Web Client 的二次開發

本文中提及到的內容只是其中的小部分。面對已有的大量源代碼,沒有技術文檔可讀,只能通過 IDE 去不斷的溯源查找每一個引用的含義及實現,最終勉強完成功能需求。遺憾的是,讀源代碼時沒有做好筆記,只顧着功能實現,剛剛準備寫此文時,就覺部分內容略感生疏。

參考

  1. Smack 解析自定義包結構

  2. LOAD TESTING OPENFIRE FASTPATH

  3. XMPP 實現羣聊截圖 (spark+openfire)

  4. Openfire 插件開發壞境配置指南

  5. Looking for fastpath_webchat.war source code svn ??

  6. Open Realtime.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章