XMPP协议之消息回执解决方案
苦恼中寻找方法
在开始做即时通信时就知道了消息回执这个概念,目的是解决通讯消息因为各种原因未送达对方而提供的一种保障机制。产生这个问题的原因主要是网络不稳定、服务器或者客户端一些异常导致没有接收到消息。
因为产品中使用的是openfire和spark的组合,所以一直就想在这个范围内找一个现成的方案,只不过通过阅读一些开发者的总结提到说openfire没有消息回执的方案。于是也看到了别人的方案:
- 发送者发送消息给服务端
- 服务端接收到消息后发送回执给发送者
- 发送者确认收到则结束,如果未收到就重发
- 服务端将消息记录一下,并推送给接收者,等待接收者的回执
- 接收者接收消息并发回执给服务端
- 服务端接收回执删除掉消息回执记录,表示已经发送完毕
- 如果一定时间内没收到重新推送消息给客户端
- 接收者如果收到消息进行去重处理,如果不重复的执行第5-6步
这个流程基本就是完成了消息回执的功能,核心点就是在于发送者-服务端-接收者三者之间建立一个消息确认机制。这个方案如果要自己实现的话需要定制一套消息协议了,这个实现方法比较多,对于XMPP来说发message、iq都可以。当然也可以看到这套方案会带来问题,就是每条消息都要执行一套确认,所以会增大流量和计算量。
流量对于移动网络来说还是很重要的,而且移动网络因为移动的原因很容易出现不稳定,所以自然这部分的流量可能会更大些。但是也正因为移动网络的不稳定就更需要消息回执来确认消息状态了,解决丢包的问题。
于是这就变成了一个双向的问题,只要能是尽量减少消息的体积以此来减少流量吧。
只不过对于我来说方法有了,怎么做是个问题,毕竟要实现一套这样的功能,还要保证稳定,否则这个消息回执功能本身不稳定还不如不要呢。基本的设计思路也有了:
- 客户端维护两个列表(发送回执队列和接收回执队列),用于保存发送/接收消息回执情况
- 服务端也维护一个列表,用于记录消息回执的接收与发送情况,服务端对列表进行超时检查,如果回执未发送的重发消息,如果收到重复的消息则去重处理
- 客户端定期检查两个列表里的回执状态,如果未收到回执的要做重发处理,如果收到的是重复的回执则进行去重处理
方案差不多有了,只不过在检阅网上资料时有了新的发现。
柳暗花明
在看别人的总结时发现XMPP有扩展协议是支持消息回执功能的,就是XEP-0184.了解下来这个协议确实是一套消息回执的实现方法,但是呢。。
- 它必须在openfire3.9以上版本才支持,这个可以在openfire的版本日志里可以看到
- 它只是一种端到端的消息回执,而且只有接收端收到消息后才会返回回执,这样对于发送者来说很麻烦,如果接收者不在线无法得知消息是否发出了,因为服务端不会告知发送者已经拿到消息了。只有等到接收者上线获取了消息后,由接收者发送一条确认的回执给接收者
这个看起来很美好的东西,发现不大好用啊。于是看了自己的openfire是4以上版本的,所以确实支持。然后检查了客户端使用的smack包里确实有XEP-0184的实现。
//这个类是一个统一调用的类
org.jivesoftware.smackx.receipts.DeliveryReceiptManager
//这个是发送者发送一个回执请求,告知客户端我要消息回执
org.jivesoftware.smackx.receipts.DeliveryReceiptRequest
//这个是接收者收到消息后返回的回执确认
org.jivesoftware.smackx.receipts.DeliveryReceipt
//这个是用于发送者监听接收者发来回执确认的事件
public interface ReceiptReceivedListener {
/**
* Callback invoked when a new receipt got received.
* <p>
* {@code receiptId} correspondents to the message ID, which can be obtained with
* {@link org.jivesoftware.smack.packet.Stanza#getStanzaId()}.
* </p>
*
* @param fromJid the jid that send this receipt
* @param toJid the jid which received this receipt
* @param receiptId the message ID of the stanza(/packet) which has been received and this receipt is for
* @param receipt the receipt
*/
void onReceiptReceived(String fromJid, String toJid, String receiptId, Stanza receipt);
}
有了这三个家伙确实是可以做一套消息确认的机制,但是要在客户端发送消息时发送一个DeliveryReceiptRequest,然后等待接收者发送回来的消息确认DeliveryReceipt。
public class ChatDemo {
public static void main(String[] args) {
AbstractXMPPConnection connection = SesseionHelper.newConn("192.168.11.111", 5222, "abc", "user1", "pwd1");
//在发消息之前通过DeliveryReceiptManager订阅回执
DeliveryReceiptManager drm = DeliveryReceiptManager.getInstanceFor(connection);
drm.addReceiptReceivedListener(new ReceiptReceivedListener() {
@Override
public void onReceiptReceived(String fromJid, String toJid,
String receiptId, Stanza receipt) {
System.err.println((new Date()).toString()+ " - drm:" + receipt.toXML());
}
});
Message msg = new Message("100069@bkos");
msg.setBody("回复我的消息1.");
msg.setType(Type.chat);
//将消息放到DeliveryReceiptRequest中,这样就可以在发送Message后发送回执请求
DeliveryReceiptRequest.addTo(msg);
try {
connection.sendStanza(msg);
} catch (NotConnectedException e) {
e.printStackTrace();
}
connection.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
System.out.println((new Date()).toString()+ "- processPacket:" + packet.toXML());
}
}, new StanzaFilter() {
@Override
public boolean accept(Stanza stanza) {
return stanza instanceof Message;
}
});
while (true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上面代码是发送者要完成的代码,这里并没有看到接收者返回回执的过程,这个实现在DeliveryReceiptManager里完成的。
private DeliveryReceiptManager(XMPPConnection connection) {
super(connection);
ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
sdm.addFeature(DeliveryReceipt.NAMESPACE);
// Add the packet listener to handling incoming delivery receipts
connection.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
DeliveryReceipt dr = DeliveryReceipt.from((Message) packet);
// notify listeners of incoming receipt
for (ReceiptReceivedListener l : receiptReceivedListeners) {
l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId(), packet);
}
}
}, MESSAGES_WITH_DELIVERY_RECEIPT);
// Add the packet listener to handle incoming delivery receipt requests
connection.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
final String from = packet.getFrom();
final XMPPConnection connection = connection();
switch (autoReceiptMode) {
case disabled:
return;
case ifIsSubscribed:
if (!Roster.getInstanceFor(connection).isSubscribedToMyPresence(from)) {
return;
}
break;
case always:
break;
}
final Message messageWithReceiptRequest = (Message) packet;
Message ack = receiptMessageFor(messageWithReceiptRequest);
if (ack == null) {
LOGGER.warning("Received message stanza with receipt request from '" + from
+ "' without a stanza ID set. Message: " + messageWithReceiptRequest);
return;
}
connection.sendStanza(ack);
}
}, MESSAGES_WITH_DEVLIERY_RECEIPT_REQUEST);
}
DeliveryReceiptManager里会订阅消息事件,当收到消息是需要回执时发送ack包,这里的ack就是带了DeliveryReceipt的一个消息包。
好了,这个XEP-0184差不多看明白了,但并不是想要的那种消息回执。它更像是手机消息或者邮件的那种接收确认回执。是端到端的一种确认机制。但是如果在服务端对这个消息做一些截取处理,做一个中间状态也是可以达到我们要的消息回执的状态的。
做法就是在服务端截取XEP-0184的消息,如果是请求消息DeliveryReceiptRequest则在服务端保存记录,同时服务端发送DeliveryReceipt(ack)给发送方。然后客户端照样接收消息返回ack后服务端截获更新服务端记录即可。
这种做法就是借用xep-0184协议来完成消息回执的功能。
真正的又一村
也不知道是否意外,在看一篇博文时发现了一个更有意思东西,就是XEP-0198.
它是干啥的呢?
流管理背后的基本概念是,初始化的实体(一个服务端或者客户端)和接收的实体(一个服务端)可以为更灵活的管理stream交换命令.下面两条流管理的特性被广泛的关注,因为它们可以提高网络的可靠性和终端用户的体验:
- Stanza确认(Stanza Acknowledgements) – 能够确认一段或者一系列Stanza是否已被某一方接收.
- 流恢复(Stream Resumption) – 能够迅速的恢复(resume)一个已经被终止的流.
这就突然发现又一村原来在这啊,XMPP毕竟最开始是基于TCP协议的,可以在流的基础上完成消息到达回执。它的特征也表明了这点,一是可以做消息确认,保证消息是否被另一方接收。另外一点就是在消息未确认接收时可以做恢复(也就是重试)。这不就完全满足我们消息回执的要求了吗?
它的工作过程是:一端发起请求,另一端必须以应答。
只不过在smack要4.1.x以上版本,而且默认是不开启流管理功能的,所以要手动的开启一下,剩下的事情由smack和openfire来完成。在建立TCPConnection前执行正面这句:
XMPPTCPConnection.setUseStreamManagementResumptionDefault(true);
这个代码就是说开启流恢复,当然流恢复开启了Stanza确认也是要开启的,可以看setUseStreamManagementResumptionDefault的实现,里面调用setUseStreamManagementDefault:
public static void setUseStreamManagementResumptionDefault(boolean useSmResumptionDefault) {
if (useSmResumptionDefault) {
// Also enable SM is resumption is enabled
setUseStreamManagementDefault(useSmResumptionDefault);
}
XMPPTCPConnection.useSmResumptionDefault = useSmResumptionDefault;
}
openfire服务端默认是开启这个功能的,在openfire.xml里有设置:
<!-- XEP-0198 properties -->
<stream>
<management>
<!-- Whether stream management is offered to clients by server. -->
<active>true</active>
<!-- Number of stanzas sent to client before a stream management
acknowledgement request is made. -->
<requestFrequency>5</requestFrequency>
</management>
</stream>
好了,这样就完成了消息回执的功能了。没想到XMPP协议已经支持了整个流程,省去了很多事情,同时openfire中websocket也是支持xep-198,所以手机端应该也是可以支持。
参考与引用
http://developerworks.github.io/2014/10/03/xmpp-xep-0198-stream-management/
http://blog.csdn.net/chszs/article/details/48576553
本文转至我自己的博客:
https://mini188.cn/c/XMPP协议之消息回执解决方案
XMPP协议之消息回执解决方案的更多相关文章
- (转)基于即时通信和LBS技术的位置感知服务(二):XMPP协议总结以及开源解决方案
在<基于即时通信和LBS技术的位置感知服务(一):提出问题及解决方案>一文中,提到尝试使用XMPP协议来实现即时通信.本文将对XMPP协议框架以及相关的C/S架构进行介绍,协议的底层实现不 ...
- 关于xmpp协议发送消息,登录认证SSL报错的问题
Q:错误描述如下 Traceback(most recent call last): File"/tails-share/features/scripts/otr-bot.py", ...
- .net平台 基于 XMPP协议的即时消息服务端简单实现
.net平台 基于 XMPP协议的即时消息服务端简单实现 昨天抽空学习了一下XMPP,在网上找了好久,中文的资料太少了所以做这个简单的例子,今天才完成.公司也正在准备开发基于XMPP协议的即时通讯工具 ...
- xmpp消息回执(6)
原始地址:XMPPFrameWork IOS 开发(七)消息回执 请参考:XEP-0184协议 协议内容: 发送消息时附加回执请求 <message from='northumberland@s ...
- 基于XMPP协议(openfire服务器)的消息推送实现
转自:http://blog.csdn.net/nomousewch/article/details/8088277 最近好像有不少朋友关注Android客户端消息推送的实现,我在之前的项目中用到过J ...
- [Python]实现XMPP协议即时通讯发送消息功能
#-*- coding: utf-8 -*- __author__ = 'tsbc' import xmpp import time #注意帐号信息,必须加@域名格式 from_user = 'che ...
- 搭建XMPP协议,实现自主推送消息到手机
关于服务器端向Android客户端的推送,主要有三种方式: 1.客户端定时去服务端取或者保持一个长Socket,从本质讲这个不叫推送,这是去服务端拽数据.但是实现简单,主要缺点:耗电等 2.Googl ...
- 基于XMPP协议的手机多方多端即时通讯方案
一.开发背景 1.国际背景 随着Internet技术的高速发展,即时通信已经成为一种广泛使用的通信方式.1996年Mirabilis公司推出了世界上第一个即时通信系统ICQ,不到10年间,即时通信(I ...
- 即时聊天IM之一 XMPP协议简述
合肥程序员群:49313181. 合肥实名程序员群:128131462 (不愿透露姓名和信息者勿加入) Q Q:408365330 E-Mail:egojit@qq.com 综述: ...
随机推荐
- CentOS7快速搭建LNMP环境
名词解释: LNMP:Linux+Nginx+MySql+PHPLAMP:LInux+Apache+MySql+PHPNginx的正确读法应该是Engine X我们使用CentOS自带的YUM来安装 ...
- python函数调用的四种方式 --基础重点
第一种:参数按顺序从第一个参数往后排#标准调用 # -*- coding: UTF-8 -*- def normal_invoke(x, y): print "--normal_invoke ...
- is not eligible for getting processed by all BeanPostProcessors
BeanPostProcessor是控制Bean初始化开始和初始化结束的接口.换句话说实现BeanPostProcessor的bean会在其他bean初始化之前完成,BeanPostProcessor ...
- selenium中浏览器及对应的驱动(可下载)
https://blog.csdn.net/huilan_same/article/details/52615123,灰蓝大神的总结,可直接下载 下载chrome浏览器,查看版本 http://blo ...
- struts2(二)---ModelDriven模型驱动
这篇文章是在上一篇文章(http://blog.csdn.net/u012116457/article/details/48194905)的基础上写的,大家可以先快速阅读一下上一篇. 这篇文章用来写一 ...
- Python定时任务
在项目中,我们可能遇到有定时任务的需求.其一:定时执行任务.例如每天早上 8 点定时推送早报.其二:每隔一个时间段就执行任务.比如:每隔一个小时提醒自己起来走动走动,避免长时间坐着.今天,我跟大家分享 ...
- windows7 dos修改mysql root密码
第一步:打开mysql 安装路径 选择bin文件 同时按下Shift+鼠标右键 点击"在此处打开命令" 第二步:输入mysql -u root -p 按回车键会提示输入密码 ...
- git fatal: 远程 origin 已经存在。
不小心将git远程地址配错了,再次配置提示以下错误: fatal: 远程 origin 已经存在. 此时只需要将远程配置删除,重新添加即可: git remote rm origin git remo ...
- [TJOI2015] 棋盘
Description 为了提高智商,ZJY去新世界旅游了.可是旅游过后的ZJY杯具的发现要打开通往原来世界的门,必须要解开门上面画的谜题.谜题是这样的:有个\(n\)行\(m\)列的棋盘,棋盘上可以 ...
- nsq源码阅读笔记之nsqd(三)——diskQueue
diskQueue是backendQueue接口的一个实现.backendQueue的作用是在实现在内存go channel缓冲区满的情况下对消息的处理的对象. 除了diskQueue外还有dummy ...