基础简介
XMPP
Extensible Messaging and Presence Protocol,简单的来讲,它就是一个发送接收处理消息的协议,但是这个协议发送的消息,既不是二进制的东东也不是字符串,而是XML。正是因为使用了XML作为消息传递的中介,Extensible 才谈的上,不是么?
IM
Instant Messenger,及时通信软件,就是大家使用的QQ、MSN Messenger和Gtalk等等。其中Gtalk 就是基于XMPP 协议的一个实现,其他的则不是。当前IM 几乎作为每个上网者必然使用的工具,在国外的大型企业中有一些企业级的IM应用,但是其商业价值还没完全发挥出来。设想既然XMPP 协议是一个公开的协议,那么每个企业都可以利用它来开发适合本身企业工作,提高自身生产效率的IM;甚至,你还可以在网络游戏中集成这种通信软件,不但让你可以边游戏边聊天,也可以开发出适合游戏本身的IM 应用,比如说一些游戏关键场景提醒功能,团队语音交流等等都可以基于IM来实现。
Spark Smack 和Openfire
开源界总是有许多有趣的东东,这三个合起来就是一个完整的XMPP IM 实现。包括服务器端——Openfire,客户端——Spark,XMPP 传输协议的实现——Smack(记住,XMPP 是一个协议,协议是需要实现的,Smack起到的就是这样的一个作用)。三者都是基于Java 语言的实现。
Spark 提供了客户端一个基本的实现,并提出了一个很好的插件架构,这对于开发者来说不能不说是一个福音。我强烈建议基于插件方式来实现你新增加的功能,而不是去改它的源代码,这样有利于你项目架构,把原始项目的影响降到最低。
Openfire 是基于XMPP 协议的IM 的服务器端的一个实现,虽然当两个用户连接后,可以通过点对点的方式来发送消息,但是用户还是需要连接到服务器来获取一些连接信息和通信信息的,所以服务器端是必须要实现的。Openfire 也提供了一些基本功能,但真的很基本的!庆幸的是,它也提供插件的扩展,像Spark 一样,同样强烈建议使用插件扩展的方式来增加新的功能,而不是修改人家的源代码。
Smack 是一个XMPP 协议的Java 实现,提供一套可扩展的API,不过有些时候,你还是不得不使用自己定制发送的XML 文件内容的方式来实现自己的功能
下图展示了三者之间的关系:
从图上可以了解到,client 端和server端都可以通过插件的方式来进行扩展,smack是二者传递数据的媒介。
Apache MINA
Openfire的通信处理基于Apache MINA框架实现。Apache MINA是一个网络应用程序框架,用来帮助用户简单地开发高性能和高可靠性的网络应用程序。它提供了一个通过Java NIO在不同的传输例如TCP/IP和UDP/IP上抽象的事件驱动的异步API。
Apache MINA 也称为:
●NIO 框架库
●客户端服务器框架库
●一个网络套接字库
MINA虽然简单但是仍然提供了全功能的网络应用程序框架:
●为不同的传输类型提供了统一的API:
○通过Java NIO提供TCP/IP 和UDP/IP支持
○通过RXTX提供串口通讯(RS232)
○In-VM管道通讯
○你能实现你自己的API!
●过滤器作为一个扩展特性; 类似Servlet过滤器
●低级和高级的API:
○低级: 使用字节缓存(ByteBuffers)
○高级: 使用用户定义的消息对象(objects)和编码(codecs)
●高度定制化线程模型:
○单线程
○一个线程池
○一个以上的线程池(也就是SEDA)
●使用Java 5 SSL引擎提供沙盒(Out-of-the-box) SSL ?TLS ?StartTLS支持
●超载保护和传输流量控制
●利用模拟对象进行单元测试
●JMX管理能力
●通过StreamIoHandler提供基于流的I/O支持
●和知名的容器(例如PicoContainer、Spring)集成
●从Netty平滑的迁移到MINA,Netty是MINA的前辈。
命名规则
Openfire中常见的类名后缀命名包括Starter、Plugin、Listener、Dispatcher、Handler、Manager、Provider,通常情况下,这些命名类包括如下意义:
XXStarter
系统启动类,如org.jivesoftware.openfire.starter.ServerStarter,调用其start()方法可启动系统应用。
XXListener
业务的最终处理类。
XXDispatcher
调度类,其中有很多关键方法,如addListener(),以组合的方式,为类内定义的静态Set
XXPlugin
实现Plugin接口的插件类,需实现initializePlugin(PluginManager manager, File pluginDirectory)方法和destroyPlugin()方法。在其初始化方法中调用Dispatcher实现类的addListener()方法如PropertyEventDispatcher.addListener(this)。
XXProvider
实现面向接口编程方式的接口类,通过反射机制创建具体实现类的对象,反射类名配置在ofproperty表对应的记录propvalue属性中。若没有相关配置,则调用默认实现类,默认实现类类名命名规则为DefaultXXProvider。
XXHandler
实际处理类,以ConnectionHandler为例,在org.jivesoftware.openfire.spi. ConnectionManagerImpl类的startClientSSLListeners(String localIPAddress)方法中,有这样一段代码:sslSocketAcceptor.bind(new InetSocketAddress(bindInterface, port), new ClientConnectionHandler(serverName));其中bind方法的第二个参数是新创建的一个ClientConnectionHandler的实例,而它就是ConnectionHandler的一个子类。
系统配置项
Openfire的系统配置项采用文件结合数据库表的方式配置,也有部分默认配置项通过Java硬编码方式配置(如org.jivesoftware.openfire. ConnectionManager接口类中定义的DEFAULT_PORT、DEFAULT_SSL_PORT、DEFAULT_COMPONENT_PORT等),Openfire中比较重要的配置位置包括:
一、src/conf目录下的openfire.xml配置文件。该配置文件为系统核心配
置文件。在第一次启动Openfire并通过管理控制台完成安装配置后会往该配置文件中填入相应的配置信息。
二、plugin.xml配置文件。该配置文件为各插件包下的核心配置文件,由它
确定插件核心处理类和相应页面插件的展现等。配置项及含义详见官方插件开发说明部分。
三、web.xml和web-custom.xml配置文件。用于配置servlet和用户自定义
servlet(插件页面用,放在插件对应目录下)。
四、ofproperty中的各条记录,该表中包括两个字段name和propvalue,分
别代表配置项名和配置项值。
系统启动流程
系统启动时调用ServerStarter类中的start()方法,通过反射加载org.jivesoftware.openfire.XMPPServer类文件,创建实例时调用其构造函数,在其构造函数中调用其start()方法实际启动服务应用程序。Start()方法中首先调用verifyDataSource()方法验证并确保数据库可以访问,然后会调用
loadModules();initModules();startModules();方法来对Module接口的实现类的各子类进行操作,依次完成模块的加载、初始化和启动操作。loadModules()方法中会调用loadModule(String module)方法通过反射加载各模块类,参数字符串module为对应的模块核心处理类的类名,如AdHocCommandHandler。现以AdHocCommandHandler为例对接下来的处理流程进行说明。通过loadModule创建AdHocCommandHandler类实例时调用其构造函数,在构造函数中初始化了其私有AdHocCommandManager对象。在initModules()时调用AdHocCommandHandler实例的initialize(XMPPServer server)方法对其私有属性对象进行初始化。然后调用start()方法,调用addDefaultCommands方法添加命令并启动命令(通过调用startCommand(AdHocCommand command)方法实现)。
网络处理
消息监听服务
SSL等监听服务的调度在ConnectionManagerImpl类中实现。ConnectionManagerImpl.createClientSSLListeners()方法启动SSL监听
消息封装
信息处理采用XML节的方式传递信息,消息封装通常采用IQ、Message、Presence。
Openfire消息包接受处理流程
数据库处理
Openfire的数据库处理采用直接调用JDBC 的方式。核心类为org.jivesoftware.database.DbConnectionManager。数据库的处理与业务处理耦合,没有划分出专门的业务逻辑层。
ConnectionProvider
此类为数据库提供者接口,如需连接mysql、hsqldb等数据库,需首先实现些接口,
处理方式
通常直接调用XXManager中的实例方法,XXManager中又调用的是对应的接口XXProvider的方法,实际操作在该接口的实现类中实现。实现类是动态绑定的(默认的实现类通常命名规则为DefaultXXProvider),在运行时根据ofproperty表中对应配置项值选择。下面以添加用户组为例进行说明。
首先获得GroupManager的一个实例,在调用其构造函数时调用initProvider()方法,在该方法中获取数据库中配置项的值,若不为空则根据该值通过反射机制获取GroupProvider接口的实现类实例对象;若为空则以DefaultGroupProvider作为GroupProvider接口的实现类并创建实例对象,然后调用GroupProvider. createGroup(String name)方法完成业务操作。
常用类
org.jivesoftware.database.DbConnectionManager
连接管理类
org.jivesoftware.util.JiveGlobals
通常用于操作ofproperty表中记录
openfire数据结构
数据库表
以下是一个说明每个表格的Openfire数据库架构。黄色行表示主键。
WEB服务器
Openfire采用内置的jetty作web服务器,在启动AdminConsolePlugin插件时调用startup()方法启动jetty服务器,9090为其明文端口,9091为其加密端口。
页面处理
Openfire没有采用现在很流行的技术架构(SSH),只使用JSP+JavaBean,但是它有自己的系统设计,就连日志都是自己做的,没有使用我们熟悉的log4j。
现有的Openfire管理控制台可采用插件方式进行扩展(详见插件开发说明部分介绍),页面采用Jsp方式实现,页面直接调用业务处理逻辑类(通常命名为XXManager)的实例方法,通常通过request对象封装的方式传递页面展现判定变量,常出现本页跳转。每个插件可定义自己的Servlet类和web.xml及
web-custom.xml配置文件。
采用装饰框架方式展现页面,decorator页面有两个,即src/web/decorators 目录下的两个页面main.jsp和setup.jsp。采用自定义的admin标签实现,标签库admin.tld放置在src/web/WEB-INF目录下,标签解析类放置在org.jivesoftware.admin包下,有SidebarTag、SubnavTag、SubSidebarTag、TabsTag四个解析类。在调用loadPlugin()方法进行插件加载时,解析插件的plugin.xml配置文件,将获取的相关信息封装在AdminConsole类的generatedModel对象中,后期通过插件解析类提取该对象中的数据并配合sitemesh装饰器进行页面展现。详见“使用dom4j设计Openfire式导航菜单”部分相关介绍。
插件开发
Openfire Plugins加载流程
官方插件开发说明
所有插件都存放在openfire根下的plugins目录下。当一个插件被以JAR 或WAR文件发布时,他自动扩展为一个文件夹。插件目录结构如下所示:
Plugin Structure
myplugin/
|- plugin.xml <- 插件定义文件
|- readme.html <- 可选的插件自己述文件,它将被显示给最终用户。 |- changelog.html <-可选的插件版本日志文件,它将被展现给最终用户。 |- logo_small.gif <- 可选的与插件关联的小图标(16x16)文件(也能为png文件)
|- logo_large.gif <-可选的与插件关联的大图标(32x32)文件(也能为png 文件)
|- classes/ <- 你的插件需要的资源文件(如properties文件)
|- database/ <- 可选的你的插件需要的数据库schema文件
|- i18n/ <- 可选的i18n文件,它们为插件提供国际化支持
|- lib/ <- 你的插件需要的库(JAR文件)
|- web <- 需要集成到管理控制台中的各类资源(如果有的话) |- WEB-INF/
|- web.xml <- 配置jsp调度的web.xml配置文件
|- web-custom.xml <- 可选的用户定义的web.xml文件,用于调度自定义servlets
|- images/
若插件需要为Openfire的管理控制台添加内容,则web文件夹必须存在。具体内容详述如下。
Plugin.xml文件指定了主插件类,下面是一个例子。
Itvi plugin.xml
description="Click to manage..."> url="sample.html" description="Click to administer settings for my plugin" />
各元数据域能在plugin.xml文件中进行设置:
?name –插件名.
?description –插件描述.
?author –插件作者.
?version –插件版本.
?date –插件版本生成日期。该日期必须为MM/dd/yyyy格式, 如07/01/2006.
?url –关于该插件的更多详细信息可以从该地址获取.
?minServerVersion –为运行该插件所需的最低的Openfire软件版本(Openfire 2.1.2及后续版本支持该选项).如果服务器版本低于需要的最低版本,插件将不会启动.
?databaseKey –如果插件需要它自己的数据库表,必须将databaseKey 元素设置为一个schema key name(通常与插件同名). 然后需将所有需支持数据库类型的数据库schema 文件放到插件的database文件夹下.
例如,给一个关键字“foo”,那么schema文件需命名为"foo_mysql.sql", "foo_oracle.sql"等.我们建议你在命名你的表时加上前缀"of"
(openfire),以避免与其他可能使用同一数据库的其他应用系统需要的表
产生冲突.关于版本的描述信息需添加到ofVersion表中,以对应的key
做标识,这样可以跟踪schema版本信息,如:
INSERT INTO ofVersion (name, version) VALUES ('foo', 0);
?databaseVersion –数据库schema版本(如果定义了数据库schema)。
有数据库schema的新插件的版本从0开始编号。如果以后的插件版本需
要更新schema,这些更新能通过在database/upgrade目录下为各版本创建子目录的方式来定义。例如目录database/upgrade/1和
database/upgrade/2将包括如 "foo_mysql.sql"和"foo_oracle.sql"
这样的包含各版本相应的数据库改变信息的脚本.这些脚本中均需要更新
ofVersion表的信息,如:
UPDATE ofVersion set version=1 where name='foo';
?parentPlugin –父插件名 ("foo.jar"插件相应的为"foo").当一个插件有一个父插件,将不会创建一个新的class loader,相应地替换为使
用父插件的class loader。这使得插件间能更紧密地联合工作。子插件
不能脱离父插件独立工作。
?licenseType –指定许可类型信息。有效值包括:
o"commercial": 插件被发布为商业性插件。
o"gpl": 插件被发布为遵循GNU Public License (GPL)协议。
o"apache": 插件被发布为Apache license系列
o"internal": 插件仅供内部使用,且不能被重新发布。
o"other": 插件被发布为与其他目录下的版本限制不同。License 许可信息需在插件自述文件中进行详细描述。
如果许可类型未设置,默认为other。
为了给最终用户提供插件的额外信息,可以为插件添加一些额外文件(全放置在插件的主目录下):
?readme.html –可选的插件自述文件,它的信息将展现给最终用户。
?changelog.html --可选的插件版本日子文件,它将被展现给最终用户。
?logo_small.png --可选的与插件关联的小图标(16x16)文件(也能为gif文件)。
?logo_large.png --可选的与插件关联的大图标(32x32)文件(也能为gif文件)。
插件必须实现Plugin接口,且有一个默认构造函数。Plugin接口包含了初始化和销毁插件的方法。
Sample plugin implementation
package org.example;
import org.jivesoftware.openfire.container.Plugin;