【IM】ACK 机制

812 阅读4分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

一、前言

SMC 定理(Single-Message Communication):端到端的通讯系统,消息不丢不重是不可能的。

IM 系统设计中,同样面临这两大难题:

  1. 消息丢失:用户A 发送一条消息给用户B,用户B 没有收到消息
  2. 消息重复:用户A 发送一条消息给用户B,用户B 收到两条消息

实际上并没有打破 SMC 定理,只是在不同层做了不同处理:

  1. 系统层面:消息重复了
  2. 业务层面:用户感知不到

(1)消失丢失有哪几种情况?

先从一个场景里来看下哪些环节可能存在丢消息的风险:

举个栗子:用户 A 给用户 B 发送一条消息。

2022-02-0621-07-19.png

根据上述时序图,发消息可分为两个部分:

  1. 发送方:用户A发送消息到服务端,对应步骤1、2、3
  2. 接收方:服务端推送消息给用户B,对应步骤4

那么按照发送方和接收方来分别讨论。

1)发送方

消息丢失,问题分析:

  1. 步骤1,用户A发送消息到服务端:可能网络波动等原因发送失败
  2. 步骤2,服务端存储消息:可能服务端宕机、DB 宕机等
  3. 步骤3,服务端返回消息给用户A:可能服务端宕机、用户A断连等

解决方案:

  1. 步骤1:客户端超时重发即可。
  2. 步骤2:客户端超时重发即可。
  3. 步骤3:服务端超时重发,客户端去重。

2)接收方

消息丢失,问题分析:

  • 步骤4,服务端推送消息给用户B:
    • 服务端宕机
    • 用户 B 的设备在把消息写入本地 DB 时,出现异常导致没能成功入库

解决方案:参考 TCP 协议的 ACK 机制,实现一套业务层的 ACK 协。


(2)解决丢失的方案:业务层 ACK 机制

ACKAcknowledge,确认):在 TCP 协议中,默认提供了 ACK 机制,通过一个协议自带的标准的 ACK 数据包,来对通信方接收的数据进行确认,告知通信发送方已经确认成功接收了数据。

问题:为什么有了 TCP 协议本身的 ACK 机制,为什么还需要业务层的 ACK 机制?

存在这种场景:客户端接收消息成功了,但处理消息或存入 DB 时候出现问题,导致用户看不到这个消息。

这个时候就需要 业务层 上的 ACK 保证了。

业务层 ACK 机制如下:

2022-02-0622-43-36.png

服务端会维护一个 “等待 ACK 队列”:

  • 服务端推送消息给用户B:消息中包含消息 IdmsgId
  • 服务端将当前消息加入到 “等待 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;
    }
}