博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android之基于XMPP协议即时通讯软件(三)
阅读量:6834 次
发布时间:2019-06-26

本文共 16967 字,大约阅读时间需要 56 分钟。

  hot3.png

CSDN博客之星投票请移驾:

本文主要介绍本应用的控制层具体实现。如需了解项目结构与框架,请移步之前系列文章:

另外,本项目已经升级到V1.0.1,已同步到开源中国代码托管:

今后更新也只会在此处同步,不会再打包上传到csdn,敬请悉知!

之前给大家介绍过,该小应用采用的是MVC设计模式,所以今天就跟大家分享一下控制层的具体实现。控制层担当一个非常重要的角色,既要处理界面传递过来的任务:点击发送消息、切换在线状态等,又要处理服务器发送过来的消息:有好友上线、收到新消息、保持长连接、掉线自动连接等。概括的说,总共分为以下四步:

①.实例化对象,作一些参数配置。

②.开始连接服务器,实现登陆。

③.注册各种事件监听,比如联系人动态变化、各种消息状态监听、开启长连接任务、掉线自动连接等。

④.用户主动退出,注销登录,断开连接。

第一步很简单,当用户启动该应用时,即启动本应用关健服务,并与界面Activity完成绑定,同时完成xmpp的参数配置,我这里是放在类的静态块里面完成的:

static {		registerSmackProviders();	}	// 做一些基本的配置	static void registerSmackProviders() {		ProviderManager pm = ProviderManager.getInstance();		// add IQ handling		pm.addIQProvider("query", "http://jabber.org/protocol/disco#info",				new DiscoverInfoProvider());		// add delayed delivery notifications		pm.addExtensionProvider("delay", "urn:xmpp:delay",				new DelayInfoProvider());		pm.addExtensionProvider("x", "jabber:x:delay", new DelayInfoProvider());		// add carbons and forwarding		pm.addExtensionProvider("forwarded", Forwarded.NAMESPACE,				new Forwarded.Provider());		pm.addExtensionProvider("sent", Carbon.NAMESPACE, new Carbon.Provider());		pm.addExtensionProvider("received", Carbon.NAMESPACE,				new Carbon.Provider());		// add delivery receipts		pm.addExtensionProvider(DeliveryReceipt.ELEMENT,				DeliveryReceipt.NAMESPACE, new DeliveryReceipt.Provider());		pm.addExtensionProvider(DeliveryReceiptRequest.ELEMENT,				DeliveryReceipt.NAMESPACE,				new DeliveryReceiptRequest.Provider());		// add XMPP Ping (XEP-0199)		pm.addIQProvider("ping", "urn:xmpp:ping", new PingProvider());		ServiceDiscoveryManager.setIdentityName(XMPP_IDENTITY_NAME);		ServiceDiscoveryManager.setIdentityType(XMPP_IDENTITY_TYPE);	}
第二步,当用户输入账号密码时,在服务中开启新线程启动连接服务器,传入参数信息(服务器、账号密码等)实现登录,同时会将登陆成功与否信息通过回调函数通知界面。也是比较简单的:

public boolean login(String account, String password) throws XXException {// 登陆实现		try {			if (mXMPPConnection.isConnected()) {// 首先判断是否还连接着服务器,需要先断开				try {					mXMPPConnection.disconnect();				} catch (Exception e) {					L.d("conn.disconnect() failed: " + e);				}			}			SmackConfiguration.setPacketReplyTimeout(PACKET_TIMEOUT);// 设置超时时间			SmackConfiguration.setKeepAliveInterval(-1);			SmackConfiguration.setDefaultPingInterval(0);			registerRosterListener();// 监听联系人动态变化			mXMPPConnection.connect();			if (!mXMPPConnection.isConnected()) {				throw new XXException("SMACK connect failed without exception!");			}			mXMPPConnection.addConnectionListener(new ConnectionListener() {				public void connectionClosedOnError(Exception e) {					mService.postConnectionFailed(e.getMessage());// 连接关闭时,动态反馈给服务				}				public void connectionClosed() {				}				public void reconnectingIn(int seconds) {				}				public void reconnectionFailed(Exception e) {				}				public void reconnectionSuccessful() {				}			});			initServiceDiscovery();// 与服务器交互消息监听,发送消息需要回执,判断是否发送成功			// SMACK auto-logins if we were authenticated before			if (!mXMPPConnection.isAuthenticated()) {				String ressource = PreferenceUtils.getPrefString(mService,						PreferenceConstants.RESSOURCE, XMPP_IDENTITY_NAME);				mXMPPConnection.login(account, password, ressource);			}			setStatusFromConfig();// 更新在线状态		} catch (XMPPException e) {			throw new XXException(e.getLocalizedMessage(),					e.getWrappedThrowable());		} catch (Exception e) {			// actually we just care for IllegalState or NullPointer or XMPPEx.			L.e(SmackImpl.class, "login(): " + Log.getStackTraceString(e));			throw new XXException(e.getLocalizedMessage(), e.getCause());		}		registerAllListener();// 注册监听其他的事件,比如新消息		return mXMPPConnection.isAuthenticated();	}

第三步比较关健,登陆成功后,我们就必须要监听服务器的各种消息状态变化,以及要维持自身的一个稳定性,即保持长连接和掉线自动重连。下面是注册所有监听的函数:

private void registerAllListener() {		// actually, authenticated must be true now, or an exception must have		// been thrown.		if (isAuthenticated()) {			registerMessageListener();// 注册新消息监听			registerMessageSendFailureListener();// 注册消息发送失败监听			registerPongListener();// 注册服务器回应ping消息监听			sendOfflineMessages();// 发送离线消息			if (mService == null) {				mXMPPConnection.disconnect();				return;			}			// we need to "ping" the service to let it know we are actually			// connected, even when no roster entries will come in			mService.rosterChanged();		}	}
①.注册联系人动态变化监听:第一次登陆时要同步本地数据库与服务器数据库的联系人,同时处理连接过程中联系人动态变化,比如说好友切换在线状态、有人申请加好友等。我这里没有将动态变化直接通知到界面线程,而是直接更新联系人数据库Roster.db,因为:我在界面线程监听了联系人数据库的动态变化,这就是ContentProvider的好处,下篇文章细说,这里就只提及一下。下面是关键部分代码:

private void registerRosterListener() {		mRoster = mXMPPConnection.getRoster();		mRosterListener = new RosterListener() {			private boolean isFristRoter;			@Override			public void presenceChanged(Presence presence) {// 联系人状态改变,比如在线或离开、隐身之类				L.i("presenceChanged(" + presence.getFrom() + "): " + presence);				String jabberID = getJabberID(presence.getFrom());				RosterEntry rosterEntry = mRoster.getEntry(jabberID);				updateRosterEntryInDB(rosterEntry);// 更新联系人数据库				mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线			}			@Override			public void entriesUpdated(Collection
entries) {// 更新数据库,第一次登陆 // TODO Auto-generated method stub L.i("entriesUpdated(" + entries + ")"); for (String entry : entries) { RosterEntry rosterEntry = mRoster.getEntry(entry); updateRosterEntryInDB(rosterEntry); } mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } @Override public void entriesDeleted(Collection
entries) {// 有好友删除时, L.i("entriesDeleted(" + entries + ")"); for (String entry : entries) { deleteRosterEntryFromDB(entry); } mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } @Override public void entriesAdded(Collection
entries) {// 有人添加好友时,我这里没有弹出对话框确认,直接添加到数据库 L.i("entriesAdded(" + entries + ")"); ContentValues[] cvs = new ContentValues[entries.size()]; int i = 0; for (String entry : entries) { RosterEntry rosterEntry = mRoster.getEntry(entry); cvs[i++] = getContentValuesForRosterEntry(rosterEntry); } mContentResolver.bulkInsert(RosterProvider.CONTENT_URI, cvs); if (isFristRoter) { isFristRoter = false; mService.rosterChanged();// 回调通知服务,主要是用来判断一下是否掉线 } } }; mRoster.addRosterListener(mRosterListener); }
②.注册消息监听,也跟联系人动态监听是一样的处理方式,将消息的动态变化同步到消息数据库Chat.db,并未直接通知界面,界面也是通过监听数据库变化来作出动态变化的。下面是关键代码:

private void registerMessageListener() {		// do not register multiple packet listeners		if (mPacketListener != null)			mXMPPConnection.removePacketListener(mPacketListener);		PacketTypeFilter filter = new PacketTypeFilter(Message.class);		mPacketListener = new PacketListener() {			public void processPacket(Packet packet) {				try {					if (packet instanceof Message) {// 如果是消息类型						Message msg = (Message) packet;						String chatMessage = msg.getBody();						// try to extract a carbon						Carbon cc = CarbonManager.getCarbon(msg);						if (cc != null								&& cc.getDirection() == Carbon.Direction.received) {// 收到的消息							L.d("carbon: " + cc.toXML());							msg = (Message) cc.getForwarded()									.getForwardedPacket();							chatMessage = msg.getBody();							// fall through						} else if (cc != null								&& cc.getDirection() == Carbon.Direction.sent) {// 如果是自己发送的消息,则添加到数据库后直接返回							L.d("carbon: " + cc.toXML());							msg = (Message) cc.getForwarded()									.getForwardedPacket();							chatMessage = msg.getBody();							if (chatMessage == null)								return;							String fromJID = getJabberID(msg.getTo());							addChatMessageToDB(ChatConstants.OUTGOING, fromJID,									chatMessage, ChatConstants.DS_SENT_OR_READ,									System.currentTimeMillis(),									msg.getPacketID());							// always return after adding							return;// 记得要返回						}						if (chatMessage == null) {							return;// 如果消息为空,直接返回了						}						if (msg.getType() == Message.Type.error) {							chatMessage = "
" + chatMessage;// 错误的消息类型 } long ts;// 消息时间戳 DelayInfo timestamp = (DelayInfo) msg.getExtension( "delay", "urn:xmpp:delay"); if (timestamp == null) timestamp = (DelayInfo) msg.getExtension("x", "jabber:x:delay"); if (timestamp != null) ts = timestamp.getStamp().getTime(); else ts = System.currentTimeMillis(); String fromJID = getJabberID(msg.getFrom());// 消息来自对象 addChatMessageToDB(ChatConstants.INCOMING, fromJID, chatMessage, ChatConstants.DS_NEW, ts, msg.getPacketID());// 存入数据库,并标记为新消息DS_NEW mService.newMessage(fromJID, chatMessage);// 通知service,处理是否需要显示通知栏, } } catch (Exception e) { // SMACK silently discards exceptions dropped from // processPacket :( L.e("failed to process packet:"); e.printStackTrace(); } } }; mXMPPConnection.addPacketListener(mPacketListener, filter);// 这是最关健的了,少了这句,前面的都是白费功夫 }
③.启动保持长连接任务。我这里与服务器保持长连接,其实是通过每隔一段时间(本应用是15分钟)去ping一次服务器,服务器收到此ping消息,会对应的回复一个pong消息,完成一次ping-pong的过程,我们暂且叫它为心跳。此ping-pong过程有一个唯一的id,用来区分每一次的ping-pong记录。为了保证应用在系统休眠时也能启动ping的任务,我们使用了闹钟服务,而不是定时器,关于闹钟服务具体使用,请参看我之前的博客:  。具体操作是:

从连上服务器完成登录15分钟后,闹钟响起,开始给服务器发送一条ping消息(随机生成一唯一ID),同时启动超时闹钟(本应用是30+3秒),如果服务器在30+3秒内回复了一条pong消息(与之前发送的ping消息ID相同),代表与服务器任然保持连接,则取消超时闹钟,完成一次ping-pong过程。如果在30+3秒内服务器未响应,或者回复的pong消息与之前发送的ping消息ID不一致,则认为与服务器已经断开。此时,将此消息反馈给界面,同时启动重连任务。实现长连接。

关健代码如下:

/***************** start 处理ping服务器消息 ***********************/	private void registerPongListener() {		// reset ping expectation on new connection		mPingID = null;// 初始化ping的id		if (mPongListener != null)			mXMPPConnection.removePacketListener(mPongListener);// 先移除之前监听对象		mPongListener = new PacketListener() {			@Override			public void processPacket(Packet packet) {				if (packet == null)					return;				if (packet.getPacketID().equals(mPingID)) {// 如果服务器返回的消息为ping服务器时的消息,说明没有掉线					L.i(String.format(							"Ping: server latency %1.3fs",							(System.currentTimeMillis() - mPingTimestamp) / 1000.));					mPingID = null;					((AlarmManager) mService							.getSystemService(Context.ALARM_SERVICE))							.cancel(mPongTimeoutAlarmPendIntent);// 取消超时闹钟				}			}		};		mXMPPConnection.addPacketListener(mPongListener, new PacketTypeFilter(				IQ.class));// 正式开始监听		mPingAlarmPendIntent = PendingIntent.getBroadcast(				mService.getApplicationContext(), 0, mPingAlarmIntent,				PendingIntent.FLAG_UPDATE_CURRENT);// 定时ping服务器,以此来确定是否掉线		mPongTimeoutAlarmPendIntent = PendingIntent.getBroadcast(				mService.getApplicationContext(), 0, mPongTimeoutAlarmIntent,				PendingIntent.FLAG_UPDATE_CURRENT);// 超时闹钟		mService.registerReceiver(mPingAlarmReceiver, new IntentFilter(				PING_ALARM));// 注册定时ping服务器广播接收者		mService.registerReceiver(mPongTimeoutAlarmReceiver, new IntentFilter(				PONG_TIMEOUT_ALARM));// 注册连接超时广播接收者		((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE))				.setInexactRepeating(AlarmManager.RTC_WAKEUP,						System.currentTimeMillis()								+ AlarmManager.INTERVAL_FIFTEEN_MINUTES,						AlarmManager.INTERVAL_FIFTEEN_MINUTES,						mPingAlarmPendIntent);// 15分钟ping以此服务器	}	/**	 * BroadcastReceiver to trigger reconnect on pong timeout.	 */	private class PongTimeoutAlarmReceiver extends BroadcastReceiver {		public void onReceive(Context ctx, Intent i) {			L.d("Ping: timeout for " + mPingID);			mService.postConnectionFailed(XXService.PONG_TIMEOUT);			//logout();// 超时就断开连接		}	}	/**	 * BroadcastReceiver to trigger sending pings to the server	 */	private class PingAlarmReceiver extends BroadcastReceiver {		public void onReceive(Context ctx, Intent i) {			if (mXMPPConnection.isAuthenticated()) {				sendServerPing();// 收到ping服务器的闹钟,即ping一下服务器			} else				L.d("Ping: alarm received, but not connected to server.");		}	}public void sendServerPing() {		if (mPingID != null) {// 此时说明上一次ping服务器还未回应,直接返回,直到连接超时			L.d("Ping: requested, but still waiting for " + mPingID);			return; // a ping is still on its way		}		Ping ping = new Ping();		ping.setType(Type.GET);		ping.setTo(PreferenceUtils.getPrefString(mService,				PreferenceConstants.Server, PreferenceConstants.GMAIL_SERVER));		mPingID = ping.getPacketID();// 此id其实是随机生成,但是唯一的		mPingTimestamp = System.currentTimeMillis();		L.d("Ping: sending ping " + mPingID);		mXMPPConnection.sendPacket(ping);// 发送ping消息		// register ping timeout handler: PACKET_TIMEOUT(30s) + 3s		((AlarmManager) mService.getSystemService(Context.ALARM_SERVICE)).set(				AlarmManager.RTC_WAKEUP, System.currentTimeMillis()						+ PACKET_TIMEOUT + 3000, mPongTimeoutAlarmPendIntent);// 此时需要启动超时判断的闹钟了,时间间隔为30+3秒	}
④.如果与服务器连接超时,则进入了我们掉线重连的任务了,因为
mService
.postConnectionFailed(XXService.
PONG_TIMEOUT
);回反馈到服务中,此时,我们会判断使用是否开启了掉线重连,关健代码如下,首先将消息由子线程发送到界面线程,文章开头说了,我们的连接是在新的线程中执行的:

public void postConnectionFailed(final String reason) {		mMainHandler.post(new Runnable() {			public void run() {				connectionFailed(reason);			}		});	}	private void connectionFailed(String reason) {		L.i(XXService.class, "connectionFailed: " + reason);		mConnectedState = DISCONNECTED;// 更新当前连接状态		if (mSmackable != null)			mSmackable.setStatusOffline();// 将所有联系人标记为离线		if (TextUtils.equals(reason, LOGOUT)) {// 如果是手动退出			((AlarmManager) getSystemService(Context.ALARM_SERVICE))					.cancel(mPAlarmIntent);			return;		}		// 回调		if (mConnectionStatusCallback != null) {			mConnectionStatusCallback.connectionStatusChanged(mConnectedState,					reason);			if (mIsFirstLoginAction)// 如果是第一次登录,就算登录失败也不需要继续				return;		}		// 无网络连接时,直接返回		if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) {			((AlarmManager) getSystemService(Context.ALARM_SERVICE))					.cancel(mPAlarmIntent);			return;		}		String account = PreferenceUtils.getPrefString(XXService.this,				PreferenceConstants.ACCOUNT, "");		String password = PreferenceUtils.getPrefString(XXService.this,				PreferenceConstants.PASSWORD, "");		// 无保存的帐号密码时,也直接返回		if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) {			L.d("account = null || password = null");			return;		}		// 如果不是手动退出并且需要重新连接,则开启重连闹钟		if (PreferenceUtils.getPrefBoolean(this,				PreferenceConstants.AUTO_RECONNECT, true)) {			L.d("connectionFailed(): registering reconnect in "					+ mReconnectTimeout + "s");			((AlarmManager) getSystemService(Context.ALARM_SERVICE)).set(					AlarmManager.RTC_WAKEUP, System.currentTimeMillis()							+ mReconnectTimeout * 1000, mPAlarmIntent);			mReconnectTimeout = mReconnectTimeout * 2;			if (mReconnectTimeout > RECONNECT_MAXIMUM)				mReconnectTimeout = RECONNECT_MAXIMUM;		} else {			((AlarmManager) getSystemService(Context.ALARM_SERVICE))					.cancel(mPAlarmIntent);		}	}
从上述代码中可以看出,在connectionFailed函数中,我们除了将此消息通知界面,同时会根据不同的reason来判断是否需要重连,如果是用户手动退出reason=LOGOUT,则直接返回咯,否则也是开启一个闹钟,启动重新连接任务,下面是该闹钟的接收处理:

// 自动重连广播接收者	private class ReconnectAlarmReceiver extends BroadcastReceiver {		public void onReceive(Context ctx, Intent i) {			L.d("Alarm received.");			if (!PreferenceUtils.getPrefBoolean(XXService.this,					PreferenceConstants.AUTO_RECONNECT, true)) {				return;			}			if (mConnectedState != DISCONNECTED) {				L.d("Reconnect attempt aborted: we are connected again!");				return;			}			String account = PreferenceUtils.getPrefString(XXService.this,					PreferenceConstants.ACCOUNT, "");			String password = PreferenceUtils.getPrefString(XXService.this,					PreferenceConstants.PASSWORD, "");			if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) {				L.d("account = null || password = null");				return;			}			Login(account, password);		}	}
是不是这样就实现了长连接呢?也许高兴得太早了,我们还有一个重要的因素没有考虑到,对了,就是手机网络,因为很多手机在系统休眠的时候是会断开网络连接的(应该是为了省电吧),所以,我们必须要动态监听网络变化,来做出处理,以下是关键代码:

public void onNetChange() {		if (NetUtil.getNetworkState(this) == NetUtil.NETWORN_NONE) {// 如果是网络断开,不作处理			connectionFailed(NETWORK_ERROR);			return;		}		if (isAuthenticated())// 如果已经连接上,直接返回			return;		String account = PreferenceUtils.getPrefString(XXService.this,				PreferenceConstants.ACCOUNT, "");		String password = PreferenceUtils.getPrefString(XXService.this,				PreferenceConstants.PASSWORD, "");		if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password))// 如果没有帐号,也直接返回			return;		if (!PreferenceUtils.getPrefBoolean(this,				PreferenceConstants.AUTO_RECONNECT, true))// 不需要重连			return;		Login(account, password);// 重连	}
OK,与服务器保持长连接,基本上就是这样了,其实还有一些问题没有考虑到,比如说内存过低,服务被系统回收,我们是没有考虑到的,这个就留个读者一个思考吧,我的想法是:在用户唤醒系统时也启动一次服务,接收此广播:

 <action android:name="android.intent.action.USER_PRESENT" />

⑤.实现服务在前台运行:这个我在之前的文章中有介绍过:,这里就不在赘述了。

第四步是用户主动退出,注销登陆,这个好像没有多少需要介绍的,无法是释放掉一些资源,关闭一些服务等等,也无需多说。看看代码即可:

// 退出	public boolean logout() {		// mIsNeedReConnection = false;// 手动退出就不需要重连闹钟了		boolean isLogout = false;		if (mConnectingThread != null) {			synchronized (mConnectingThread) {				try {					mConnectingThread.interrupt();					mConnectingThread.join(50);				} catch (InterruptedException e) {					L.e("doDisconnect: failed catching connecting thread");				} finally {					mConnectingThread = null;				}			}		}		if (mSmackable != null) {			isLogout = mSmackable.logout();			mSmackable = null;		}		connectionFailed(LOGOUT);// 手动退出		return isLogout;	}

好了,整个控制层大概就讲到这里,总结一下:

重要的是第三步:注册监听和长连接的处理,其中长连接处理也是最为关键和麻烦的。

文章比较长,其实也花了我几个小时的时间,首先感谢你看到了文章末尾,由于个人水平限制,难免会有一些失误或者不准确的地方,欢迎大家批评指出。

转载于:https://my.oschina.net/cjkall/blog/195777

你可能感兴趣的文章
数据库分片(Database Sharding)详解
查看>>
技术风险防控平台:打造金融交易系统的故障免疫能力
查看>>
怎样把酷狗音乐wav转化成高品质的MP3格式?
查看>>
浅谈算法学习
查看>>
前端知识点——图片
查看>>
thinkphp源码分析(三)—自动加载篇(Loader的分析)
查看>>
Blink 真香
查看>>
一块听听:Mixin 主网上线语音直播文字稿
查看>>
brew安装错误brew Error: /usr/local must be writable!
查看>>
可应用于实际的14个NLP突破性研究成果(三)
查看>>
[LeetCode] 41. First Missing Positive
查看>>
阿里如何将“高峰前扩容、高峰后缩容”的梦想照进现实?
查看>>
分布式系统关注点——初识「高可用」
查看>>
Node.js学习之路22——利用cheerio制作简单的网页爬虫
查看>>
聊一聊我对 React Context 的理解以及应用
查看>>
很多程序员都不会的问题,你知道多少?
查看>>
Scrapy-redis分布式组件
查看>>
package.json里的一些属性讲解
查看>>
leetcode 12 Integer to Roman
查看>>
Swoole+Lumen:同步编程风格调用MySQL异步查询
查看>>