XMPP协议_smack源码分析(一)

其实基于XMPP协议的smack开源框架使用起来非常方便,只需要下面三个步奏即可使用成功

1.创建连接配置

//两个参数为服务器主机名和端口号
ConnectionConfiguration config = new ConnectionConfiguration(domain,port);

2.创建用于连接服务器的核心类XMPPConnection

//将连接配置对象当作参数传入给XMPPConnection构造函数
XMPPConnection connection = new XMPPConnection(config);

3.用XMPPConnection实例进行连接

//进行连接
connection.connect();

下面就是每一步的详解了:
第一步: ConnectionConfiguration的初始化
其实在这三个步奏中smack源码中进行啦很多我们不知道的操作,现在就来看看源码吧!
首先第一步看ConnectionConfiguration类的构造函数:

public ConnectionConfiguration(String host, int port) {
        initHostAddresses(host, port);
        init(host, ProxyInfo.forDefaultProxy());
    }

其实根据方法的命名就知道是进行初始化主机地址和初始化代理方式相关的一些信息,因为在第二部中会将ConnectionConfiguration实例作为参数传递给XMPPConnection使用
现在就来看看initHostAddress(host,port)方法吧:

private void initHostAddresses(String host, int port) {
        hostAddresses = new ArrayList<HostAddress>(1);
        HostAddress hostAddress;
        try {
             hostAddress = new HostAddress(host, port);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
        hostAddresses.add(hostAddress);
    }

一看便知就是将主机地址和端口号封装成HostAddress对象保存在成员变量hostAddress集合中.
再来看看init(host,ProxyInfo.forDefaultProxy方法:

protected void init(String serviceName, ProxyInfo proxy) {
        this.serviceName = serviceName;
        this.proxy = proxy;
        // Build the default path to the cacert truststore file. By default we are
        // going to use the file located in $JREHOME/lib/security/cacerts.
        String javaHome = System.getProperty("java.home");
        StringBuilder buffer = new StringBuilder();
        buffer.append(javaHome).append(File.separator).append("lib");
        buffer.append(File.separator).append("security");
        buffer.append(File.separator).append("cacerts");
        truststorePath = buffer.toString();
        // Set the default store type
        truststoreType = "jks";
        // Set the default password of the cacert file that is "changeit"
        truststorePassword = "changeit";
        keystorePath = System.getProperty("javax.net.ssl.keyStore");
        keystoreType = "jks";
        pkcs11Library = "pkcs11.config";

        //Setting the SocketFactory according to proxy supplied
        socketFactory = proxy.getSocketFactory();
    }

代码行数看不到,现在还不知道怎么解决,后期会处理,请见谅
从第一行到20行,请见谅,都是在封装一些杂七杂八的数据,太多啦不解释,也很烦,主要是看第22行代码,通过一个ProxyInfo 实例获取一个创建socket的工厂类实例,该实例会在XMPPConnection中用到,后面会解释.
关于这个ProxyInfo 不是很复杂,其代码也不多,这里我就只贴最核心的方法

public SocketFactory getSocketFactory()
    {
        if(proxyType == ProxyType.NONE)
        {
            return new DirectSocketFactory();
        }
        else if(proxyType == ProxyType.HTTP)
        {
            return new HTTPProxySocketFactory(this);
        }
        else if(proxyType == ProxyType.SOCKS4)
        {
            return new Socks4ProxySocketFactory(this);
        }
        else if(proxyType == ProxyType.SOCKS5)
        {
            return new Socks5ProxySocketFactory(this);
        }
        else
        {
            return null;
        }
    }

其实就是根据不同的代理类型创建不同的socket工厂给调用者,这些socket工厂类都是创建socket实例的,只不过细节上有些差异

好,到这里关于ConnectionConfiguration 就解释完啦,其主要目的就是封装一些连接的信息好提供给XMPPConnection进行使用

第二步: XMPPConnection的初始化

这个类的实例化就在简单不过了,代码如下:

public XMPPConnection(ConnectionConfiguration config) {
        super(config);
    }

就是将ConnectionConfiguration 用super方法赋值给成员变量,这样XMPPConnection就可以使用一些连接的信息啦.

第三步:这是最关键的一步,也是最复杂的一步

让我们来看XMPPConnection的connect()方法是怎么写的,源码如下:

/**
     * Establishes a connection to the XMPP server and performs an automatic login
     * only if the previous connection state was logged (authenticated). It basically
     * creates and maintains a socket connection to the server.<p>
     * <p/>
     * Listeners will be preserved from a previous connection if the reconnection
     * occurs after an abrupt termination.
     *
     * @throws XMPPException if an error occurs while trying to establish the connection.
     *      Two possible errors can occur which will be wrapped by an XMPPException --
     *      UnknownHostException (XMPP error code 504), and IOException (XMPP error code
     *      502). The error codes and wrapped exceptions can be used to present more
     *      appropriate error messages to end-users.
     */
    public void connect() throws XMPPException {
        // Establishes the connection, readers and writers
        connectUsingConfiguration(config);
        // Automatically makes the login if the user was previously connected successfully
        // to the server and the connection was terminated abruptly
        if (connected && wasAuthenticated) {
            // Make the login
            if (isAnonymous()) {
                // Make the anonymous login
                loginAnonymously();
            }
            else {
                login(config.getUsername(), config.getPassword(), config.getResource());
            }
            notifyReconnection();
        }
    }

从英文翻译来看,意思是 连接服务器,只有在授权的情况下会去自动登录,否则只会连接服务器,然后会调用login()或者loginAnonymously()方法进行授权验证,授权的时候其实是调用的PacketWriter发送数据的,当然后面会解释怎样发送数据
请看第17行代码,用连接配置对象进行连接,我们进入到这个方法查看源码:

 private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
        XMPPException exception = null;
        Iterator<HostAddress> it = config.getHostAddresses().iterator();
        List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
        boolean xmppIOError = false;
        while (it.hasNext()) {
            exception = null;
            HostAddress hostAddress = it.next();
            String host = hostAddress.getFQDN();
            int port = hostAddress.getPort();
            try {
                if (config.getSocketFactory() == null) {
                    this.socket = new Socket(host, port);
                }
                else {
                    this.socket = config.getSocketFactory().createSocket(host, port);
                }
            } catch (UnknownHostException uhe) {
                String errorMessage = "Could not connect to " + host + ":" + port + ".";
                exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout,
                        errorMessage), uhe);
            } catch (IOException ioe) {
                String errorMessage = "XMPPError connecting to " + host + ":" + port + ".";
                exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error,
                        errorMessage), ioe);
                xmppIOError = true;
            }
            if (exception == null) {
                // We found a host to connect to, break here
                config.setUsedHostAddress(hostAddress);
                break;
            }
            hostAddress.setException(exception);
            failedAddresses.add(hostAddress);
            if (!it.hasNext()) {
                // There are no more host addresses to try
                // throw an exception and report all tried
                // HostAddresses in the exception
                StringBuilder sb = new StringBuilder();
                for (HostAddress fha : failedAddresses) {
                    sb.append(fha.getErrorMessage());
                    sb.append("; ");
                }
                XMPPError xmppError;
                if (xmppIOError) {
                    xmppError = new XMPPError(XMPPError.Condition.remote_server_error);
                }
                else {
                    xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout);
                }
                throw new XMPPException(sb.toString(), xmppError, exception);
            }
        }
        socketClosed = false;
        initConnection();
    }

请看13到16行,不就是创建socket吗,创建原则就是根据 刚才初始化时传递的config对象中是否有SocketFactory来创建

然后再看55行代码,初始化连接,查看该方法源码如下:

/**
     * Initializes the connection by creating a packet reader and writer and opening a
     * XMPP stream to the server.
     *
     * @throws XMPPException if establishing a connection to the server fails.
     */
    private void initConnection() throws XMPPException {
        boolean isFirstInitialization = packetReader == null || packetWriter == null;
        compressionHandler = null;
        serverAckdCompression = false;
        // Set the reader and writer instance variables
        initReaderAndWriter();
        try {
            if (isFirstInitialization) {
                packetWriter = new PacketWriter(this);
                packetReader = new PacketReader(this);
                // If debugging is enabled, we should start the thread that will listen for
                // all packets and then log them.
                if (config.isDebuggerEnabled()) {
                    addPacketListener(debugger.getReaderListener(), null);
                    if (debugger.getWriterListener() != null) {
                        addPacketSendingListener(debugger.getWriterListener(), null);
                    }
                }
            }
            else {
                packetWriter.init();
                packetReader.init();
            }
            // Start the packet writer. This will open a XMPP stream to the server
            packetWriter.startup();
            // Start the packet reader. The startup() method will block until we
            // get an opening stream packet back from server.
            packetReader.startup();
            // Make note of the fact that we're now connected.
            connected = true;
            if (isFirstInitialization) {
                // Notify listeners that a new connection has been established
                for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
                    listener.connectionCreated(this);
                }
            }
        }

该方法的作用就是初始化连接,初始化输入输出流,初始化PacketWriter对象和PacketReader对象,这两个对象会创建和开启子线程写入和读取数据
请看13行代码,进入该方法的源码:

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"));
                }
                catch (Exception e) {
                    e.printStackTrace();
                    compressionHandler = null;
                    reader = new BufferedReader(
                            new InputStreamReader(socket.getInputStream(), "UTF-8"));
                    writer = new BufferedWriter(
                            new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
                }
            }
        }
        catch (IOException ioe) {
            throw new XMPPException(
                    "XMPPError establishing connection with server.",
                    new XMPPError(XMPPError.Condition.remote_server_error,
                            "XMPPError establishing connection with server."),
                    ioe);
        }
        // If debugging is enabled, we open a window and write out all network traffic.
        initDebugger();
    }

从第4到7行, 11到15行, 20到23行目的都一样,创建进行读与写的流对象writer和reader,这两个对象就是用于读写数据的,让我们再回到initConnection()方法中,从17到40行
这几步操作是创建PacketWriter和PacketReader,并进行初始化和两个对象中的线程跑起来.
好,那么就来分析分析PacketWriter和PacketReader这两个类吧!
先来PacketWriter,初始化方法如下:

/**
     * Creates a new packet writer with the specified connection.
     *
     * @param connection the connection.
     */
    protected PacketWriter(XMPPConnection connection) {
        this.queue = new ArrayBlockingQueue<Packet>(500, true);
        this.connection = connection;
        init();
    }

构造方法是保存XMPPConnection连接对象和创建数组队列,看第9行代码init()的内部源码如下:

/** 
    * Initializes the writer in order to be used. It is called at the first connection and also 
    * is invoked if the connection is disconnected by an error.
    */ 
    protected void init() {
        this.writer = connection.writer;
        done = false;
        writerThread = new Thread() {
            public void run() {
                writePackets(this);
            }
        };
        writerThread.setName("Smack Packet Writer (" + connection.connectionCounterValue + ")");
        writerThread.setDaemon(true);
    }

第6行获取XMPPConnection中已经实例化的writer流对象,然后第9行创建写的线程,线程执行的操作是执行writePackets方法,该方法源码如下:

private void writePackets(Thread thisThread) {
        try {
            // Open the stream.
            openStream();
            // Write out packets from the queue.
            while (!done && (writerThread == thisThread)) {
                Packet packet = nextPacket();
                if (packet != null) {
                    writer.write(packet.toXML());
                    if (queue.isEmpty()) {
                        writer.flush();
                    }
                }
            }
            // Flush out the rest of the queue. If the queue is extremely large, it's possible
            // we won't have time to entirely flush it before the socket is forced closed
            // by the shutdown process.
            try {
                while (!queue.isEmpty()) {
                    Packet packet = queue.remove();
                    writer.write(packet.toXML());
                }
                writer.flush();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
            // Delete the queue contents (hopefully nothing is left).
            queue.clear();
            // Close the stream.
            try {
                writer.write("</stream:stream>");
                writer.flush();
            }
            catch (Exception e) {
                // Do nothing
            }
            finally {
                try {
                    writer.close();
                }
                catch (Exception e) {
                    // Do nothing
                }
            }
        }
        catch (IOException ioe) {
            // The exception can be ignored if the the connection is 'done'
            // or if the it was caused because the socket got closed
            if (!(done || connection.isSocketClosed())) {
                done = true;
                // packetReader could be set to null by an concurrent disconnect() call.
                // Therefore Prevent NPE exceptions by checking packetReader.
                if (connection.packetReader != null) {
                        connection.notifyConnectionError(ioe);
                }
            }
        }
    }

第4行,打开流,源码如下:

void openStream() throws IOException {
        StringBuilder stream = new StringBuilder();
        stream.append("<stream:stream");
        stream.append(" to=\"").append(connection.getServiceName()).append("\"");
        stream.append(" xmlns=\"jabber:client\"");
        stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
        stream.append(" version=\"1.0\">");
        writer.write(stream.toString());
        writer.flush();
    }

由于是基于XMPP协议,传递的是xml流信息,所以传递的格式也是这样,同时在第4行代码中将服务器的名字写在啦writer流中,在回到上一步writePackets()方法中:
是 两个while无限循环都是从数组队列中获取Packet实例,该包中就是封装啦xml数据的类,然后将xml信息写入到writer流中,
这就是PacketWriter的使用.
关于PacketReader类的源码就不用贴啦,基本上PacketWriter一样.

好,现在让我们回到XMPPConnection中的connect方法中,上面这一大啪啦代码都是connect()方法中connectConfiguration()操作调用的,现在继续往下,我在贴一下XMPPConnection类中的connect方法的源码:因为不多

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

接下来就是login和loginAnonymously方法了,这两个方法是将用户名和密码发送给服务器进行授权认证的:我贴一个就行了,原理相同,就是用的类不同而已,下面是login()方法的源码:

public synchronized void login(String username, String password, String resource) throws XMPPException {
        if (!isConnected()) {
            throw new IllegalStateException("Not connected to server.");
        }
        if (authenticated) {
            throw new IllegalStateException("Already logged in to server.");
        }
        // Do partial version of nameprep on the username.
        username = username.toLowerCase().trim();
        String response;
        if (config.isSASLAuthenticationEnabled() &&
                saslAuthentication.hasNonAnonymousAuthentication()) {
            // Authenticate using SASL
            if (password != null) {
                response = saslAuthentication.authenticate(username, password, resource);
            }
            else {
                response = saslAuthentication
                        .authenticate(username, resource, config.getCallbackHandler());
            }
        }
        else {
            // Authenticate using Non-SASL
            response = new NonSASLAuthentication(this).authenticate(username, password, resource);
        }
        // Set the user.
        if (response != null) {
            this.user = response;
            // Update the serviceName with the one returned by the server
            config.setServiceName(StringUtils.parseServer(response));
        }
        else {
            this.user = username + "@" + getServiceName();
            if (resource != null) {
                this.user += "/" + resource;
            }
        }
        // If compression is enabled then request the server to use stream compression
        if (config.isCompressionEnabled()) {
            useCompression();
        }
        // Indicate that we're now authenticated.
        authenticated = true;
        anonymous = false;
        // Create the roster if it is not a reconnection or roster already created by getRoster()
        if (this.roster == null) {
            if(rosterStorage==null){
                this.roster = new Roster(this);
            }
            else{
                this.roster = new Roster(this,rosterStorage);
            }
        }
        if (config.isRosterLoadedAtLogin()) {
            this.roster.reload();
        }
        // Set presence to online.
        if (config.isSendPresence()) {
            packetWriter.sendPacket(new Presence(Presence.Type.available));
        }
        // Stores the authentication for future reconnection
        config.setLoginInfo(username, password, resource);
        // If debugging is enabled, change the the debug window title to include the
        // name we are now logged-in as.
        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
        // will be null
        if (config.isDebuggerEnabled() && debugger != null) {
            debugger.userHasLogged(user);
        }
    }

代码多,但真实有用的就是第16到第25行了, 这几行是调用了SASLAuthentication 类的authenticate()方法进行认证,SASLAuthentication 是一个授权认证的类,让我们到authenticate()方法去看一下

public String authenticate(String username, String password, String resource)
            throws XMPPException {
        // Locate the SASLMechanism to use
        String selectedMechanism = null;
        for (String mechanism : mechanismsPreferences) {
            if (implementedMechanisms.containsKey(mechanism) &&
                    serverMechanisms.contains(mechanism)) {
                selectedMechanism = mechanism;
                break;
            }
        }
        if (selectedMechanism != null) {
            // A SASL mechanism was found. Authenticate using the selected mechanism and then
            // proceed to bind a resource
            try {
                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
                currentMechanism = constructor.newInstance(this);
                // Trigger SASL authentication with the selected mechanism. We use
                // connection.getHost() since GSAPI requires the FQDN of the server, which
                // may not match the XMPP domain.
                currentMechanism.authenticate(username, connection.getServiceName(), password);
                // Wait until SASL negotiation finishes
                synchronized (this) {
                    if (!saslNegotiated && !saslFailed) {
                        try {
                            wait(30000);
                        }
                        catch (InterruptedException e) {
                            // Ignore
                        }
                    }
                }
                if (saslFailed) {
                    // SASL authentication failed and the server may have closed the connection
                    // so throw an exception
                    if (errorCondition != null) {
                        throw new XMPPException("SASL authentication " +
                                selectedMechanism + " failed: " + errorCondition);
                    }
                    else {
                        throw new XMPPException("SASL authentication failed using mechanism " +
                                selectedMechanism);
                    }
                }
                if (saslNegotiated) {
                    // Bind a resource for this connection and
                    return bindResourceAndEstablishSession(resource);
                }
                else {
                    // SASL authentication failed so try a Non-SASL authentication
                    return new NonSASLAuthentication(connection)
                            .authenticate(username, password, resource);
                }
            }
            catch (XMPPException e) {
                throw e;
            }
            catch (Exception e) {
                e.printStackTrace();
                // SASL authentication failed so try a Non-SASL authentication
                return new NonSASLAuthentication(connection)
                        .authenticate(username, password, resource);
            }
        }
        else {
            // No SASL method was found so try a Non-SASL authentication
            return new NonSASLAuthentication(connection).authenticate(username, password, resource);
        }

同样也是,真正有作用的就是第22行代码了,再点进到authenticate()方法中查看:

public void authenticate(String username, String host, String password) throws IOException, XMPPException {
        //Since we were not provided with a CallbackHandler, we will use our own with the given
        //information
        //Set the authenticationID as the username, since they must be the same in this case.
        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", host, props, this);
        authenticate();
    }

同样,这个方法将用户名,主机地址和密码进行保存,然后调用本类的authenticate()方法,再看:

protected void authenticate() throws IOException, XMPPException {
        String authenticationText = null;
        try {
            if(sc.hasInitialResponse()) {
                byte[] response = sc.evaluateChallenge(new byte[0]);
                authenticationText = StringUtils.encodeBase64(response, false);
            }
        } catch (SaslException e) {
            throw new XMPPException("SASL authentication failed", e);
        }
        // Send the authentication to the server
        getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));
    }

废话不多说,13行的send方法:

public void send(Packet stanza) {
        connection.sendPacket(stanza);
    }

有点眉目啦,connection是什么,其实就是XMPPConnection的实例对象,调用其sendpacket方法即可将封装啦用户名,密码和主机名的packet信息发送出去啦,再点进去一看

public void sendPacket(Packet packet) {
        if (!isConnected()) {
            throw new IllegalStateException("Not connected to server.");
        }
        if (packet == null) {
            throw new NullPointerException("Packet is null.");
        }
        packetWriter.sendPacket(packet);
    }

这个方法已经是XMPPConnection类中的操作啦, 绕了一大圈绕回来了, 最后还是让最先在connectUsingConfiguration()方法中创建的PacketWriter进行消息的发送,最后无非也是调用PacketWriter中writePackets方法将xml流信息写入writer中
个 好了,这样三步就完成 , 仔细一想内容真多

资料与源码都已上传.
(http://download.csdn.net/download/alanhand/9137313)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值