概述
Smack是一个开源的实现了XMPP协议的库,特别是4.1.0版本以后,直接支持Android系统,无需再使用以前那个专门针对Android系统的aSmack移植库了.虽然在移动端上,用XMPP协议来做IM并不是一个最优选择,市面上这些大公司基本都是用自己定制的私有协议,没有采用XMPP协议的,不过我们可以抛开协议层面,只分析一下Smack库在网络层的实现,也是有借鉴意义的。
总体结构
Smack抽象出一个XMPPConnection的概念,要想收发消息,首先得建立这个connection,而且这种connection是可以由多个实例的。XMPPConnection只是一个接口,AbstractXMPPConnection实现了这个接口并加入了login,connect,processStanza等方法。AbstractXMPPConnection有两个实现类,XMPPBOSHConnection和XMPPTCPConnection。其中XMPPBOSHConnection是基于Http协议来实现的,而XMPPTCPConnection是直接用Socket来实现的长连接通信,本文分析的也就是XMPPTCPConnection。一个简单的使用实例如下:
XMPPTCPConnection con = new XMPPTCPConnection("igniterealtime.org");
// Connect to the server
con.connect();
// Most servers require you to login before performing other tasks.
con.login("jsmith", "mypass");
// Start a new conversation with John Doe and send him a message.
Chat chat = ChatManager.getInstanceFor(con).createChat("jdoe@igniterealtime.org", new MessageListener() {
public void processMessage(Chat chat, Message message) {
// Print out any messages we get back to standard out.
System.out.println("Received message: " + message);
}
});
chat.sendMessage("Howdy!");
// Disconnect from the server
con.disconnect();
接口介绍
XMPPConnection这个接口里有几个主要的方法 :
public void sendStanza(Stanza stanza) throws NotConnectedException, InterruptedException;
public void addConnectionListener(ConnectionListener connectionListener);
public void addPacketInterceptor(StanzaListener packetInterceptor, StanzaFilter packetFilter);
public void addPacketSendingListener(StanzaListener packetListener, StanzaFilter packetFilter);
public PacketCollector createPacketCollector(StanzaFilter packetFilter);
public void addAsyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter);
public void addSyncStanzaListener(StanzaListener packetListener, StanzaFilter packetFilter);
sendStanza 发送包到服务器。在最新版的Smack中,Stanza就是以前版本中的Packet
addConnectionListener 添加ConnectionListener到XMPPConnection中。在该Listener中,监听者可以得到连接是否成功建立,连接关闭,连接异常关闭,重连是否成功等事件
addPacketInterceptor 向Connection中注册拦截器StanzaListener,所有发往服务器的包都会先过一遍拦截器,你可以在拦截器中对这些包进行处理;StanzaFilter过滤器可以允许你定制哪些包才需要拦截; StanzaListener和StanzaFilter常常配对使用,代码中有各种wrapper类(如ListenerWrapper、InterceptorWrapper等),就是把这两个接口组合在一个类中,一个负责过滤包,一个负责实际处理包
addPacketSendingListener 注册一个Listener,当把包通过Socket写出去后,会回调这个Listener告知正在发送状态
createPacketCollector 当你想接收某种类型的包时,可以新建一个包收集器。和StanzaListener不同,包收集器是阻塞式的,直到指定的包收到或者出现超时(我们可以设置等待一个包的最大时间)等异常
PacketCollector messageCollector = connection.createPacketCollector(messageFilter);
try {
connection.createPacketCollectorAndSend(request).nextResultOrThrow();
// Collect the received offline messages
Message message = messageCollector.nextResult();
while (message != null) {
messages.add(message);
message = messageCollector.nextResult();
}
}
finally {
// Stop queuing offline messages
messageCollector.cancel();
}
return messages;
- addAsyncStanzaListener和addSyncStanzaListener 添加处理收到的包的回调接口;其中一个叫同步一个叫异步区别在于,执行回调方法所用的线程池不一样,其中异步用的是Executors.newCachedThreadPool,而同步用的是一个Executors.newSingleThreadExecutor,可以保证执行顺序
// First handle the async recv listeners. Note that this code is very similar to what follows a few lines below,
// the only difference is that asyncRecvListeners is used here and that the packet listeners are started in
// their own thread.
final Collection<StanzaListener> listenersToNotify = new LinkedList<StanzaListener>();
synchronized (asyncRecvListeners) {
for (ListenerWrapper listenerWrapper : asyncRecvListeners.values()) {
if (listenerWrapper.filterMatches(packet)) {
listenersToNotify.add(listenerWrapper.getListener());
}
}
}
for (final StanzaListener listener : listenersToNotify) {
asyncGo(new Runnable() {
@Override
public void run() {
try {
listener.processPacket(packet);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Exception in async packet listener", e);
}
}
});
}
// Loop through all collectors and notify the appropriate ones.
for (PacketCollector collector: collectors) {
collector.processPacket(packet);
}
// Notify the receive listeners interested in the packet
listenersToNotify.clear();
synchronized (syncRecvListeners) {
for (ListenerWrapper listenerWrapper : syncRecvListeners.values()) {
if (listenerWrapper.filterMatches(packet)) {
listenersToNotify.add(listenerWrapper.getListener());
}
}
}
// Decouple incoming stanza processing from listener invocation. Unlike async listeners, this uses a single
// threaded executor service and therefore keeps the order.
singleThreadedExecutorService.execute(new Runnable() {
@Override
public void run() {
for (StanzaListener listener : listenersToNotify) {
try {
listener.processPacket(packet);
} catch(NotConnectedException e) {
LOGGER.log(Level.WARNING, "Got not connected exception, aborting", e);
break;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Exception in packet listener", e);
}
}
}
});
AbstractXMPPConnection实现了XMPPConnection接口,各种Listener的注册和回调就是在这个类里完成的,但如login,connect,shutdown等方法的具体实现是位于其子类中的。
连接过程
真正执行连接动作的是XMPPTCPConnection中connectInternal的方法
protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException {
closingStreamReceived.init();
// Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if
// there is an error establishing the connection
connectUsingConfiguration();
// We connected successfully to the servers TCP port
initConnection();
// Wait with SASL auth until the SASL mechanisms have been received
saslFeatureReceived.checkIfSuccessOrWaitOrThrow();
// Make note of the fact that we're now connected.
connected = true;
callConnectionConnectedListener();
}
connectUsingConfiguration方法中,用配置类XMPPTCPConnectionConfiguration提供的hostAddress,timeout等数据创建一个Socket连接出来。随后进行了一些初始化,例如初始化reader,writer变量:
private void initReaderAndWriter() throws IOException {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
if (compressionHandler != null) {
is = compressionHandler.getInputStream(is);
os = compressionHandler.getOutputStream(os);
}
// OutputStreamWriter is already buffered, no need to wrap it into a BufferedWriter
writer = new OutputStreamWriter(os, "UTF-8");
reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
// If debugging is enabled, we open a window and write out all network traffic.
initDebugger();
}
PacketWriter对包的发送进行了封装,该类里维护一个BlockingQueue,所有要发送的包都先插入到这个队列中,同时起一个线程不停消费这个队列,最终是通过writer把数据写往服务器
while (!queue.isEmpty()) {
Element packet = queue.remove();
writer.write(packet.toXML().toString());
}
writer.flush();
而PacketReader则是对包的读取和解析进行了封装,类里面有个XmlPullParser,通过reader进行了初始化
packetReader.parser = PacketParserUtils.newXmppParser(reader);
然后起了一个线程不停进行包的解析
Async.go(new Runnable() {
public void run() {
parsePackets();
}
}, "Smack Packet Reader (" + getConnectionCounter() + ")");
}
解析出来的包回调到AbstractXMPPConnection类中的parseAndProcessStanza方法,最终调用各种已注册好的StanzaListener、PacketCollector来处理
XMPPConnectionRegistry
这个静态类中有个ConnectionCreationListener的集合
private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
new CopyOnWriteArraySet<ConnectionCreationListener>();
当XMPPConnection初始化的时候,会通知给各个Listener
protected AbstractXMPPConnection(ConnectionConfiguration configuration) {
saslAuthentication = new SASLAuthentication(this, configuration);
config = configuration;
// Notify listeners that a new connection has been established
for (ConnectionCreationListener listener : XMPPConnectionRegistry.getConnectionCreationListeners()) {
listener.connectionCreated(this);
}
}
像ReconnectionManager,PingManager等策略管理类,会在静态代码块中直接注册ConnectionCreationListener
static {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
public void connectionCreated(XMPPConnection connection) {
if (connection instanceof AbstractXMPPConnection) {
ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
}
}
});
}
ReconnectionManager
由于可以创建多个XMPPConnection的实例,ReconnectionManager的实例也有多个,和XMPPConnection一一对应,实际上ReconnectionManager持有了XMPPConnection的弱引用,用于进行与Connection相关的操作。
类里面还定义了不同的重连策略ReconnectionPolicy,有按固定频率重连的,也有按随机间隔重连的,
private int timeDelay() {
attempts++;
// Delay variable to be assigned
int delay;
switch (reconnectionPolicy) {
case FIXED_DELAY:
delay = fixedDelay;
break;
case RANDOM_INCREASING_DELAY:
if (attempts > 13) {
delay = randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
}
else if (attempts > 7) {
delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
}
else {
delay = randomBase; // 10 seconds
}
break;
default:
throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
}
return delay;
}
ReconnectionManager向XMPPConnection注册了ConnectionListener,当XMPPConnection中发生连接异常时,如PacketWriter、PacketReader读写包异常时,会通过ConnectionListener中的connectionClosedOnError方法,通知ReconnectionManager进行重连重试。
PingManager、ServerPingWithAlarmManager
PingManager实现了协议规定的定时发送Ping消息到服务器的策略,默认是30分钟的间隔。ServerPingWithAlarmManager是针对Android平台的实现,用AlarmManager来实现的定时策略,在代码里写死是30分钟的频率,这在移动端肯定是不适用的,另外也没看到针对各种网络环境的处理,看来为保证长连接的稳定性,需要开发者自己再去实现一些心跳和重连策略