從3月開始研究Openfire,其實就是要做一套IM系統,也正是這個原因才了解到Openfire。之前還真沒想過有這麼多的開源產品可以做IM,而且也沒想到XMPP這個協議竟然如何強大。看來還是標准為先,好的標准可以推動產業發展啊。
Openfire的搭建與簡單的demo之前寫過篇《技術筆記:XMPP之openfire+spark+smack》,當時主要關注的怎麼讓這套體系跑起來吧,只不過現在還是在這個階段,只是多學了點東西留下點筆記吧。
最開始覺得搭建一套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擴展起來非常方便,但是就這些標簽也著實夠大的,像平常的文字聊天時,或許中間標記產生的流量也和聊天內容相當了。畢竟我還沒到這種需要考慮大流量的階段,所以這只是一個想法而已。
Openfire的源代碼整體看了看還是比較清晰的,擴展上支持插件與組件模式。在最近擴展的中發現openfire的源代碼本身不太好去修改,依賴性很強,唯獨模塊間的依賴比較松散些,模塊內的類依賴基本是緊耦合的。只不過Openfire可以通過插件擴展,對源代碼本身的依賴就小了許多,所以說整體來說還是很不錯的。
在Openfire中的插件擴展方式主要是:
在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也是一種比較常用的擴展方法,可以通過對特定子域的包進行處理。比如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); }
在Openfire中所以的傳輸都是基於packet,在packet上再派生出不同的通訊原語,如message、roster、JID、IQ等等。基於這個原理只要提供一套對於包的攔截機制就可以實現一套比較強大的擴展機制。在Openfire中就提供了這樣的機制處理。在IQRouter\PresenceRouter\MessageRouter中都提供了對於包的攔截器。
// 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本身已經不適應了的。
我的要求基本都可以達成,而且這樣以後升級新版本也非常簡單,不會出現問題。
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的,暫時還不想拋棄掉,等將來考慮是不是再重寫吧。