前言
Smack官方地址
即时通讯的项目用到Smack 4.1.9版本,由于项目需求Smack某些API不符合要求,有些需求采用自己后台接口实现,例如:文件上传、文件下载、注册、添加好友,删除好友,好友列表、用户详情、群列表、群成员、查询离线消息、查询历史消息、离开群,这样做的好处可以做到业务与IM分离,用户关系可以从其他系统导入直接使用,并且后面也还存在Pjsip的用户体系。
项目坑
项目里面最坑就是断线之后聊天室要重新加入才能收到消息,Smack 4.2.0修复了这个bug,断线之后不需要重新加入聊天室(群), [SMACK-572] - Rejoin MUC rooms after reconnect。
服务器做时间校验存在后台返回报文格式不对,无法正常进行时间校验(浪费两天找这个问题),升级成4.1.9解决这个问题,本来想用最新版本但是里面有些API的用法发生改变也就没用了,建议用Smack最新版本,可以避免很多麻烦。
[SMACK-716] - EntityTimeManager.getTime() does not set the recipients JID
代码
Android Studio build.gradle依赖:
//smack依赖包
compile 'org.igniterealtime.smack:smack-android-extensions:4.1.9'
compile 'org.igniterealtime.smack:smack-android:4.1.9'
compile 'org.igniterealtime.smack:smack-tcp:4.1.9'
compile 'org.igniterealtime.smack:smack-im:4.1.9'
- 初始化
这里只做一个简单的Smack初始化,数据加密可能还涉及到证书生成、添加校验。具体证书的生成可以参看这篇博客TLS 双向认证,即时通讯项目涉及到用户切换,所以设计XMPP流每个用户都重新初始化,否则用户退出后,切换到新用户存在各种问题。
try {
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(getClass().getResourceAsStream("/truststore"), storePass.toCharArray());
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
//初始化xmpp流
XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
.setServiceName(URConstant.xmppIp)
.setHost(URConstant.xmppIp)
.setPort(URConstant.xmppPort)
// 添加服务器域名、IP、端口
// 域名可以跟IP一样
//.setServiceName("applexmpp.com")
//.setHost("172.19.26.12")
//.setPort(5222)
.setSecurityMode(ConnectionConfiguration.SecurityMode.ifpossible)
//设置为true,利于开发调试
.setDebuggerEnabled(options.getDebuggerEnabled())
.setConnectTimeout(options.getConnectTimeout())
//添加证书
// .setCustomSSLContext(sslContext)
// .setHostnameVerifier()
.setCompressionEnabled(true)
.build();
connection = new XMPPTCPConnection(config);
- 注册
/**
* 注册
*
* @param account 注册帐号
* @param password 注册密码
* @return true 注册成功 false 注册失败
*/
public boolean register(String account, String password) {
try {
if (!connection.isConnected()) {
connection.connect();
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
try {
AccountManager.getInstance(connection).createAccount(account, password);
} catch (XMPPException | SmackException e) {
e.printStackTrace();
return false;
}
return true;
}
- 登陆
其中出现的一些异常可以做一接口回调反馈给UI层
/**
* 登录tigase服务器
*
* @param userName 用户名
* @param pwd 密码
*/
public void loginTigase(String userName, String pwd) {
try {
if (!connection.isConnected()) {
connection.connect();
}
} catch (Exception e) {
e.printStackTrace();
return;
}
if (connection.isConnected()) {
try {
connection.login(userName, pwd, "android");
if (connection.isAuthenticated()) {
// 允许自动连接
ReconnectionManager reconnectionManager = ReconnectionManager.getInstanceFor(connection);
// 重联间隔5秒
reconnectionManager.setFixedDelay(5);
reconnectionManager.enableAutomaticReconnection();//开启重联机制
// 维持ping
PingManager.setDefaultPingInterval(10);
PingManager pingManager = PingManager.getInstanceFor(connection);
// 监听连接状态
pingManager.registerPingFailedListener(connectListener);
//获取、校验服务器时间
TimeInfo.checkServerIntervalTime(connection);
} else {
}
} catch (XMPPException | IOException e) {
e.printStackTrace();
} catch (SmackException e) {
e.printStackTrace();
}
} else {
}
}
时间校验工具类
public class TimeInfo {
private static long serverIntervalTime;
/**
* 获取本地时间与服务器的时间差,用于消息时间验证
*/
public static long checkServerIntervalTime(XMPPTCPConnection connection) {
final EntityTimeManager timeManager = EntityTimeManager.getInstanceFor(connection);
EntityTimeManager.setAutoEnable(true);
try {
String utcTime = timeManager.getTime(URConstant.xmppIp).getUtc();
//返回时区
String tzo = timeManager.getTime(URConstant.xmppIp).getTzo();
long serverTime = utc2Local(utcTime, tzo);
return serverTime - System.currentTimeMillis();
} catch (SmackException.NoResponseException | XMPPException.XMPPErrorException | SmackException.NotConnectedException e) {
e.printStackTrace();
return 0;
}
}
/**
* 函数功能描述:UTC时间转本地时间格式
*
* @param utcTime UTC时间格式
* @param pysj 时区
* @return 本地时间格式的时间
* eg:utc2Local("2017-06-14 09:37:50.788+08:00", "yyyy-MM-dd HH:mm:ss.SSSXXX", "yyyy-MM-dd HH:mm:ss.SSS")
*/
public static long utc2Local(String utcTime, String pysj) {
@SuppressLint("SimpleDateFormat")
SimpleDateFormat utcFormater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");//UTC时间格式
utcFormater.setTimeZone(TimeZone.getTimeZone("UTC"));
Date gpsUTCDate = null;
try {
gpsUTCDate = utcFormater.parse(utcTime);
} catch (ParseException e) {
e.printStackTrace();
return System.currentTimeMillis();
}
@SuppressLint("SimpleDateFormat")
SimpleDateFormat localFormater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//当地时间格式
localFormater.setTimeZone(TimeZone.getTimeZone("GMT" + pysj));
String localTime = localFormater.format(gpsUTCDate.getTime());
Date date;
try {
date = localFormater.parse(localTime);
} catch (ParseException e) {
e.printStackTrace();
return System.currentTimeMillis();
}
return date.getTime();
}
}
- 登出
/**
* XMPP登出 将流置为空,新用户重新初始化
*/
public void logOut() {
URLog.d("XMPPConnectionManager 退出登陆");
//这里需要先将登陆状态改变为“离线”,再断开连接,不然在后台还是上线的状态
Presence presence = new Presence(Presence.Type.unavailable);
try {
connection.sendPacket(presence);
if (connection != null) {
connection.disconnect();
connection = null;
}
} catch (SmackException.NotConnectedException e) {
e.printStackTrace();
}
}
*注销
/**
* 注销前用户登录
*
* @return
*/
private boolean deleteUser() {
try {
try {
if (!connection.isConnected()) {
connection.connect();
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
AccountManager.getInstance(connection).deleteAccount();
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
感悟
这个项目让我知道日志的重要性,熟悉XMPP协议报文是非常必要的,要不然出现问题了就是一脸懵逼不知道从哪排查起。