一、Smack库概述
Smack是一个开源、易用的XMPP/Jabber客户端库,它使用Java语言开发,由Jive Software开发。
Smack的优点是编程简单。
Smack的缺点是其API并非为大量并发用户设计,每个客户都要1个线程,占用资源相对较大,因此用Smack做模拟测试时,1台机器只能模拟有限(数千个)客户。
截止2015年10月25日,Smack库已经发展到4.1.4版。最新的好消息是Smack在4.1.0版后将直接支持Android系统,而无需再使用以前的Smack移植版aSmack库了。Smack库源码托管于GitHub,主页见: https://github.com/igniterealtime/Smack/
1.1 Smack 4的改变
Smack库从3.4版发展到4.0.x版后,其API有较大的变化,主要有:
1. 把Connection类重命名为XMPPConnection类,XMPPConnection类是XMPPTCPConnection类和XMPPBOSHConnection类的父类。
2. 把各种Provider类进行了分包
3. keep-alive(持久连接)机制从smack-core库移到了smack-extensions库,keep-alive机制现在由PingManager类提供。
4. PrivacyList类的toString()方法重命名为getName()
5. 当Chat实例的所有引用都撤掉后,应该调用Chat.close()方法
否则Chat对象会有内存泄露的隐患,直到ChatManager对象被垃圾回收器回收后内存泄露隐患才会消失。
6. ServerTrustManager类被移除了,如果要使用带SSL认证的XMPP,你只需提供自己的SSLContext对象给ConnectionConfiguration对象即可。
7. Packet.setProperty()从smack-core库移到了smack-extensions库,其API现在可以在org.jivesoftware.smackx.jiveproperties包中找到。
8. Connection.getAccountManager()方法现在改成了AccountManager.getInstance(XMPPConnection)方法
9. 异常API做了改进
10.ToContains过滤器被移除了
1.2 Smack库的组成
Smack库可以内嵌到任意的Java应用程序中。Smack库有数个JAR文件组成,非常具有灵活性。
1. smack-core.jar提供了核心XMPP功能。都是XMPP RFC规范定义的XMPP特性。
2. smack-extensions.jar支持许多由XMPP Standards Foundation定义的扩展(XEP)功能。包括群聊、文件传输、用户搜索等等。以后可查看文档《扩展手册》:https://github.com/igniterealtime/Smack/blob/master/documentation/extensions/index.html(目前还是无效的)
3. smack-experimental.jar支持许多由XMPP Standards Foundation定义的体验性(XEP)功能。其API和功能特性都被认为是不稳定的。
4.smack-legacy.jar支持许多由XMPP Standards Foundation定义的遗留(XEP)功能。
5. smack-bosh.jar支持BOSH通信(XEP-0124规范定义的)。此代码被认为处于Beta阶段。
6. smack-jingle.jar支持Jingle。此代码很老,目前处于无维护的状态。
7. smack-resolver-dnsjava.jar支持对DNS SRV记录的解析,主要用于那些不支持javax.naming API的平台。
8. smack-debug.jar用于协议流量的增强型GUI调试器。当调试模式开启后,如果它在类路径下,它会自动被使用。以后可查看文档《调试模式》:https://github.com/igniterealtime/Smack/blob/master/documentation/debugging.html(目前还是无效的)
二、Smack的配置
Smack的初始化过程涉及到2阶段的调用。
1.初始化系统属性通过SmackConfiguration类初始化所有的系统可访问属性,这些属性都是通过getXXX方法取回属性值的。
2. 初始化启动类如果继承了SmackInitializer接口后,都可以在调用initialize()方法后得到初始化,这意味着得到初始化的类在启动后都是活动的。如果没有继承SmackInitializer接口,那么要实现初始化,必须要放置一个静态代码块来实现——他在类装载时会自动执行。初始化是通过配置文件来完成的。默认情况下,Smack会载入Smack JAR文件中内嵌的配置文件(它位于org.jivesoftware.smack/smack-config.xml)。这个指定的配置文件包含了一系列需载入初始化的类列表。所有的管理器类型的类都需要被初始化,这些管理器类就包含在上面所说的初始化列表中。
三、XMPPConnection管理
3.1 创建连接
org.jivesoftware.smack.XMPPConnection类可管理到XMPP服务器的连接,它默认的连接实现类是org.jivesoftware.smack.XMPPTCPConnection。它主要使用两个构造方法,
一个是XMPPTCPConnection(StringserverName)方法,参数为服务器名。连接会使用所有默认的设置,有:
1)执行DNSSRV查询,找到服务器确切的地址和端口(通常是5222)。
2)与服务器协商最大数安全,包括TLS加密。但如果有必要,连接会回落到较低的安全设置。
3)XMPP资源名“Smack”会被用于连接。
第二个是XMPPTCPConnection(ConnectionConfigurationcc)构造器,它会指定高级的连接设置。其中包括:
1)手动指定服务器地址和端口,而不是通过DNSSRV查询。
2)能开启连接压缩。
3)指定自定义的连接资源名(如Work或Home)。用户到服务器的每一个连接都必须有唯一的资源名。比如对于用户"jsmith@example.com",完整的带资源的地址应该是"jsmith@example.com/Smack"。通过携带唯一的资源名,用户可以同时从不同的位置登录到同一个服务器,这适用于多设备的情况。每一个资源使用的在线优先级值:用于决定由哪一个带资源的指定连接来接收到地址"jsmith@example.com"的消息。
第一种连接方式:
boolean target = false;
AbstractXMPPConnection conn = new XMPPTCPConnection(username, password, serverName);
try {
conn.connect();
target = conn.isConnected();
if(target){
System.out.println("XMPP 服务器连接成功");
}else{
System.out.println("XMPP 服务器连接不成功");
}
} catch (SmackException | IOException | XMPPException e) {
e.printStackTrace();
}
第二种连接方式:
boolean target=false;
XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
.setUsernameAndPassword(username, password)
.setServiceName(serverName)
.setHost(serverName)
.setPort(5222)
.build();
AbstractXMPPConnection conn = new XMPPTCPConnection(config);
try {
conn.connect();
target = conn.isConnected();
if(target){
System.out.println("XMPP 服务器连接成功");
}else{
System.out.println("XMPP 服务器连接不成功");
}
} catch (SmackException | IOException | XMPPException e) {
e.printStackTrace();
}
3.2 连接和关闭连接
//为新连接创建配置
ConnectionConfigurationconfig = new ConnectionConfiguration(“jabber.org”, 5222);
AbstractXMPPConnectionconn = new XMPPTCPConnection(config);
//连接到服务器
conn.connect();
//登录到服务器
conn.login(“username”,“password”,“SomeResource”);
…
//关闭连接
conn.disconnect();
默认情况下,一旦连接断开,Smack会尝试重建连接。使用ConnectionConfiguration类的setReconnectionAllowed(Boolean)方法可以开启或关闭重连的功能。重连管理器会立即尝试重连到服务器,并且会增加延时设置,以便提高重连的成功率。在重连管理器正在等待下一次重连的期间,如果你想强制重连,可以使用AbstractXMPPConnection类的connect()方法,它会尝试建立一个新连接。如果手动尝试也失败了,那么重连管理器会继续重连的工作。
四、使用Chat消息通信
来回收发消息是即时通信的核心功能。尽管单条消息是以包的形式发送和接收的,通常还是把他视为聊天的消息字符串,使用org.jivesoftware.smack.Chat类。
4.1 Chat类
一个聊天Chat会在两个用户之间创建一个消息线程(通过线程ID)。下面的代码片段演示了怎样创建一个新聊天,然后向用户发送一条文本消息:
//假设已经创建了一个名为"connection"的XMPPConnection
ChatManager chatmanager = connection.getChatManager();
Chat newChat = chatmanager.createChat("jsmith@jivesoftware.com", newMessageListener(){
public void processMessage(Chat chat,Message message){
System.out.println(“Receivedmessage: “+ message);
}
});
try{
newChat.sendMessage(“Howdy!”);
}catch(XMPPExceptione){
System.out.println(“Error Deliveringblock”);
}
Chat.sendMessage(String)方法可以方便地创建一个消息Message对象,用字符串参数设置消息正文Body,然后发送消息。在某些情况下你可能希望在发送消息前设置额外的值,使用Chat.createMessage()方法和Chat.sendMessage(Message)方法,如下面的代码片段所示:
Message newMessage = new Message();
newMessage.setBody(“Howdy!”);
message.setProperty(“favoriteColor”,“red”);
newChat.sendMessage(newMessage);
前面的例子中,我们可以注意到,在创建聊天Chat时指定了一个消息监听器MessageListener,在任意时刻,当来自其它用户的聊天消息到达后,消息监听器会得到通知。下面的代码片段使用了监听器做鹦鹉学舌,它会回显来自其他用户传递的消息。
//假设在聊天Chat中已经设置了消息监听器MessageListener
publicvoid processMessage(Chat chat, Message message){
// 把用户发送的消息内容发送给用户
chat.sendMessage(message.getBody());
}
4.2 来电聊天
当提示有另一个用户的聊天消息到了后,设置有轻微的不同,因为你是首次接收到聊天消息。取代明确地创建一个Chat来发送消息,当ChatManager创建了Chat实例后,你需要注册处理新创建的Chat实例。ChatManager会通过线程ID找到匹配的Chat,如果Chat不存在,那么它会创建一个新Chat对象来匹配。要得到这个新Chat,你必须注册来得到通知。可以注册一个消息监听器来接收所有要到来的消息。
//假定已经创建了名为”connection”的XMPPConnection
ChatManager cm = connection.getChatManager().addChatListener(
new ChatManagerListener(){
@Override
public void chatCreated(Chat chat, BooleancreatedLocally){
if(!createdLocally)
chat.addMessageListener(newMyNewMessageListener());
}
});
除了基于线程的Chat消息,也有一些客户端不发送线程ID作为Chat的一部分。要处理这种情况,Smack会基于JID尝试匹配接收的消息到最匹配现有的Chat。它会尝试用完整的JID来查找Chat,如果搜不到,再尝试用基本的JID来查找Chat。如果找不到现有的Chat来匹配,那么会创建一个新Chat。
五、名单Roster和在线状态Presence
名单可以让你跟踪其他用户是否在线,而且名单可以让你把用户组织到群组,比如朋友群或工作群。而其它的即时通信IM系统则把名单Roster视为好友列表、联系人列表等等。
5.1 名单条目
Roster用于跟踪其他用户是否在线。用户的联系人可以以分组的方式进行组织,比如“好友”、“同事”。然后就可以查看组中的每个用户是否在线了。要检索Roster,使用XMPPConnection.getRoster()方法。Roster类允许你查找所有的Roster实体,以及他们属于哪个组,每个实体当前的在线状态。名单中的每一个用户都由RosterEntry来表示,它包括:
1)一个XMPP地址(比如”jsmith@example.com”)
2)你为用户编写的备注姓名(比如”Joe”)
3)名单中的群列表。如果名单的条目不属于任何群组,那么它被称为"unfiledentry"。
下面的代码片段会打印名单中所有的条目:
//假定已经创建了名为”connection”的XMPPConnection
Rosterroster = connection.getRoster();
Collection<RosterEntry>entries = roster.getEntries();
for(RosterEntryentry : entries){
System.out.println(entry);
}
还有获取单个条目的方法、获取"unfiledentry"的方法,获取一个群或所有群的方法。
5.2 在线状态
名单中的每个条目都有一个与之相关的在线状态。Roster.getPresence(Stringuser)方法会返回一个表示用户是否在线的Presence对象。如果为空是你还没有订阅用户是否在线的返回。注意:通常情况下,在线状态的订阅总是绑定到名单中的用户,但这并不适应所有的情况。
一个用户的在线状态要么是在线,要么是离线。当用户在线时,他们的在线状态还可以包含扩展的信息,比如用户当前正在做什么,用户是否愿意被打扰等等。具体参考Presence类。
5.3 监听名单Roster和在线状态Presence的改变
Roster类的典型应用场景是以树状结构显示用户群和列表,并且用户列表中包含用户是否在线的状态。比如,参考下图所示的一个ExodusXMPP客户端的Roster。
在线状态的信息可能会经常变化,Roster条目也可能经常修改或删除。要监听Roster和Presence数据的变化,你应该使用RosterListener。要得到Roster改变的所有提醒,那么必须在登录XMPP服务器之前注册RosterListener。下面的代码片段注册了一个Roster的RosterListener,它能够在标准输出中打印任何Presence的改变。一个标准的客户端可以使用类似的代码用变化的信息来更新Roster界面。
//假定已经创建了名为”connection”的XMPPConnection
Roster roster = connection.getRoster();
roster.addRosterListener(new RosterListener(){
// 忽略事件
public void entriesAdded(Collection<String> addresses){}
public void entriesDeleted(Collection<String>addresses){}
public void entriesUpdated(Collection<String> addresses){}
public void presenceChanged(Presencepresence){
System.out.println(“Presencechanged: “+ presence.getFrom() + “ “ + presence);
}
});
5.4 添加Entries到Roster
Roster和Presence使用一种基于权限的模型,用户必须得到其他人的许可才能把这些人添加到Roster。这样可以保护用户的隐私,确保了只有获得同意的用户才能查看到他们的Presence信息。因此,当你想添加某个用户到你的Roster中,必须得到该用户接受你的请求才可以。
如果有用户请求订阅你的在线状态Presence,这个用户必须先把你添加到他的Roster,因此他会发起请求,你必须选择接受或拒绝该请求。Smack通过以下三种方式来处理Presence的预订请求:
1)自动接受所有Presence的预订请求
2)自动拒绝所有Presence的预订请求
3)手动处理每一个Presence预订请求
这三种方式可以通过Roster.setSubscriptionMode(intsubscriptionMode)方法来设置请求的处理方式。简单的客户端通常使用第一种自动方式处理预订请求,而功能比较全的客户端应该选择第三种手动处理请求的方式,让终端用户自行决定是接受请求或是拒绝请求。如果使用手动方式,应该注册一个PacketListener来监听Presence.Type.SUBSCRIBE类型的Presence包。
六、读写Packet(数据包)
从客户端发送到XMPP服务器的每一条消息都称为一个Packet(数据包)。org.jivesoftware.smack.packet库中包含了XMPP支持的消息Message、在线状态Presence、IQ)三种不同的基本数据包类型的封装类。而像Chat或GroupChat这样的类则提供了更高层的结构来管理数据包的自动创建和发送。但是,开发者还是可以直接创建和发送数据包的。下面的代码就是修改自己的在线状态,让其他人知道你不在线。
//假设已经创建了一个名为"connection"的XMPPConnection
Presence presence = new Presence(Presence.Type.unavailable);
presence.setStatus("Gone fishing");
connection.sendStanza(presence);
6.1 PacketListener与PacketCollector
Smack提供了两种读取到来的数据包的方式:
org.jivesoftware.smack.PacketCollector(包收集器):该类提供synchronously(同步)方法,等待接受数据包(Packets).
org.jivesoftware.smack.PacketListener(包监听器): 该接口提供asynchronously(异步)方法,等待结束数据(Packets)。
两者都使用PacketFilter实例来判断应该处理哪一个数据包。PacketListener(包监听器)用于事件风格的编程,而PacketCollector(包收集器)有一个数据包的结果队列,你可以做轮询或阻塞等操作。也就是说,如果你想在数据包到来时执行一些动作,那么包监听器很适合。如果你想等待指定的数据包的到来,那么包收集器很适合。包收集器和包监听器都使用Connection连接实例创建。
数据包集合(PacketCollector)和数据包接口(PacketListener)是通过XMPPConnection实例对象创建。
6.2 拦截器
org.jivesoftware.smack.filter.StanzaFilter接口可以决定,那些特定的数据包会被传递到PacketCollector 或者PacketListener。
许多预先定义的拦截器都实现了org.jivesoftware.smack.filter接口。
下面的代码片段,演示了注册的数据包监听器和数据包集合
// Create a packet filter to listen for new messages from a particular
// user. We use an AndFilter to combine two other filters._
StanzaFilter filter = new AndFilter(new StanzaTypeFilter(Message.class),
new FromContainsFilter("mary@jivesoftware.com"));
// Assume we've created an XMPPConnection name "connection".
// First, register a packet collector using the filter we created.
PacketCollector myCollector = connection.createPacketCollector(filter);
// Normally, you'd do something with the collector, like wait for new packets.
// Next, create a packet listener. We use an anonymous inner class for brevity.
PacketListener myListener = new PacketListener() {
**public** **void** processPacket(Packet packet) {
// Do something with the incoming packet here._
}
};
// Register the listener._
connection.addPacketListener(myListener, filter);
标准段落(字符)拦截器
smack 类库中已经包含很多包拦截器,你也可以创建属于自己的拦截器,只需要代码实现StanzaFilter接口,默认的拦截器包含如下:
- StanzaTypeFilter --特定类型的数据包过滤器
- StanzaIdFilter -- 特定数据包标识筛选器
- ThreadFilter -- 特定线程标识数据包过滤器
- ToContainsFilter -- 特定发送地址数据包过滤器
- FromContainsFilter -- 特定接受地址数据包过滤器.
- StanzaExtensionFilter -- 特定包扩展数据包过滤器
- AndFilter -- implements the logical AND operation over two filters.
- OrFilter -- implements the logical OR operation over two filters.
- NotFilter -- implements the logical NOT operation on a filter.
七、信息包插件提供者
Smack提供的体系是堵塞自定义的XML信息包扩展和IQ包分析器的系统(The Smack provider architecture is a system for plugging in custom XML parsing of packet extensions and IQ packets)。标准的Smack扩展(Smack Extensions)是使用提供者的体系结构搭建的。存在以下两种类型的提供者:
- IQProvider –将IQ请求( IQ requests)解析成Java对象(Java objects)
- PacketExtension – 将附属在信息包上的XML子文档解析成信息包扩展实例(PacketExtension instances)
7.1 IQProvider
IQProvider 默认情况下,Smack只知道如何处理类似以下几个名字空间的子信息包的IQ信息包(IQ packets):
jabber:iq:auth
jabber:iq:roster
jabber:iq:register
因为许多IQ类型是XMPP及其扩展部分的一部分,所以提供一个可插入的IQ分析机制。IQ Providers被程序自动的注册或通过创建在你的JAR 文件的META-INF目录下创建一个mack.providers文件。该文件是一个包含一个或多个iqProvider条目(iqProvider entries)的XML文档,如下例所示:
<?xml version="1.0"?>
<smackProviders>
<iqProvider>
<elementName>query</elementName>
<namespace>jabber:iq:time</namespace>
<className>org.jivesoftware.smack.packet.Time</className>
</iqProvider>
</smackProviders>
每一个IQ provider都和一个元素名(element name)和名字空间( namespace)相联系。在上面的例子中,元素名是query,名字空间是abber:iq:time。如果有多重提供者条目(multiple provider entries)尝试注册并控制相同的名字空间,那么从类路径(classpath)载入的第一个条目将有优先权。
IQ provider类可以实现IQProvide接口,或者继承IQ类。在前面的例子中,每一个IQProvider负责解析原始的XML流从而创建一个IQ实例。在下面的例子中,bean introspection将被用于尝试自动使用在IQ packet XML中发现的值设置IQ实例的属性。一个XMPP时间信息包如下所示:
<iq type=’result’ to=’joe@example.com’ from=’mary@example.com’ id=’time_1’>
<query xmlns=’jabber:iq:time’>
<utc>20020910T17:58:35</utc>
<tz>MDT</tz>
<display>Tue Sep 10 12:58:35 2002</display>
</query>
</iq>
为了让这个信息包自动的映射成上面的providers file中所列的时间对象(Time object),它必须有以下几个方法:setUtc(String), setTz(String), 和 setDisplay(String)。自动检查(introspection)的服务将试着自动的将字符串值转化成a boolean, int, long, float, double,或 Class 类型。转化成何种类型由IQ实例的需要来决定。
7.2 PacketExtensionProvider
PacketExtensionProvider 信息包插件提供者(Packet extension providers)为信息包提供一个可插入的系统,这些信息包是一个IQ, message和presence packets的自定义名字空间的子元素。每一个插件提供者(extension provider)使用一个元素名(element name)和名字空间(namespace)在smack.providers文件中注册,如下例所示:
<?xml version="1.0"?>
<smackProviders>
<extensionProvider>
<elementName>x</elementName>
<namespace>jabber:iq:event</namespace>
<className>org.jivesoftware.smack.packet.MessageEvent</className>
</extensionProvider>
</smackProviders>
如果有多重提供者条目(multiple provider entries)尝试注册并控制相同的名字空间,那么从类路径(classpath)载入的第一个条目将有优先权。 一旦在一个信息包中发现信息包插件,解析器将传递到正确的提供者。每一个提供者可以实现PacketExtensionProvider接口或者是一个标准的Java Bean。在前面的例子中,每一个插件提供者(extension provider)负责解析原始的XML流去构造一个实例。在下面的例子中,bean introspection将被用于尝试自动使用在信息包插件子元素(packet extension sub-element)中的值设置类的属性。 当一个插件提供者(extension provider)没有用元素名(element name)和名字空间(namespace)对注册是,Smack将存储所有在缺省信息包插件(DefaultPacketExtension)对象中的最高级别元素(top-level elements),并匹配到信息包上。
七、smack debug模式讲解
smakc 内置两套debuging 控制台,让你追踪XMPP服务端和客户端之间所有XML 流动,在smack-debug.jar包含Lite调试器和增强调试器,在smack-core.jar包含debug输出控制台。
调试模式可以用不同的方式来启用:
1、在创建新连接之前,先添加以下代码行:
SmackConfiguration.DEBUG = true;
1、设置Java系统属性smack.debugenabled真实。可以在命令行上设置系统属性,例如:
java -Dsmack.debugEnabled=true SomeApp
如果你希望在你的应用程序中显式禁用调试模式,可以通过使用命令行参数 或者是在在打开新连接之前,请在应用程序中添加以下行:
SmackConfiguration.DEBUG = false;
smack 使用以下逻辑决定调试控制台使用;
1、它首先会尝试使用在Java系统属性指定类smack.debuggerclass调试器。如果你需要开发属于自己的Debug,需要smackdebugger接口,然后设置系统属性的命令行如:
java -Dsmack.debuggerClass=my.company.com.MyDebugger SomeApp
2、如果第一步失败,Smack会尝试使用增强型Debug,smackx-debug.jar文件中包含了增强型Debug,因此,你需要将jar文件的路径添加到ClassPath路径中。smack-core.jar Lite调试器和增强调试器不可用的情况下,只有控制台调试器。
增强调试器
当smack Debug模式是启动时,Debug窗口会显示每一个创建连接信息,同时也包含以下信息:
- XMPPConnection tabs -- 每个选项卡显示有关连接的调试信息.
- Smack info tab -- shows information about Smack (e.g. Smack version, installed components, etc.). The connection tab will contain the following information:
- All Stanzas --通过smack 分析发送和接收的数据包的信息分析.
- Raw Sent Stanzas -- 通过Smack产生的原始XML流量发送到服务器.
- Raw Received Stanzas -- 服务器向客户端发送的原始XML.
- Ad-hoc message -- 允许发送特定类型的数据包.
- Information -- 显示连接状态和统计数.
Lite Debugger
当smack Debug模式是启动时,Debug窗口会显示每一个创建连接信息,同时也包含以下信息:
- Client Traffic (red text) -- 通过Smack产生的原始XML流量发送到服务器.
- Server Traffic (blue text) --服务器向客户端发送的原始XML.
- Interpreted Stanzas (green text) -- shows XML packets from the server as parsed by Smack. Right click on any of the panes to bring up a menu with the choices to copy of the contents to the system clipboard or to clear the contents of the pane.