「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」
一、前言
SMC定理(Single-Message Communication):端到端的通讯系统,消息不丢不重是不可能的。
在 IM 系统设计中,同样面临这两大难题:
- 消息丢失:用户A 发送一条消息给用户B,用户B 没有收到消息
- 消息重复:用户A 发送一条消息给用户B,用户B 收到两条消息
实际上并没有打破 SMC 定理,只是在不同层做了不同处理:
- 系统层面:消息重复了
- 业务层面:用户感知不到
(1)消失丢失有哪几种情况?
先从一个场景里来看下哪些环节可能存在丢消息的风险:
举个栗子:用户 A 给用户 B 发送一条消息。
根据上述时序图,发消息可分为两个部分:
- 发送方:用户A发送消息到服务端,对应步骤1、2、3
- 接收方:服务端推送消息给用户B,对应步骤4
那么按照发送方和接收方来分别讨论。
1)发送方
消息丢失,问题分析:
- 步骤1,用户A发送消息到服务端:可能网络波动等原因发送失败
- 步骤2,服务端存储消息:可能服务端宕机、
DB宕机等 - 步骤3,服务端返回消息给用户A:可能服务端宕机、用户A断连等
解决方案:
- 步骤1:客户端超时重发即可。
- 步骤2:客户端超时重发即可。
- 步骤3:服务端超时重发,客户端去重。
2)接收方
消息丢失,问题分析:
- 步骤4,服务端推送消息给用户B:
- 服务端宕机
- 用户 B 的设备在把消息写入本地
DB时,出现异常导致没能成功入库
解决方案:参考 TCP 协议的 ACK 机制,实现一套业务层的 ACK 协。
(2)解决丢失的方案:业务层 ACK 机制
ACK(Acknowledge,确认):在 TCP 协议中,默认提供了 ACK 机制,通过一个协议自带的标准的 ACK 数据包,来对通信方接收的数据进行确认,告知通信发送方已经确认成功接收了数据。
问题:为什么有了 TCP 协议本身的 ACK 机制,为什么还需要业务层的 ACK 机制?
存在这种场景:客户端接收消息成功了,但处理消息或存入
DB时候出现问题,导致用户看不到这个消息。这个时候就需要 业务层 上的
ACK保证了。
业务层 ACK 机制如下:
服务端会维护一个 “等待 ACK 队列”:
- 服务端推送消息给用户B:消息中包含消息
Id(msgId) - 服务端将当前消息加入到 “等待
ACK队列” - 用户B 成功接收到消息后,会给服务端发送一个
ACK包(包含对应消息Id) - 若服务端长时间没有收到对应的
ACK包,会尝试重发 - 服务端重发一定次数后仍没收到对应
ACK包,则:将消息存入离线消息font>
小结:ACK 机制 + 超时重传 + 去重,能解决大部分问题。
二、MQTT 实战
MQTT 中有 3 种可靠通信 QoS 级别:
QoS 0:表示消息最多收到一次,即消息可能丢失,但是不会重复。QoS 1:表示消息至少收到一次,即消息保证送达,但是可能重复。QoS 2:表示消息只会收到一次,即消息有且只有一次。
对应 Client 端使用:
public class DefaultMqttListener implements IMqttListener, Runnable {
long count = 0;
long start = System.currentTimeMillis();
private HashMap serverDetailsHash;
public DefaultMqttListener(HashMap serverProp) {
this.serverDetailsHash = serverProp;
}
CallbackConnection myconnection;
@Override
public void init() {
MQTT mqtt = new MQTT();
String user = env("APOLLO_USER", (String) serverDetailsHash.get("userName")); //No I18N
String password = env("APOLLO_PASSWORD", (String) serverDetailsHash.get("password")); //No I18N
String host = env("APOLLO_HOST", (String) serverDetailsHash.get("hostName")); //No I18N
int port = Integer.parseInt(env("APOLLO_PORT", (String) serverDetailsHash.get("port")));
try {
mqtt.setHost(host, port);
mqtt.setUserName(user);
mqtt.setPassword(password);
final CallbackConnection connection = mqtt.callbackConnection();
myconnection = connection;
connection.listener(new org.fusesource.mqtt.client.Listener() {
public void onConnected() {
}
public void onDisconnected() {
}
public void onFailure(Throwable value) {
value.printStackTrace();
System.exit(-2);
}
public void onPublish(UTF8Buffer topic, Buffer msg, Runnable ack) {
long time = System.currentTimeMillis();
callback(topic, msg, ack, connection, time);
}
});
connection.connect(new Callback<Void>() {
@Override
public void onSuccess(Void value) {
NmsLogMgr.M2MERR.log("MQTT Listener connected in ::::", Log.SUMMARY);
ArrayList getTopics = (ArrayList) serverDetailsHash.get("Topics");
for (int i = 0; i < getTopics.size(); i++) {
HashMap getTopic = (HashMap) getTopics.get(i);
String topicName = (String) getTopic.get("topicName");
String qosType = (String) getTopic.get("qosType");
Topic[] topic = {new Topic(topicName, getQosType(qosType))};
connection.subscribe(topic, new Callback<byte[]>() {
public void onSuccess(byte[] qoses) {
}
public void onFailure(Throwable value) {
value.printStackTrace();
System.exit(-2);
}
});
}
//Topic[] topics = {new Topic("adminTest", QoS.AT_LEAST_ONCE),new Topic("adminTest1", QoS.AT_LEAST_ONCE)};
}
@Override
public void onFailure(Throwable value) {
value.printStackTrace();
System.exit(-2);
}
});
// Wait forever..
synchronized (Listener.class) {
while (true) {
Listener.class.wait();
}
}
} catch (URISyntaxException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static String env(String key, String defaultValue) {
String rc = System.getenv(key);
if (rc == null) {
return defaultValue;
}
return rc;
}
@Override
public void callback(UTF8Buffer topic, Buffer msg, Runnable ack, CallbackConnection connection, long time) {
// TODO Auto-generated method stub
try {
String Message = msg.utf8().toString();
MQTTMessage mqttMsg = new MQTTMessage();
mqttMsg.setMQTTMessage(Message);
mqttMsg.setTime(time);
mqttMsg.setTopic(topic);
mqttMsg.sethostName((String) serverDetailsHash.get("hostName"));
MQTTCacheManager.mgr.addToCache(mqttMsg);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void close() {
// TODO Auto-generated method stub
NmsLogMgr.M2MERR.log("myconnection closed", Log.SUMMARY);
myconnection.disconnect(new Callback<Void>() {
@Override
public void onSuccess(Void value) {
System.exit(0);
}
@Override
public void onFailure(Throwable value) {
value.printStackTrace();
System.exit(-2);
}
});
}
@Override
public void run() {
this.init();
// TODO Auto-generated method stub
}
public QoS getQosType(String name) {
Properties qosContainer = new Properties();
qosContainer.put("0", QoS.AT_MOST_ONCE);
// qosContainer.put("1", QoS.AT_LEAST_ONCE);
// qosContainer.put("2", QoS.EXACTLY_ONCE);
QoS qosName = (QoS) qosContainer.get(name);
return qosName;
}
}