基于 Smack 的 xmpp 学习笔记

使用 kotlin, rxjava, smack 完成的一个xmpp demo。支持网络切换自动重连,可以直接 编译为 Android Library。

XMPP 开发学习

由于 aSmack 已经弃用,目前使用的是 smack 原版 4.2.0

aSmack is deprecated and obsolete. Starting with Version 4.1 Smack is able to run without modifications on Android.

More information on how to use Smack 4.1 in your Android Project can be found in the Smack 4.1 Readme and Upgrade Guide.

smack 的 github repo

Instructions how to use Smack in your Java or Android project are provided in the Smack 4.2 Readme and Upgrade Guide.

使用Android 的同学可以进入上面的链接。

Android 需要依赖的

dependencies {
  compile "org.igniterealtime.smack:smack-android-extensions:4.2.0"
  compile "org.igniterealtime.smack:smack-tcp:4.2.0"
}

A typical Smack setup may also want to additional declare dependencies on smack-tcp, smack-extensions and smack-experimental

如果是一个完整的 xmpp 还需要额外依赖 这几个库,我没有使用

但是后面在做注册用户这个需求的时候发现 AccountManager这个类在 smack-extensions里面,如果需要一些完整的功能,还是全加上好了,或者按照自己需求。

完整的 JID [email protected]/Resource 基本组成部分

Node/Username - 用户名/节点 用户的基本标识 Domain - 登陆的XMPP服务器域名 Resource - 资源/来源,用于区别客户端来源, XMPP协议设计为可多客户端同时登陆, Resource就是用于区分同一用户不同端登陆 Bare - 除去Resource部分, 包含[email protected]

接口使用

连接 (Connection)

public void connect() {

        try {
            XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder();
            configBuilder.setHostAddress(InetAddress.getByName(SERVER_IP));
            configBuilder.setHost(SERVER_IP);
            configBuilder.setPort(SERVER_PORT);
            configBuilder.setXmppDomain(SERVER_IP);
            configBuilder.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled);
            configBuilder.setDebuggerEnabled(true);
            configBuilder.setCompressionEnabled(true);
            configBuilder.setSendPresence(false);

            mConnection = new XMPPTCPConnection(configBuilder.build());

            mConnection.connect();

            Log.d(TAG, "connection status is -> " + String.valueOf(mConnection.isConnected()));
        } catch (UnknownHostException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (XmppStringprepException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (IOException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (SmackException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        } catch (XMPPException e) {
            e.printStackTrace();
            Log.d(TAG, e.toString());
        }
    }

端口记得要去openfire后台去看,默认是 5222, 我一开始写成了9090,后来发现9090 的openfire 管理后台的端口。切记切记

登录(Login)

/**
     * 登录
     * @param user_name 用户名
     * @param passwd 密码
     * @return 是否成功
     */
    fun login(user_name: String, passwd: String): Boolean {
        log_d(TAG, "login")
        if (mConnection != null && mConnection!!.isConnected) {
            try {
                mConnection?.login(user_name, passwd)
                setStatus(XMPP_STATUS_ONLINE)

//                mConnection?.addConnectionListener(this)
                ChatManager.getInstanceFor(mConnection).addIncomingListener(this)
                ChatManager.getInstanceFor(mConnection).addOutgoingListener(this)

                log_d(TAG, "login successful")
                return true

            } catch (e: XMPPException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            } catch (e: SmackException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
                if (e is SmackException.AlreadyLoggedInException) {
                    return true
                }
            } catch (e: IOException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            } catch (e: InterruptedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            }

        }
        return false
    }

setStatus是修改状态

/**
     * 更改用户状态
     * @param code 状态常量
     */
    fun setStatus(code: Int) {
        log_d(TAG, "setStatus")
        if (mConnection != null && mConnection!!.isConnected) {

            try {
                var presence: Presence? = null

                when (code) {
                    XMPP_STATUS_ONLINE -> {
                        log_d(TAG, "设置在线")
                        presence = Presence(Presence.Type.available)
                    }

                    XMPP_STATUS_CHAT_ME -> {
                        log_d(TAG, "设置Q我吧")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.chat
                    }

                    XMPP_STATUS_BUSY -> {
                        log_d(TAG, "设置忙碌")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.dnd
                    }

                    XMPP_STATUS_LEAVE -> {
                        log_d(TAG, "设置离开")
                        presence = Presence(Presence.Type.available)
                        presence.mode = Presence.Mode.away
                    }

                    XMPP_STATUS_OFFLINE -> {

                        log_d(TAG, "设置离线")
                        presence = Presence(Presence.Type.unavailable)
                    }
                }

                mConnection?.sendStanza(presence)
                log_d(TAG, "set status successful")
            } catch (e: SmackException.NotConnectedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
                connect()
            } catch (e: InterruptedException) {
                e.printStackTrace()
                log_e(TAG, e.toString())
            }


        }
    }

注册(Register)

java.lang.IllegalStateException: Creating account over insecure connection

不安全的连接创建 account

accountManager.sensitiveOperationOverInsecureConnection(true);

现在不安全了

public boolean registerUser(String user_name, String passwd) {
        if (mConnection != null && mConnection.isConnected()) {
            // 已经connect 上了,才可以进行注册操作
            try {
                AccountManager accountManager = AccountManager.getInstance(mConnection);
                if (accountManager.supportsAccountCreation()) {
                    accountManager.sensitiveOperationOverInsecureConnection(true);
                    accountManager.createAccount(Localpart.from(user_name), passwd);

                    return true;
                }
            } catch (SmackException.NoResponseException e) {
                e.printStackTrace();
            } catch (XMPPException.XMPPErrorException e) {
                e.printStackTrace();
                if (e.getXMPPError().getCondition() == XMPPError.Condition.conflict) {
                    // 用户名已存在
                }
            } catch (SmackException.NotConnectedException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (XmppStringprepException e) {
                e.printStackTrace();
            }
        }

        return false;

    }

创建成功。openfile 可以看到,虽然没有 name [doge]

如果已经存在用户的,则会有异常。

发送消息(Send Message)

/**
     * 发送单人聊天 消息
     * @param chat 单人聊天室
     * @param message 发送的消息
     */
    fun sendSingleMessage(chat: Chat, message: String) {
        log_d(TAG, "sendSingleMessage->$message")
        if (mConnection != null) {
            try {
                chat.send(message)
            } catch (e: SmackException.NotConnectedException) {
                log_e(TAG, e.toString())
                e.printStackTrace()
                connect()
            } catch (e: InterruptedException) {
                log_e(TAG, e.toString())
                e.printStackTrace()
            }
        }
    }

之前一直使用 chat.send(message: String) 这个接口,在用spark 聊天的时候,一直显示我发送的是“广播”,但我确实是只发给了一个人,也就是“发给一个人的广播“。这本身就是一个很怪的事情。而且这个“广播”的丢失率还很高。后面看了接口,发现 chat.send(message: String) 默认发送的是 normal类型的(normal为什么是广播?),

后来改动了一下,好像改善了消息丢失的问题。

val stanza = Message()
stanza.body = message
stanza.type = Message.Type.chat
chat.send(stanza)

也只是修改了message 的 类型为 chat 而已。起码 spark 里面不会显示 广播了。

发送消息必须要先关注(订阅)对方,不然的话发送不能成功。

添加好友

    /**
     * 添加好友 无分组
     * @param user_name jid
     * @param nick_name 用户昵称
     * @return 是否添加成功
     */
    fun addFriend(user_name: String, nick_name: String): Boolean {
        log_d(TAG, "addFriend")
        if (mConnection != null) {
            try {
                Roster.getInstanceFor(mConnection).createEntry(JidCreate.bareFrom(generateJID(user_name)),
                        nick_name, null)
                return true
            } catch (e: Exception) {
                log_e(TAG, e.toString())
                e.printStackTrace()
            }
        }
        return false
    }

添加好友到指定分组

    /**
     * 添加好友 加入到指定分组
     * @param user_name jid
     * @param nick_name 用户昵称
     * @param group_name 用户组
     * @return 是否添加成功
     */
    fun addFriendToGroup(user_name: String, nick_name: String, group_name: String): Boolean {
        log_d(TAG, "addFriendToGroup")
        if (mConnection != null) {
            try {
                val subscription = Presence(Presence.Type.subscribe)
                subscription.to = JidCreate.entityBareFrom(generateJID(user_name))
                mConnection?.sendStanza(subscription)
                Roster.getInstanceFor(mConnection).createEntry(JidCreate.entityBareFrom(generateJID(user_name)),
                        nick_name,
                        arrayOf(group_name))

                return true
            } catch (e: Exception) {
                log_e(TAG, e.toString())
            }
        }
        return false
    }

获取好友(Get All Friends)

    /**
     * 获取所有好友信息
     * @return 所有好友列表
     */
    fun getAllFriends(): List<RosterEntry>? {
        log_d(TAG, "getAllFriends")
        if (mConnection != null) {
            val entryList = ArrayList<RosterEntry>()
            val rotryEntry = Roster.getInstanceFor(mConnection).entries
            entryList += rotryEntry
            return entryList
        }

        return null
    }

获取好友列表

还封装了好几个接口

  • isAuthenticated() 判断是否已经登录
  • isConnect() 判断是否连接
  • disconnect() 断开连接
  • getGroups() 获取所有分组
  • getFriendsInGroup 获取指定分组内的所有好友
  • … … 等等

使用的时候,我用了 RxJava 的 链式调用,用起来还不错。

登录
fun login(user_name: String, passwd: String) {
            log_d(TAG, "login name->$user_name")

            curUserName = user_name
            curPasswd = passwd
            if (mXmppApiManager.isAuthenticated()) {
                // 已经登录过
                val userName = mXmppApiManager.getAuthenticatedUser()
                if (!TextUtils.isEmpty(userName)) {
                    log_d(TAG, "account logined as -> " + userName!!)
                } else {
                    log_d(TAG, "login successful as -> " + user_name)
                }
            } else {
                if (!mXmppApiManager.isConnected()) {
                    // 先进行连接

                    Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                        emitter.onNext(mXmppApiManager.connect())
                    })
                            .subscribeOn(Schedulers.io())
                            .flatMap { isConnectSuccessful ->
                                if (isConnectSuccessful) {
                                    // 连接成功后
                                    // 进行注册
                                    log_d(TAG, "xmpp connect successful")
                                    Observable.just(mXmppApiManager.registerUser(user_name, passwd))
                                } else {
                                    // 连接失败
                                    log_d(TAG, "xmpp connect failed")
                                    // 几秒后进行重连
                                    handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
                                    Observable.just(false)
                                }
                            }
                            .flatMap { isRegisterSuccessful ->
                                if (isRegisterSuccessful) {
                                    // 注册成功
                                    // 进行登录
                                    log_d(TAG, "xmpp register successful")
                                    Observable.just(mXmppApiManager.login(user_name, passwd))
                                } else {
                                    // 注册失败
                                    log_d(TAG, "xmpp register failed")
                                    Observable.just(false)
                                }
                            }.observeOn(AndroidSchedulers.mainThread())
                            .subscribe({ isLoginSuccessful ->
                                if (isLoginSuccessful!!) {
                                    log_d(TAG, "login successful as -> " + mXmppApiManager.getAuthenticatedUser()!!)
                                } else {
                                    log_d(TAG, "login failed")
                                }
                            })
                } else {
                    // 直接进行登录操作
                    Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                        emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
                    })
                            .subscribeOn(Schedulers.io())
                            .flatMap { isRegisterSuccessful ->
                                if (isRegisterSuccessful) {
                                    // 注册成功
                                    // 进行登录
                                    log_d(TAG, "xmpp register successful")
                                    Observable.just(mXmppApiManager.login(user_name, passwd))
                                } else {
                                    // 注册失败
                                    log_d(TAG, "xmpp register failed")
                                    Observable.just(false)
                                }
                            }.observeOn(AndroidSchedulers.mainThread())
                            .subscribe { isLoginSuccessful ->
                                if (isLoginSuccessful!!) {
                                    val userName = mXmppApiManager.getAuthenticatedUser()
                                    if (!TextUtils.isEmpty(userName)) {
                                        log_d(TAG, "account logined as -> " + userName!!)
                                    } else {
                                        log_d(TAG, "login successful as -> " + userName)
                                    }
                                } else {
                                    log_d(TAG, "login failed")
                                }
                            }
                }

            }
        }

在登录的时候先进行一些连接的判断。

这样使用的时候就不需要去处理是否连接的问题。

还有注册也是一样

        fun register(user_name: String, passwd: String) {
            // 如果已经验证过的,需要退出登录?
            log_d(TAG, "register name->$user_name")

            if (!mXmppApiManager.isConnected()) {
                // 先进行连接
                Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                    emitter.onNext(mXmppApiManager.connect())
                })
                        .subscribeOn(Schedulers.io())
                        .observeOn(Schedulers.io())
                        .flatMap { isConnectSuccessful ->
                            if (isConnectSuccessful) {
                                // 连接成功后
                                // 进行注册
                                log_d(TAG, "xmpp connect successful")
                                Observable.just(mXmppApiManager.registerUser(user_name, passwd))
                            } else {
                                // 连接失败
                                log_d(TAG, "xmpp connect failed")
                                // 几秒后进行重连
                                handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
                                Observable.just(false)
                            }
                        }
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe { isRegisterSuccessful ->
                            if (isRegisterSuccessful) {
                                // 注册成功
                                // 进行登录
                                log_d(TAG, "xmpp register successful")
                            } else {
                                // 注册失败
                                log_d(TAG, "xmpp register failed")
                            }
                        }
            } else {
                // 直接进行注册操作
                Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
                    emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
                })
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe { isRegisterSuccessful ->
                            if (isRegisterSuccessful) {
                                // 注册成功
                                log_d(TAG, "xmpp register successful")
                            } else {
                                // 注册失败
                                log_d(TAG, "xmpp register failed")
                            }
                        }
            }
        }

这样用起来就比较方便。

需要注意的问题

如何保持XMPP连接稳定

遇到问题

应用程序处于活动状态。但是迟早,XMPP连接都没有任何提示。服务器表示客户端仍处于联机状态,但未发送或者接收数据包。

XMPPConnection connection.isConnected()返回 True。

实际上 客户端 无法知道 实际连接已经丢失。

解决方案
  1. 首先在 openfire 服务器后台发现了这个

服务器可以在断开闲置连接前发送XMPP Ping请求给该客户端。客户端必须回复 Ping请求,这样服务器能判断客户端连接确实是闲置状态。 XMPP规范要求所有客户端必须响应 Ping请求。如果客户端不支持该Ping请求,必须返回错误(这本身就是一个响应)。

所以,我们的客户端必须对 服务器的 ping 请求进行回复。但是 smack 4.2 的 incomingMessage 仅仅会返回 用户消息,所以在 incomingmessage 回调里面没有办法完成这件事情。

搜索一番之后发现一个:

   connection = new XMPPTCPConnection(config);  
   PingManager pingManager = PingManager.getInstanceFor(connection); pingManager.setPingInterval(300);//seconds

在 connect 完成后 加了这个设置,测试了一下,仍然后断线的问题,但是频率少了。应该是有一点作用的。

  1. 添加 XMPPConnectionListener 连接监听

如果需要保持长期的连接,需要对很多异常进行进行处理,也就是重连机制的实现。

比如说

  • 监听到 connectitonCloseError 的时候
  • 监听到 connectionClose 的时候
  • 或者是连接异常的时候

都可以加入 自动重连的逻辑,从而保证 连接的稳定性。

  1. 添加 网络变化 监听

移动应用的网络情况千变万化,有时候并不稳定,所以需要加入网络情况的判断

   class NetworkChangeReceiver: BroadcastReceiver() {

       private val TAG = NetworkChangeReceiver::class.java.simpleName

       override fun onReceive(context: Context?, intent: Intent?) {
           log_i(TAG, "onReceive 网络状态发生变化")

           // 如果api小于21,getNetworkinfo(int networType) 已弃用
           if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
               log_i(TAG, "API 小于 21")

               val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

               // Wi-Fi 连接
               val wifiNetworkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
               // 移动数据连接
               val dataNetworkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE)

               if (wifiNetworkInfo.isConnected && dataNetworkInfo.isConnected) {
                   log_i(TAG, "Wi-Fi 已连接,移动数据已连接")
                   RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_WIFI_CONNECTED) as Object)
               } else if (wifiNetworkInfo.isConnected && !dataNetworkInfo.isConnected) {
                   log_i(TAG, "Wi-Fi 已连接,移动数据已断开")
                   RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_WIFI_CONNECTED) as Object)
               } else if (!wifiNetworkInfo.isConnected && dataNetworkInfo.isConnected) {
                   log_i(TAG, "Wi-Fi 已断开,移动数据已连接")
                   RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_MOBILE_CONNECTED) as Object)
               } else {
                   RxBus2.getIntanceBus().post(RxMessage(RxMessageConstants.MESSAGE_TYPE_NETWOKR, RxMessageConstants.MESSAGE_NETWORK_DISCONNETED) as Object)
                   log_i(TAG, "Wi-Fi 已断开,移动数据已断开")
               }

           } else {
               log_i(TAG, "API 大于 21")

               val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

               val networks = connectivityManager.allNetworks

               var result = 0 // mobile false = 1, mobile true = 2 wifi = 4

               for (network in networks) {
                   val networkInfo = connectivityManager.getNetworkInfo(network)

                   networkInfo?.let {
                       //检测到有数据连接,但是并连接状态未生效,此种状态为wifi和数据同时已连接,以wifi连接优先
                       if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && !networkInfo.isConnected) {
                           result += 1
                       }

                       //检测到有数据连接,并连接状态已生效,此种状态为只有数据连接,wifi并未连接上
                       if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && networkInfo.isConnected) {
                           result += 2
                       }

                       //检测到有wifi连接,连接状态必为true
                       if (networkInfo.type == ConnectivityManager.TYPE_WIFI) {
                           result += 4
                        }
                  }
            }
             
             // 存在组合情况,以组合相加的唯一值作为最终状态的判断
              when (result) {
                  0   ->  {
                      log_i(TAG, "Wi-Fi 已断开,移动数据已断开")
                  }
                  2   ->  {
                      log_i(TAG, "Wi-Fi 已断开,移动数据已连接")
                  }
                  4   ->  {
                      log_i(TAG, "Wi-Fi 已连接,移动数据已断开")
                  }
                  5   ->  {
                      log_i(TAG, "Wi-Fi 已连接,移动数据已连接")
                  }
              }
          }

      }
   }           

在网络变化的时候,进行连接判断,或者重连操作,也是一种保持稳定性的方法。

使用的第三方库

compile "org.igniterealtime.smack:smack-android-extensions:4.2.0"
compile "org.igniterealtime.smack:smack-tcp:4.2.0"
compile "org.igniterealtime.smack:smack-extensions:4.2.0"

// 只是在使用的时候 方便一些
compile 'io.reactivex.rxjava2:rxjava:2.1.8'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
  • 可以根据网络状况自动重连
  • 可以在中断后自动进行重连

代码在这里

 
comments powered by Disqus