从3月开始研究Openfire,其实就是要做一套IM系统,也正是这个原因才了解到Openfire。之前还真没想过有这么多的开源产品可以做IM,而且也没想到XMPP这个协议竟然如何强大。看来还是标准为先,好的标准可以推动产业发展啊。
Openfire的搭建与简单的demo之前写过篇《技术笔记:XMPP之openfire+spark+smack》,当时主要关注的怎么让这套体系跑起来吧,只不过现在还是在这个阶段,只是多学了点东西留下点笔记吧。
1、对于XMPP的学习很重要
最开始觉得搭建一套Openfire+spark太简单啦,而且将spark的界面修改一下就可以变成一个新的产品,所以当时觉得XMPP协议这么高深的东西不用太深入。只不过随着简单的事情结束了才发现,最核心的还是协议本身,了解协议可以更了解系统的运作,才能体会到这套系统是有多复杂。当然是对于我来说有点复杂,特别是涉及到前后端结合设计与开始时。
为此我推荐一个国内的XMPP协议翻译网站:http://wiki.jabbercn.org/%E9%A6%96%E9%A1%B5。
当然如果英文好那就原版吧:http://xmpp.org/about/technology-overview.html
经过一段时间学习后,感觉QQ和微信在基础原理上真的和XMPP很类似,只是使用的协议格式有些差别,或许这就是即时通讯的抽象层次吧。但是使用XML这种标记语言是不是很浪费流量呢?虽然XMPP扩展起来非常方便,但是就这些标签也着实够大的,像平常的文字聊天时,或许中间标记产生的流量也和聊天内容相当了。毕竟我还没到这种需要考虑大流量的阶段,所以这只是一个想法而已。
2、Openfire的一些设计点与思路
Openfire的源代码整体看了看还是比较清晰的,扩展上支持插件与组件模式。在最近扩展的中发现openfire的源代码本身不太好去修改,依赖性很强,唯独模块间的依赖比较松散些,模块内的类依赖基本是紧耦合的。只不过Openfire可以通过插件扩展,对源代码本身的依赖就小了许多,所以说整体来说还是很不错的。
在Openfire中的插件扩展方式主要是:
- IQHandler
在XMPP协议中IQ包是指的信息/查询,可以用于服务器与客户端之间进行数据查询,Openfir中实现了一个IQRouter来处理IQ包。自然IQHandler就是具体的IQ包处理单元啦。IQHandler是基于namespace来进行拦截处理的,自定义自己的命名空间后即可。
IQHandler提供了两个抽象方法,用于派生类实现:
/** * Handles the received IQ packet. * * @param packet the IQ packet to handle. * @return the response to send back. * @throws UnauthorizedException if the user that sent the packet is not * authorized to request the given operation. */ public abstract IQ handleIQ(IQ packet) throws UnauthorizedException; /** * Returns the handler information to help generically handle IQ packets. * IQHandlers that aren't local server iq handlers (e.g. chatbots, transports, etc) * return <tt>null</tt>. * * @return The IQHandlerInfo for this handler */ public abstract IQHandlerInfo getInfo();
handleIQ方法就是解包和业务处理过程,最后返回结果包。
getInfo是用于返回当前IQHandler的命令空间
然后要使这个handler生效还需要注册到IQRouter,可以在插件启动时创建IQHandler的对象并add进去:
@Override public void initializePlugin(PluginManager manager, File pluginDirectory) { iqHandler = new IQGroupChatHander(); iqRouter = XMPPServer.getInstance().getIQRouter(); iqRouter.addHandler(iqHandler); } @Override public void destroyPlugin() { iqRouter.removeHandler(iqHandler); }
- Compoent
Compoent也是一种比较常用的扩展方法,可以通过对特定子域的包进行处理。比如MUC通过注册不同的Service,每个Service都有一个subdomain,系统会将不同的subdomain的数据包分发到专门服务中处理。这样就带来了一个好处,可以完全自定义一套子域的包处理业务,后面实现公众号订阅号就想通过这种思路来解决。而且Openfire还有远程组件的机制,可以扩展成为一个独立的业务系统,这样openfire可以只充当消息处理的核心。
具体的应用也比较简单,实现Component接口,并注册到ComponentManager中。而Component接口中最为重要的方法就是processPacket方法,代码如下:
/** * Processes a packet sent to this Component. * * @param packet the packet. * @see ComponentManager#sendPacket(Component, Packet) */ public void processPacket(Packet packet);
注意方法中的参数是Packet,这个表示所有子域下的通讯原语都可以用于这里处理。
注册与退出的方法如下:
componentManager = ComponentManagerFactory.getComponentManager(); try { componentManager.addComponent("subdomain", this); } catch (Exception e) { Log.error(e.getMessage(), e); System.err.println(e); } try { componentManager.removeComponent("subdomain"); } catch (ComponentException e) { Log.error(e.getMessage(), e); }
- PacketInterceptor
在Openfire中所以的传输都是基于packet,在packet上再派生出不同的通讯原语,如message、roster、JID、IQ等等。基于这个原理只要提供一套对于包的拦截机制就可以实现一套比较强大的扩展机制。在Openfire中就提供了这样的机制处理。在IQRouterPresenceRouterMessageRouter中都提供了对于包的拦截器。
// Invoke the interceptors before we process the read packet InterceptorManager.getInstance().invokeInterceptors(packet, session, true, false);
拦截器都注册在InterceptorManager中,在路由处理包时都会调用拦截器,上面的代码就是在路由中截取的代码例子。
那么同样的实现一个拦截器PacketInterceptor接口,并注册到InterceptorManager即可。
InterceptorManager.getInstance().addInterceptor(interceptor); InterceptorManager.getInstance().removeInterceptor(interceptor);
有了以上三种方法,Openfire可以发展出各种用法,所以官方自己也实现了放多插件供使用。在此也建议对于openfire的扩展最好还是使用插件吧,除非自己的定制要求很高,Openfire本身已经不适应了的。
我的要求基本都可以达成,而且这样以后升级新版本也非常简单,不会出现问题。
3、Spark的纠结
Spark同样出自于jivesoftware,但感觉扩展上就不那么好了。也许是我没有完全弄明白它的扩展原理吧。其实我的需求是重写Spark的UI,同时加入自己的功能,比如群、订阅号等。最开始想着Spark也是支持插件的,但是最后改代码时才发现,里面依赖太深了,基本上和界面相关的都存在依赖,最后可能都要重写一套。
其实在Spark中是有一个UIComponentRegistry类的,一些主要的界面都在这个类中注册的。但可恶的是这些注册的类大多都不能派生出新类来替换这些注册的类。比如
private static Class<? extends ChatRoom> chatRoomClass = ChatRoomImpl.class;
这是聊天窗口的注册类,那么如果我想写一个自己的聊天窗口,是不是直接把这个注册类替换即可呢?不行,因为在其他代码会尽然会这样使用
@Override public void filesDropped(Collection<File> files, Component component) { if (component instanceof ChatRoomImpl) { ChatRoomImpl roomImpl = (ChatRoomImpl) component; for (File file : files) { SparkManager.getTransferManager().sendFile(file, roomImpl.getParticipantJID()); } SparkManager.getChatManager().getChatContainer() .activateChatRoom(roomImpl); } }
在另一个类里尽然直接使用派生类进行了类型判断,这还只是一个点,类似的点太多了。所以最开始想着通过派生UIComponentRegistry中注册的类来达到预期目的已经不大可能了。再加上时间有限,也就懒得管这么多了,就让开发直接在源代码上改。
可恶的是2.7.7版本升级时发现代码大变,这个版本升级smack4.x版本,而且大量使用了1.8的新特性。所以又经过了一番代码合并才升级上来。另外说到smack基本不提供扩展,只提供事件的订阅。
只不过spark是跨平台的,很容易就能在mac下运行,而且代码是java的,暂时还不想抛弃掉,等将来考虑是不是再重写吧。