kaywang 发表于 2019-1-30 11:24:56

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 进行了必要的社会主义改造。
http://blog2mih.qiniudn.com/img/of.png
  由于该方面的应用大多局限在企业内部,相关的资料不大好找,期间也遇到了不少的坑,好在经过艰辛的爬坑过程,大部分问题都得以解决。因此也希望以博文的形式给正在研究 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>

[*]  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,后面也会提到。
http://blog2mih.qiniudn.com/img/project-jre.png
  然后Finish完成项目创建。
  接着将下载下来的 Spark 源代码拖进项目的目录下,这个时候会看到src目录下会有很多报错提示,没关系。进入项目的 Build Path 设置里,将src目录从 Source 栏中移除。然后将 Spark 主体源码src/java以及 Fastpath 插件源码src/plugins/fastpath/src/java加入到 Source 栏中。如果自己开发 Spark 插件或者改造其他插件,设置类似。
http://blog2mih.qiniudn.com/img/source-config.png
引入 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>org.jivesoftware.launcher.Startup
[*]  JRE:确认 JRE 版本为 1.7+
[*]  Classpath:将 Spark 的 resources 以及插件的 resources 文件夹加入到 User Entries 中 (选择User Entries, 点击Advanced, 选择Add Folders)
http://blog2mih.qiniudn.com/img/UserEntries.png

[*]  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 的登录界面了。
http://blog2mih.qiniudn.com/img/RunSpark.png
  输入可用的 Openfire 服务器以及用户名密码登录后,即可看到 Spark 的主界面了。Openfire 服务器的搭建及简单使用详情请 Google,这里就不予以说明。然而 Fastpath 插件,这里没有显示出来,原因在后面会提到。
http://blog2mih.qiniudn.com/img/Spark.png
  若开启了 Smack Debug,还会出现 Smack Debug 窗口。
http://blog2mih.qiniudn.com/img/SmarckDebug.png
  运行时,可能会出现bin目录拒绝访问的异常,原因是 Spark 自带的一个插件LanguagePlugin会在试图在运行目录下面寻找 spark.jar,但是调试时bin目录下缺少 spark.jar。该问题在 spark 安装版本时不会出现,调试时可以直接忽略,或者通过下面的build>生成target\build\lib\spark.jar,然后拷贝至 MyEclipse 的项目bin目录下面。
Build
  再来看看 Spark 客户端的构建,进入项目的 Build 目录中,查看 build.xml,里面定义了各种 build target。可以打开 MyEclipse 的 Ant 窗口,将该 build 文件加入其中。
http://blog2mih.qiniudn.com/img/AntWin.png
  直接运行默认的 target:release。提示 Build Successful,项目目录下新增了一个 target 文件夹。进入target/build/bin目录,然后运行 bat 或者 sh 文件,即可启动 Spark。
  执行 Build 任务时,可能会遇到 Java 版本错误、编译版本错误等问题,导致 Build 失败。留意 Ant Build 文件中 Java Version、Ant Version、Javac Target 要求。
1  
2
1  
2
  
3
  
4
  
5

  在 MyEclipse 中的 External Tool Configuration 中可以配置指定的 JRE 来运行 Ant 任务。
  Build 之后,MyEclipse 中的 SparkDemo 项目出现了错误提示,该错误是 ANT 运行时产生的编译警告,可以在 Problems 窗口中删除该部分警告即可。
http://blog2mih.qiniudn.com/img/DeleteComplierWarnings.png
  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
  
    ...    2.7.0
  
    1.7.0
  而我们的 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 插件了。
http://blog2mih.qiniudn.com/img/Fastpath.png
  有时运行 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 -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("").append(name).append("");
  
    sb.append("").append(type).append("");
  
    sb.append("").append(size).append("");
  
    sb.append("").append(encoded).append("");
  

  
    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当中。
1SparkManager.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 中接收到图片如下:
http://blog2mih.qiniudn.com/img/image-chat.png
  最后我们来看看搭载图片的 XMPP 消息包的具体内容:
http://blog2mih.qiniudn.com/img/ImagePacket.png
  在 message body 的后面出现了我们的自定义扩展内容,encoded元素内存放的则是图片的 Base64 转码。
结语
  开发过程中,由于参考资料较少,中间花费了不少时间,遇到几大方面的问题

[*]  Spark、Openfire 等一系列环境搭建,调试准备
[*]  定位需要进行修改的模块
[*]  SWING 开发
[*]  Fastpath 处理图片消息
[*]  利用 install4j 打包 Spark 安装程序
[*]  Fastpath Web Client 的二次开发

  本文中提及到的内容只是其中的小部分。面对已有的大量源代码,没有技术文档可读,只能通过>参考

[*]  Smack 解析自定义包结构
[*]  LOAD TESTING OPENFIRE FASTPATH
[*]  XMPP 实现群聊截图 (spark+openfire)
[*]  Openfire 插件开发坏境配置指南
[*]  Looking for fastpath_webchat.war source code svn ??
[*]  Open Realtime.


页: [1]
查看完整版本: spark client IM