XMPP 開發(fā)學(xué)習(xí)
由于 aSmack 已經(jīng)棄用,目前使用的是 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 的同學(xué)可以進(jìn)入上面的鏈接危号。
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
如果是一個(gè)完整的 xmpp 還需要額外依賴 這幾個(gè)庫(kù)橡疼,我沒有使用
但是后面在做注冊(cè)用戶
這個(gè)需求的時(shí)候發(fā)現(xiàn) AccountManager
這個(gè)類在 smack-extensions
里面,如果需要一些完整的功能揭朝,還是全加上好了队贱,或者按照自己需求。
完整的 JID
Username@Domain/Resource
基本組成部分Node/Username - 用戶名/節(jié)點(diǎn) 用戶的基本標(biāo)識(shí)
Domain - 登陸的XMPP服務(wù)器域名
Resource - 資源/來源潭袱,用于區(qū)別客戶端來源, XMPP協(xié)議設(shè)計(jì)為可多客戶端同時(shí)登陸, Resource就是用于區(qū)分同一用戶不同端登陸
Bare - 除去Resource部分, 包含Username@Domain
接口使用
連接 (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后臺(tái)去看柱嫌,默認(rèn)是 5222, 我一開始寫成了9090,后來發(fā)現(xiàn)9090 的openfire 管理后臺(tái)的端口屯换。切記切記
登錄(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
是修改狀態(tài)
/**
* 更改用戶狀態(tài)
* @param code 狀態(tài)常量
*/
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, "設(shè)置在線")
presence = Presence(Presence.Type.available)
}
XMPP_STATUS_CHAT_ME -> {
log_d(TAG, "設(shè)置Q我吧")
presence = Presence(Presence.Type.available)
presence.mode = Presence.Mode.chat
}
XMPP_STATUS_BUSY -> {
log_d(TAG, "設(shè)置忙碌")
presence = Presence(Presence.Type.available)
presence.mode = Presence.Mode.dnd
}
XMPP_STATUS_LEAVE -> {
log_d(TAG, "設(shè)置離開")
presence = Presence(Presence.Type.available)
presence.mode = Presence.Mode.away
}
XMPP_STATUS_OFFLINE -> {
log_d(TAG, "設(shè)置離線")
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())
}
}
}
注冊(cè)(Register)
java.lang.IllegalStateException: Creating account over insecure connection
不安全的連接創(chuàng)建 account
accountManager.sensitiveOperationOverInsecureConnection(true);
現(xiàn)在不安全了
public boolean registerUser(String user_name, String passwd) {
if (mConnection != null && mConnection.isConnected()) {
// 已經(jīng)connect 上了编丘,才可以進(jìn)行注冊(cè)操作
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;
}
創(chuàng)建成功。openfile 可以看到彤悔,雖然沒有 name [doge]
如果已經(jīng)存在用戶的嘉抓,則會(huì)有異常。
發(fā)送消息(Send Message)
/**
* 發(fā)送單人聊天 消息
* @param chat 單人聊天室
* @param message 發(fā)送的消息
*/
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) 這個(gè)接口晕窑,在用spark 聊天的時(shí)候抑片,一直顯示我發(fā)送的是“廣播”,但我確實(shí)是只發(fā)給了一個(gè)人杨赤,也就是“發(fā)給一個(gè)人的廣播“蓝丙。這本身就是一個(gè)很怪的事情。而且這個(gè)“廣播”的丟失率還很高望拖。后面看了接口渺尘,發(fā)現(xiàn) chat.send(message: String) 默認(rèn)發(fā)送的是 normal類型的(normal為什么是廣播?)说敏,
后來改動(dòng)了一下鸥跟,好像改善了消息丟失的問題。
val stanza = Message()
stanza.body = message
stanza.type = Message.Type.chat
chat.send(stanza)
也只是修改了message 的 類型為 chat 而已。起碼 spark 里面不會(huì)顯示 廣播了医咨。
發(fā)送消息必須要先關(guān)注(訂閱)對(duì)方枫匾,不然的話發(fā)送不能成功。
添加好友
/**
* 添加好友 無分組
* @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
}
獲取好友列表
還封裝了好幾個(gè)接口
-
isAuthenticated()
判斷是否已經(jīng)登錄 -
isConnect()
判斷是否連接 -
disconnect()
斷開連接 -
getGroups()
獲取所有分組 -
getFriendsInGroup
獲取指定分組內(nèi)的所有好友 - ... ... 等等
使用的時(shí)候拟淮,我用了 RxJava 的 鏈?zhǔn)秸{(diào)用干茉,用起來還不錯(cuò)很泊。
登錄
fun login(user_name: String, passwd: String) {
log_d(TAG, "login name->$user_name")
curUserName = user_name
curPasswd = passwd
if (mXmppApiManager.isAuthenticated()) {
// 已經(jīng)登錄過
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()) {
// 先進(jìn)行連接
Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
emitter.onNext(mXmppApiManager.connect())
})
.subscribeOn(Schedulers.io())
.flatMap { isConnectSuccessful ->
if (isConnectSuccessful) {
// 連接成功后
// 進(jìn)行注冊(cè)
log_d(TAG, "xmpp connect successful")
Observable.just(mXmppApiManager.registerUser(user_name, passwd))
} else {
// 連接失敗
log_d(TAG, "xmpp connect failed")
// 幾秒后進(jìn)行重連
handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
Observable.just(false)
}
}
.flatMap { isRegisterSuccessful ->
if (isRegisterSuccessful) {
// 注冊(cè)成功
// 進(jìn)行登錄
log_d(TAG, "xmpp register successful")
Observable.just(mXmppApiManager.login(user_name, passwd))
} else {
// 注冊(cè)失敗
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 {
// 直接進(jìn)行登錄操作
Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
})
.subscribeOn(Schedulers.io())
.flatMap { isRegisterSuccessful ->
if (isRegisterSuccessful) {
// 注冊(cè)成功
// 進(jìn)行登錄
log_d(TAG, "xmpp register successful")
Observable.just(mXmppApiManager.login(user_name, passwd))
} else {
// 注冊(cè)失敗
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")
}
}
}
}
}
在登錄的時(shí)候先進(jìn)行一些連接的判斷。
這樣使用的時(shí)候就不需要去處理是否連接的問題委造。
還有注冊(cè)也是一樣
fun register(user_name: String, passwd: String) {
// 如果已經(jīng)驗(yàn)證過的戳鹅,需要退出登錄?
log_d(TAG, "register name->$user_name")
if (!mXmppApiManager.isConnected()) {
// 先進(jìn)行連接
Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
emitter.onNext(mXmppApiManager.connect())
})
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.flatMap { isConnectSuccessful ->
if (isConnectSuccessful) {
// 連接成功后
// 進(jìn)行注冊(cè)
log_d(TAG, "xmpp connect successful")
Observable.just(mXmppApiManager.registerUser(user_name, passwd))
} else {
// 連接失敗
log_d(TAG, "xmpp connect failed")
// 幾秒后進(jìn)行重連
handler.postDelayed(reconnectRunnable, RECONNECT_TIME_MILLSECOND)
Observable.just(false)
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isRegisterSuccessful ->
if (isRegisterSuccessful) {
// 注冊(cè)成功
// 進(jìn)行登錄
log_d(TAG, "xmpp register successful")
} else {
// 注冊(cè)失敗
log_d(TAG, "xmpp register failed")
}
}
} else {
// 直接進(jìn)行注冊(cè)操作
Observable.create(ObservableOnSubscribe<Boolean> { emitter ->
emitter.onNext(mXmppApiManager.registerUser(user_name, passwd))
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { isRegisterSuccessful ->
if (isRegisterSuccessful) {
// 注冊(cè)成功
log_d(TAG, "xmpp register successful")
} else {
// 注冊(cè)失敗
log_d(TAG, "xmpp register failed")
}
}
}
}
這樣用起來就比較方便。
需要注意的問題
如何保持XMPP連接穩(wěn)定
遇到問題
應(yīng)用程序處于活動(dòng)狀態(tài)昏兆。但是遲早枫虏,XMPP連接都沒有任何提示。服務(wù)器表示客戶端仍處于聯(lián)機(jī)狀態(tài)爬虱,但未發(fā)送或者接收數(shù)據(jù)包隶债。
XMPPConnection connection.isConnected()
返回 True。實(shí)際上 客戶端 無法知道 實(shí)際連接已經(jīng)丟失跑筝。
解決方案
-
首先在 openfire 服務(wù)器后臺(tái)發(fā)現(xiàn)了這個(gè)
服務(wù)器可以在斷開閑置連接前發(fā)送XMPP Ping請(qǐng)求給該客戶端死讹。客戶端必須回復(fù) Ping請(qǐng)求继蜡,這樣服務(wù)器能判斷客戶端連接確實(shí)是閑置狀態(tài)。 XMPP規(guī)范要求所有客戶端必須響應(yīng) Ping請(qǐng)求逛腿。如果客戶端不支持該P(yáng)ing請(qǐng)求稀并,必須返回錯(cuò)誤(這本身就是一個(gè)響應(yīng))。
所以单默,我們的客戶端必須對(duì) 服務(wù)器的 ping 請(qǐng)求進(jìn)行回復(fù)碘举。但是 smack 4.2 的 incomingMessage 僅僅會(huì)返回 用戶消息,所以在 incomingmessage 回調(diào)里面沒有辦法完成這件事情搁廓。
搜索一番之后發(fā)現(xiàn)一個(gè):
connection = new XMPPTCPConnection(config); PingManager pingManager = PingManager.getInstanceFor(connection); pingManager.setPingInterval(300);//seconds
?
在 connect 完成后 加了這個(gè)設(shè)置引颈,測(cè)試了一下,仍然后斷線的問題境蜕,但是頻率少了蝙场。應(yīng)該是有一點(diǎn)作用的。
-
添加
XMPPConnectionListener
連接監(jiān)聽如果需要保持長(zhǎng)期的連接粱年,需要對(duì)很多異常進(jìn)行進(jìn)行處理售滤,也就是重連機(jī)制的實(shí)現(xiàn)。
比如說
- 監(jiān)聽到
connectitonCloseError
的時(shí)候 - 監(jiān)聽到
connectionClose
的時(shí)候 - 或者是連接異常的時(shí)候
都可以加入 自動(dòng)重連的邏輯,從而保證 連接的穩(wěn)定性完箩。
- 監(jiān)聽到
-
添加 網(wǎng)絡(luò)變化 監(jiān)聽
移動(dòng)應(yīng)用的網(wǎng)絡(luò)情況千變?nèi)f化赐俗,有時(shí)候并不穩(wěn)定,所以需要加入網(wǎng)絡(luò)情況的判斷
class NetworkChangeReceiver: BroadcastReceiver() { private val TAG = NetworkChangeReceiver::class.java.simpleName override fun onReceive(context: Context?, intent: Intent?) { log_i(TAG, "onReceive 網(wǎng)絡(luò)狀態(tài)發(fā)生變化") // 如果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) // 移動(dòng)數(shù)據(jù)連接 val dataNetworkInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE) if (wifiNetworkInfo.isConnected && dataNetworkInfo.isConnected) { log_i(TAG, "Wi-Fi 已連接阻逮,移動(dòng)數(shù)據(jù)已連接") 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 已連接,移動(dòng)數(shù)據(jù)已斷開") 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 已斷開秩彤,移動(dòng)數(shù)據(jù)已連接") 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 已斷開叔扼,移動(dòng)數(shù)據(jù)已斷開") } } 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 { //檢測(cè)到有數(shù)據(jù)連接,但是并連接狀態(tài)未生效呐舔,此種狀態(tài)為wifi和數(shù)據(jù)同時(shí)已連接币励,以wifi連接優(yōu)先 if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && !networkInfo.isConnected) { result += 1 } //檢測(cè)到有數(shù)據(jù)連接,并連接狀態(tài)已生效珊拼,此種狀態(tài)為只有數(shù)據(jù)連接食呻,wifi并未連接上 if (networkInfo.type == ConnectivityManager.TYPE_MOBILE && networkInfo.isConnected) { result += 2 } //檢測(cè)到有wifi連接,連接狀態(tài)必為true if (networkInfo.type == ConnectivityManager.TYPE_WIFI) { result += 4 } } } // 存在組合情況澎现,以組合相加的唯一值作為最終狀態(tài)的判斷 when (result) { 0 -> { log_i(TAG, "Wi-Fi 已斷開仅胞,移動(dòng)數(shù)據(jù)已斷開") } 2 -> { log_i(TAG, "Wi-Fi 已斷開,移動(dòng)數(shù)據(jù)已連接") } 4 -> { log_i(TAG, "Wi-Fi 已連接剑辫,移動(dòng)數(shù)據(jù)已斷開") } 5 -> { log_i(TAG, "Wi-Fi 已連接干旧,移動(dòng)數(shù)據(jù)已連接") } } } } }
在網(wǎng)絡(luò)變化的時(shí)候,進(jìn)行連接判斷妹蔽,或者重連操作椎眯,也是一種保持穩(wěn)定性的方法。
使用的第三方庫(kù)
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"
// 只是在使用的時(shí)候 方便一些
compile 'io.reactivex.rxjava2:rxjava:2.1.8'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
- 可以根據(jù)網(wǎng)絡(luò)狀況自動(dòng)重連
- 可以在中斷后自動(dòng)進(jìn)行重連