OpenFire源码学习之二十七:Smack源码解析

Smack

Smack是一个用于和XMPP服务器通信的类库,由此可以实现即时通讯和聊天。Android中开发通讯APP也可以使用这个包。关于smack的中文开发文档,目前网上也有很多。

下面本,将从源码中分析smack的几个案例。

连接

关于smackConnection是连接XMPP服务器的默认实现。他有两个构造函数,一个是XMPPConecttion(String) 接收服务器地址名的参数。这个是默认的比较常用的构造方法。

l 查找一个DNSSRC,首先需要找到它精确的服务器地址和端口默认是5222

l 它默认引用了TLS加密协议

l 它连接资源名用“Smack”

另一种个构造方法是XMPPServer(ConnectionConfiguration),这里指定了它的配置信息

l 它需要手动设置服务器地址和端口,而不是DNS SRC查找了

l 开启压缩连接

l 自定义的安全设置,比如同样的在连接的时候采用TLS加密的方式

l 每个自定义的连接需要有个独一无二的资源名称。比如:jsmith@example.com。一个完整的地址应该是这样的:jsmith@example.com/Smack

一、测试连接

编写客户端连接代码

public static void main(String[] args) {
        //1打开调试
		XMPPConnection.DEBUG_ENABLED=true;  
        //2申明连接
		XMPPConnection conn = new XMPPConnection("127.0.0.1");  
		try {
            //3建立连接
			conn.connect();
            //登陆
			conn.login("703000", "123"); 
		} catch (XMPPException e) {
			e.printStackTrace();
		}
	}

申明连接:

XMPPConnection conn = new XMPPConnection("127.0.0.1");  

XMPPConnection继承Connection抽象类。

XmppConnection的构造函数中:

 public XMPPConnection(String serviceName) {
        // 创建这个新连接的配置
        super(new ConnectionConfiguration(serviceName));
        config.setCompressionEnabled(false);
        config.setSASLAuthenticationEnabled(true);
        config.setDebuggerEnabled(DEBUG_ENABLED);
    }

1.它会创建当前连接配置也就是ConnectionConfiguration该类实现了Cloneable  

   接口。Cloneable是一个克隆类。所谓克隆就是复制一个一模一样的类。Java中之所  

   以有克隆的存在,原因很简单:

l 效率和简单性,简单的copy一个对象在堆上的的内存比遍历一个对象网然后内存深

copy明显效率高并且简单。

l 不给别的类强加意义。如果A实现了Cloneable,同时有一个引用指向B,如果直接复制内存进行深copy的话,意味着B在意义上也是支持Clone的,但是这个是在使用BA中做的,B甚至都不知道。破坏了B原有的接口。

l 有可能破坏语义。如果A实现了Cloneable,同时有一个引用指向B,该B实现为单例模式,如果直接复制内存进行深copy的话,破坏了B的单例模式。

l 方便且更灵活,如果A引用一个不可变对象,则内存deep copy是一种浪费。Shadow copy给了程序员更好的灵活性。

  ConnectionConfiguration类实现的了Cloneable。我们来看看该类主要是做什么的呢?Smack调用了它的构造函数,根据源码的描述,该构造函数很简单,它是为DNSsrv寻找实际的主机的地址和端口。在这个构造函数有两个步骤:

1)执行DNS,查找需要获取的主机和端口

    hostAddresses = DNSUtil.resolveXMPPDomain(serviceName);

    DNSUtil是一个DNS查找主机的工具类,在这里smack调用了  

    resolveXMPPDomain()方法。

     再来看下这个方法吧:

public static List<HostAddress> resolveXMPPDomain(final String domain) {
        if (dnsResolver == null) {
            List<HostAddress> addresses = new ArrayList<HostAddress>(1);
            addresses.add(new HostAddress(domain, 5222));
            return addresses;
        }
        return resolveDomain(domain, 'c');
    }

这里的HostAddress其实就是一个普通的实例类,里面包含这地址、端口之类的基 

本信息。而这个构造函数也就是根据地址参数给定一个正式域名和端口号。

2)是一个初始化的过程。

       在这个初始化的方法中,主要是构建一个信任的类库,这里呢,默认的则是jrehome

       $JREHOME/lib/security/cacerts.

       当拿到jre home下的lib后,它会创建一个可变的字符序列StringBuilder。 

       依次读取所需的配置环境。接在做的则是:设置根据代理提供的SocketFactory

到了这里,创建连接的配置都做完了。

2.设置压缩流:

   如果连接使用压缩流的话,压缩流在建立TLS成立后开始请求,在流压缩后,网络流量 

   将降低到90%。

3.设置SASL认证

  这里默认的情况是使用SASL认证的。

4.设置连接调试

  这里默认也选择了true.

关于创建连接的初始化配置已经完成了。现在就开始了真正的连接了。下面我们就一起来看看它究竟是如何来创建的连接的。

conn.connect();

XMPPConnectionconnect方法它主要是创建并维护一个套子连接到XMPP服务器

public void connect() throws XMPPException {
        // Establishes the connection, readers and writers
        connectUsingConfiguration(config);
        // Automatically makes the login if the user was previouslly connected successfully
        // to the server and the connection was terminated abruptly
        if (connected && wasAuthenticated) {
            // Make the login
            try {
                if (isAnonymous()) {
                    // Make the anonymous login
                    loginAnonymously();
                }
                else {
                    login(config.getUsername(), config.getPassword(),
                            config.getResource());
                }
                notifyReconnection();
            }
            catch (XMPPException e) {
                e.printStackTrace();
            }
        }
    }

在这里的第一步:先建立读、写连接,用connectUsingConfiguration方法来创建

在该方法中创建socket套子连接。参数SocketFactoy抽象套接字工厂是一种捕获与正被创建的套接字相关的各种策略的简单方式,以不需要对请求套接字的代码进行特殊配置的方式生成这种套接字。 它有个方法createSocket(InetAddress host, int port) :

this.socket = config.getSocketFactory().createSocket(host, port);

然后smack调用了initConnection方法,这是个初始化连接创建XMPP服务的读写数据流。在这个方法中有以下几个步骤:

1)设置读写的实例变量 initReaderAndWriter()

 private void initReaderAndWriter() throws XMPPException {
        try {
            if (compressionHandler == null) {
                reader =
                        new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
                writer = new BufferedWriter(
                        new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
            }
            else {
                try {
                    OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
                    writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));

                    InputStream is = compressionHandler.getInputStream(socket.getInputStream());
                    reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                }
.......
        // If debugging is enabled, we open a window and write out all network traffic.
        initDebugger();
    }

下面有个方法initDebugger(),检查debug模式是否打开

2)添加消息包监听。

我本地的测试环境启用了debug模式。addPacketListener() 

3)开始写数据包,它将打开一个XMPP流服务

       这里用到了一个线程来打开这个流服务

writerThread = new Thread() {
            public void run() {
                writePackets(this);
            }
        };<span style="color:#000000;background:rgb(255,255,255);">
 4<span style="font-family:宋体;">)开始读数据包,</span></span><span style="color:#000000;background:rgb(255,255,255);">它是一个</span>阻塞方法,直到我们得到一个开放流数据包从服务器返回。
synchronized public void startup() throws XMPPException {
        readerThread.start();
        try {
            int waitTime = SmackConfiguration.getPacketReplyTimeout();
            wait(3 * waitTime);
        }
        catch (InterruptedException ie) {
            // Ignore.
        }
        if (connectionID == null) {
            throw new XMPPException("Connection failed. No response from server.");
        }
        else {
            connection.connectionID = connectionID;
        }
    }

在这个方法里面首先会启动数据包的读方法。然后根据设置的超时时间,smack

设置的默认超时时间为5000毫秒

在这里已经和openfire建立连接了。在openfire的开发环境里面,已经监听到了

Smack的请求消息了。这里我们可以看到smack发送了一条流消息

<stream:stream to="127.0.0.1" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">

Smack想服务端发动tls

<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>

当服务端接收到smacktls协商后会调用方法tlsNegotiated()

此时服务端也返回一个流标记作为响应给客户端

<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="xjgho-2017758" id="74f88b46" xml:lang="en" version="1.0">
<stream:features>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>DIGEST-MD5</mechanism><mechanism>JIVE-SHAREDSECRET</mechanism><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>CRAM-MD5</mechanism></mechanisms>
<compression xmlns="http://jabber.org/features/compress"><method>zlib</method>
</compression><auth xmlns="http://jabber.org/features/iq-auth"/>
<register xmlns="http://jabber.org/features/iq-register"/></stream:features>

登陆

Samck继承了比较全面的登陆服务器模式,如果服务器支持SASL验证。那么smack也能过对照SASL验证的方法,如果验证的时间超过5(smack默认设置为5)。当前在登陆之前,必须先连接到服务器端。

登陆的如下步骤:

1.使用SASL验证 

response = saslAuthentication.authenticate(username, password, resource);

Authenticate()方法是执行对制定的用户进行SASL验证。如果验证成功了,则执

资源绑定和会话连接。这个方法将返回完整的JID服务器提供的资源绑定连接。

在这里smack采用默认的SASL验证机制DIGEST-MD5。

接下来smack将从连接中找到需要登陆主机的主机名字。将它交给方法 authenticate()。我们重点来看看这个方法吧。

currentMechanism.authenticate(username, connection.getHost(), serviceName, password);

这个方法构建和发送身份认证节点到服务器。但官方是说这个方法并不是一个可取的方

法。因为它非常的inflexable(?),它是不可转变的。

关于anth节点:

客户端的身份验证节点需要包含XMPP/serverName的digest-uri

public void authenticate(String username, String host, String serviceName, String password) throws IOException, XMPPException {
        this.authenticationId = username;
        this.password = password;
        this.hostname = host;        

        String[] mechanisms = { getName() };
        Map<String,String> props = new HashMap<String,String>();        
        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", serviceName, props, this);
        authenticate();
    }

createSaslClient()方法创建一个SaslClient使用参数提供。这种方法使用JCA安全提供者框架,描述了在Java密码架构API规范和参考,定位和选择一个SaslClient实现。首先,它获得的有序列表SaslClientFactory实例注册安全提供者的SaslClientFactory服务和指定的SASL机制(年代) 然后它调用createSaslClient()在每个工厂实例列表上的一个非空SaslClient直到一生产实例。它将返回null SaslClient实例,null如果搜索失败产生一个非空SaslClient实例。

一个安全提供者为SaslClientFactory寄存器与JCA安全提供者框架形式的键

SaslClientFactory.mechanism_name

和值的实现类名称的javax.security.sasl.SaslClientFactory 例如,一个提供者,它包含一个工厂类,com奇才sasl消化。ClientFactory,支持消化md5机制将登记下列条目与JCA:SaslClientFactory 消化md5 com.wiz.sasl.digest.ClientFactory

看到Java密码架构API规范和参考了解如何安装和配置安全服务提供商。

Authenticate()方法开始发送身份验证到服务器。在这个发送验证的过程过samck

会等待30秒,等到SASL谈判的结束。如果SASL验证失败所以试试非SASL验证

如果验证成功会调用

SASLAuthentication.bindResourceAndEstablishSession()方法

它会创建一个新包收集器为这个连接。一个数据包过滤器决定了哪些数据包将被累积的收集器。一个PacketCollector更适合使用比PacketListener当你需要等待一个特定的结果。

然后调用连接发送数据包

connection.sendPacket(bindResource);

获取好友列表

获取好友列表代码:

Collection<RosterEntry> rosters = conn.getRoster().getEntries();  
			for (RosterEntry rosterEntry : rosters){  
				 System.out.print("name: " +rosterEntry.getName()+ ",jid: " +rosterEntry.getUser()); 
			}

smack登陆的时候XMPPConnectionlogin方法

response = saslAuthentication.authenticate(username, password, resource);中会发送下面验证信息:

<auth mechanism="DIGEST-MD5" xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></auth>

OpenFire服务端接收到消息的处理:

 else if ("auth".equals(tag)) {
            // User is trying to authenticate using SASL 用户试图使用SASL验证
            startedSASL = true;
            // Process authentication stanza 验证处理
            saslStatus = SASLAuthentication.handle(session, doc);
        }

handl会调用方法发送信息

private static void sendChallenge(Session session, byte[] challenge) {
        StringBuilder reply = new StringBuilder(250);
        if (challenge == null) {
            challenge = new byte[0];
        }
        String challenge_b64 = StringUtils.encodeBase64(challenge).trim();
        if ("".equals(challenge_b64)) {
            challenge_b64 = "="; // Must be padded if null
        }
        reply.append(
                "<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">");
        reply.append(challenge_b64);
        reply.append("</challenge>");
        session.deliverRawText(reply.toString());
    }

这里拼写了字符串内容

<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09InhqZ2hvLTIwMTc3NTgiLG5vbmNlPSJ6OGpkaUI0K3ZaRWwxZEwweXUyL1pHYVNZSFpBaVFKNHFENkphQlh5Iixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</challenge>

接下来Smack将发送账号验证信息

<iq id="jvpcP-1" type="get">
  <query xmlns="jabber:iq:auth">
    <username>703000</username>
  </query>
</iq>

IQ packet: 

<iq id="bGJkL-4" type="set" from="xjgho-2017758/36650a42">
  <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
    <resource>Smack</resource>
  </bind>
</iq>

登陆2

1、在XMPPConnection类调用方法login(),在login()中调用:

response = saslAuthentication.authenticate(username, password, resource);

该方法为消息认证smack会发出如下认证:

<auth mechanism="DIGEST-MD5" 

xmlns="urn:ietf:params:xml:ns:xmpp-sasl">

</auth>

Openfire在处理的认证的时候通过类SASLAuthentication中的handle()方法

来处理。调用send方法标示怀疑,通过生成密钥告诉smack

<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09InhqZ2hvLTIwMTc3NTgiLG5vbmNlPSJDTWdTOHBqWW5pU05BZ3FZVXRPc1h3ejI4cFZyZjlkcjlEZ1poczVJIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</challenge>

Openfire发出疑问后,smcak接到消息马上作出相应:

<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">Y2hhcnNldD11dGYtOCx1c2VybmFtZT0iNzAzMDAwIixyZWFsbT0ieGpnaG8tMjAxNzc1OCIsbm9uY2U9IkNNZ1M4cGpZbmlTTkFncVlVdE9zWHd6MjhwVnJmOWRyOURnWmhzNUkiLG5jPTAwMDAwMDAxLGNub25jZT0iaUYzbjdGWmQ3aHlwbUpQZ0R1c3VJUnUrL1dBSzRHMWFXOGpOSU95OCIsZGlnZXN0LXVyaT0ieG1wcC94amdoby0yMDE3NzU4IixtYXhidWY9NjU1MzYscmVzcG9uc2U9ZjE5MmNmOTc5OWM0NTMzZTM2M2VjZGMyMTRjMDMyOGIscW9wPWF1dGgsYXV0aHppZD0iNzAzMDAwIg==</response>

Of接收到应答后处理:

else if (startedSASL && "response".equals(tag)) {
            // User is responding to SASL challenge. Process response 用户答应挑战SASL。过程响应
            saslStatus = SASLAuthentication.handle(session, doc);
        }

通过密钥传输校验smack应答的消息。

而在这时候,smack在会等待30秒的时间。

openfire校验完之后smack再次发出:

<stream:stream to="xjgho-2017758" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">

OF处理

<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="xjgho-2017758" id="97cf51b6" xml:lang="en" version="1.0"><stream:features><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>

Smack再次调用方法:

bindResourceAndEstablishSession(resource)

发送数据包

<iq type="result" id="1wNL6-4" to="xjgho-2017758/99763d49"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>703000@xjgho-2017758/Smack</jid></bind></iq>
<iq id="1wNL6-5" type="set">
  <session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
</iq>

OF应答:

<iq type="result" id="1wNL6-5" to="703000@xjgho-2017758/Smack"/>


Smack中请求好友:XMMPConnection

if (config.isRosterLoadedAtLogin()) {
            this.roster.reload();
        }

发送请求消息:

<iq id="1wNL6-6" type="get" from="703000@xjgho-2017758/Smack">
  <query xmlns="jabber:iq:roster"/>
</iq>

OF返回好友:

 <iq type="result" id="1wNL6-6" to="703000@xjgho-2017758/Smack"><query xmlns="jabber:iq:roster"><item jid="702976@xjgho-2017758" name="欧阳湘杰" subscription="both"><group>stu1017129</group></item><item jid="703004@xjgho-2017758" name="尹昌" subscription="both"><group>stu1017129</group></item><item jid="703002@xjgho-2017758" name="易丽花" subscription="both"><group>stu1017129</group></item></query></iq>


Smack发送在线状态:

if (config.isSendPresence()) {
            packetWriter.sendPacket(new Presence(Presence.Type.available));
        }

消息:

<presence id="1wNL6-7"></presence>

单点消息发送

Smack发送消息代码:

Chat chat = conn.getChatManager().createChat("703000@XJGHO-2017758", new MessageListener() {
				@Override
			    public void processMessage(Chat chat, Message message) {
			        System.out.println("Received message: " + message);
			    }
			});
chat.sendMessage("Howdy!");

代码分析:

先解释下需要用到的类:

Chat:两个用户之间的聊天消息类

每个聊天有一个独特的线程ID,用于跟踪哪些消息属于一个特定的对话。一些消息被发送没有线程ID,和一些客户不要发送线程ID在所有。因此,如果一个消息没有线程ID到它被路由到最近创建的聊天消息发送者。

解释:

1、首先会调用createChat()方法,该方法需要两个参数一个jid字符串,一个是消息

 监听器。这个方法创建一个聊天通道

public Chat createChat(String userJID, MessageListener listener) {
        String threadID;
        do  {
            threadID = nextID();
        } while (threadChats.get(threadID) != null);

        return createChat(userJID, threadID, listener);
    }

2、发送消息。向OF发送消息

chat.sendMessage("Howdy!");

<message id="lSVLq-8" to="703000@xjgho-2017758" from="703693@xjgho-2017758/Smack" type="chat">
  <body>Howdy!</body>
  <thread>KQTOg0</thread>
</message>

这个消息里边有thread节点

返回当前线程的id的消息,这是一个独特的标识符序列的“聊天”的消息。如果没有线程id被设置,那么该方法将会返回null。

监听好友的出席状态

关于好友出席状态在Roster

private final List<RosterListenerrosterListeners;

这个内存集合记录了好友的出席状态

使用方法:

Roster roster = con.getRoster();
roster.addRosterListener(new RosterListener() {
    // Ignored events public void entriesAdded(Collection<String> addresses) {}
    public void entriesDeleted(Collection<String> addresses) {}
    public void entriesUpdated(Collection<String> addresses) {}
    public void presenceChanged(Presence presence) {
        System.out.println("Presence changed: " + presence.getFrom() + " " + presence);
    }
});

调试

对于openfire的一个客户端,smack也是一个非常的调试工具。

打开调试工具只需要设置调试模式为true

XMPPConnection.DEBUG_ENABLED=true



原文地址:https://www.cnblogs.com/huwf/p/4273341.html