J'ai 7 solutions pour réaliser des push de messages web en temps réel, 7 !

Echange technique, compte public : Programmeur Xiaofu

Bonjour à tous, je suis Xiaofu~

J'ai un ami ~

J'ai fait une petite station cassée, et maintenant je souhaite implémenter une fonction de push de messages web au sein de la station. Oui, c'est le petit point rouge sur l'image ci-dessous, une fonction très couramment utilisée.

Cependant, il n'a pas encore compris comment s'y prendre. Ici, je l'aide à trier plusieurs solutions et à les mettre en œuvre simplement.

Télécharger le cas , rappelez-vous Star

Qu'est-ce qu'un message push (push)

Il existe de nombreux scénarios push. Par exemple, lorsque quelqu'un suit mon compte officiel, je reçois un message push pour m'inciter à cliquer pour ouvrir l'application.

Message push ( push) fait généralement référence à la poussée active de messages vers la page Web actuelle de l'utilisateur ou l'APP de l'appareil mobile via un certain outil par le personnel d'exploitation du site Web.

Le message push est généralement divisé en web端消息推送et 移动端消息推送.

Celui ci-dessus appartient à la poussée de message côté mobile, et la poussée de message côté Web est courante, comme les messages sur site, le nombre d'e-mails non lus, le nombre d'alarmes de surveillance, etc., et il est également largement utilisé .

Avant l'implémentation spécifique, analysons les exigences précédentes. En fait, la fonction est très simple. Tant qu'un événement est déclenché (partage actif de ressources ou envoi actif de messages en arrière-plan), le point rouge de notification sur la page Web sera en temps réel +1.

Habituellement, il existe plusieurs tables de messages push côté serveur pour enregistrer différents types de messages poussés par les utilisateurs déclenchant différents événements. Le front-end interroge activement (pulls) ou reçoit passivement (push) le nombre de tous les messages non lus des utilisateurs.

Le message push n'est rien de plus que deux formes de push ( push) et pull ( pull). Examinons chacune d'elles.

sondage court

L' interrogation ( polling) devrait être le moyen le plus simple d'implémenter un système de diffusion de messages. Ici, nous diviserons l'interrogation en 短轮询et 长轮询.

L'interrogation courte est facile à comprendre. À un intervalle de temps spécifié, le navigateur envoie une HTTPrequête au serveur, le serveur renvoie les données de message non lues au client en temps réel, et le navigateur les restitue et les affiche.

Une simple minuterie JS peut le faire, demander l'interface de comptage des messages non lus toutes les secondes et afficher les données renvoyées.

setInterval(() => {
  // 方法请求
  messageCount().then((res) => {
      if (res.code === 200) {
          this.messageCount = res.data
      }
  })
}, 1000);

L'effet est toujours bon. Bien que la mise en œuvre de l'interrogation courte soit simple, les lacunes sont également évidentes. Étant donné que les données push ne changent pas fréquemment, qu'il y ait ou non un nouveau message dans le backend à ce moment-là, le client fera un requête, ce qui causera inévitablement beaucoup au serveur : beaucoup de stress, un gaspillage de bande passante et de ressources serveur.

long scrutin

L'interrogation longue est une version améliorée de l'interrogation courte ci-dessus, qui peut réduire autant que possible le gaspillage des ressources du serveur tout en garantissant les performances relatives en temps réel des messages. L'interrogation longue est largement utilisée dans les intergiciels, tels que le centre de Nacosconfiguration apollo, la file d'attente de messages kafkaet RocketMQl'interrogation longue.

Le modèle d'interaction du centre de configuration Nacos est-il push ou pull ? Dans cet article, j'ai présenté en détail le principe de mise en œuvre des sondages Nacoslongs , et les amis intéressés peuvent y jeter un coup d'œil.

Cette fois, j'ai utilisé le apollocentre de configuration pour implémenter une interrogation longue et appliqué une classe DeferredResult, qui est servelet3.0un mécanisme de requête asynchrone fourni par l'encapsulation Spring plus tard, et la signification directe est de retarder le résultat.

DeferredResultIl peut permettre aux threads de conteneur de libérer rapidement les ressources occupées sans bloquer le thread de requête, afin d'accepter plus de requêtes pour améliorer le débit du système, puis de démarrer des threads de travail asynchrones pour traiter la logique métier réelle et terminer l'appel DeferredResult.setResult(200)pour soumettre la réponse. résultat.

Ci-dessous, nous utilisons une longue interrogation pour implémenter le message push.

Étant donné qu'un ID peut être surveillé par plusieurs longues requêtes d'interrogation, j'utilise la structure guavafournie par le package pour Multimapstocker les longues interrogations, et une clé peut correspondre à plusieurs valeurs. Une fois les changements clés surveillés, tous les sondages longs correspondants répondront. Le frontal obtient le code d'état du délai d'attente sans demande, connaît le changement de données, interroge activement l'interface pour connaître le nombre de messages non lus et met à jour les données de la page.

@Controller
@RequestMapping("/polling")
public class PollingController {

    // 存放监听某个Id的长轮询集合
    // 线程同步结构
    public static Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedMultimap(HashMultimap.create());

    /**
     * 公众号:程序员小富
     * 设置监听
     */
    @GetMapping(path = "watch/{id}")
    @ResponseBody
    public DeferredResult<String> watch(@PathVariable String id) {
        // 延迟对象设置超时时间
        DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
        // 异步请求完成时移除 key,防止内存溢出
        deferredResult.onCompletion(() -> {
            watchRequests.remove(id, deferredResult);
        });
        // 注册长轮询请求
        watchRequests.put(id, deferredResult);
        return deferredResult;
    }

    /**
     * 公众号:程序员小富
     * 变更数据
     */
    @GetMapping(path = "publish/{id}")
    @ResponseBody
    public String publish(@PathVariable String id) {
        // 数据变更 取出监听ID的所有长轮询请求,并一一响应处理
        if (watchRequests.containsKey(id)) {
            Collection<DeferredResult<String>> deferredResults = watchRequests.get(id);
            for (DeferredResult<String> deferredResult : deferredResults) {
                deferredResult.setResult("我更新了" + new Date());
            }
        }
        return "success";
    }

Lorsque la demande dépasse le délai d'expiration défini, une exception est levée AsyncRequestTimeoutException. Ici, la @ControllerAdvicecapture globale peut être utilisée pour un retour unifié. Le frontal obtient le code d'état convenu, puis lance à nouveau une longue demande d'interrogation, et ainsi de suite.

@ControllerAdvice
public class AsyncRequestTimeoutHandler {

    @ResponseStatus(HttpStatus.NOT_MODIFIED)
    @ResponseBody
    @ExceptionHandler(AsyncRequestTimeoutException.class)
    public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
        System.out.println("异步请求超时");
        return "304";
    }
}

Testons-le. Tout d'abord, la page lance une longue demande d'interrogation pour /polling/watch/10086surveiller le changement de message, la demande est suspendue, les données ne sont pas modifiées jusqu'à l'expiration du délai et la longue demande d'interrogation est lancée à nouveau ; puis les données sont modifiées manuellement /polling/publish/10086, le l'interrogation longue est répondue et le traitement frontal Une fois la logique métier terminée, la demande est lancée à nouveau, et ainsi de suite.

Par rapport à l'interrogation courte, l'interrogation longue a beaucoup amélioré les performances, mais elle génère toujours plus de requêtes, ce qui est un peu imparfait.

flux iframe

Le flux iframe consiste à insérer une <iframe>balise cachée dans la page, et via l' srcinterface API de demander le nombre de messages dans la page, une longue connexion est créée entre le serveur et le client, et le serveur continue à lui iframetransmettre des données.

Les données transmises sont généralement HTMLou un script intégré javascriptpour obtenir l'effet de mise à jour de la page en temps réel.

Cette méthode est simple à mettre en œuvre et le frontal n'a besoin que d'une seule <iframe>étiquette pour l'obtenir.

<iframe src="/iframe/message" style="display:none"></iframe>

Le serveur assemble directement les données de script html et js pour y responseécrire

@Controller
@RequestMapping("/iframe")
public class IframeController {
    @GetMapping(path = "message")
    public void message(HttpServletResponse response) throws IOException, InterruptedException {
        while (true) {
            response.setHeader("Pragma", "no-cache");
            response.setDateHeader("Expires", 0);
            response.setHeader("Cache-Control", "no-cache,no-store");
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().print(" <script type=\"text/javascript\">\n" +
                    "parent.document.getElementById('clock').innerHTML = \"" + count.get() + "\";" +
                    "parent.document.getElementById('count').innerHTML = \"" + count.get() + "\";" +
                    "</script>");
        }
    }
}

Mais personnellement, je ne le recommande pas, car il apparaîtra sur le navigateur que la requête n'a pas été chargée et l'icône continuera de tourner, ce qui est un tueur de trouble obsessionnel-compulsif.

SSE (à ma façon)

WebSocketDe nombreuses personnes ne savent peut-être pas qu'en plus de ce mécanisme bien connu, il existe également un événement envoyé par le serveur ( Server-sent events), abrégé lorsque le serveur envoie des messages au client SSE.

SSEIl est HTTPbasé sur le protocole Nous savons que le protocole HTTP au sens général ne permet pas au serveur de pousser activement des messages vers le client, mais SSE est une exception, ce qui change une façon de penser.

SSE ouvre un canal unidirectionnel entre le serveur et le client, et le serveur répond avec un paquet de données unique au lieu text/event-streamd'un type d'informations de flux de données, qui est transmis du serveur au client en cas de changement de données.

L'idée globale de mise en œuvre est un peu similaire à la lecture vidéo en ligne. Le flux vidéo sera poussé en continu vers le navigateur. Vous pouvez également comprendre que le client effectue un téléchargement qui prend beaucoup de temps (le réseau n'est pas fluide).

SSESemblable à la WebSocketfonction, la communication entre le serveur et le navigateur peut être établie et le serveur peut envoyer des messages au client, mais il existe quelques différences :

  • SSE est basé sur le protocole HTTP et ne nécessite pas de protocole spécial ni d'implémentation de serveur pour fonctionner ; WebSocketun serveur séparé est nécessaire pour gérer le protocole.
  • Communication unidirectionnelle SSE, uniquement une communication unidirectionnelle du serveur vers le client ; communication en duplex intégral webSocket, c'est-à-dire que les deux parties de la communication peuvent envoyer et recevoir des informations en même temps.
  • SSE est simple à mettre en œuvre et a un faible coût de développement, sans qu'il soit nécessaire d'introduire d'autres composants ; la transmission de données WebSocket nécessite une analyse secondaire et le seuil de développement est plus élevé.
  • SSE prend en charge la déconnexion et la reconnexion par défaut ; WebSocket doit être implémenté par lui-même.
  • SSE ne peut transmettre que des messages texte et les données binaires doivent être encodées et transmises ; WebSocket prend en charge la transmission de données binaires par défaut.

Comment choisir entre SSE et WebSocket ?

La technologie n'est pas bonne ou mauvaise, seulement ce qui est le plus approprié

SSE semble avoir été inconnu de tous, en partie à cause de l'avènement des WebSockets, qui fournissent un protocole plus riche pour effectuer une communication bidirectionnelle en duplex intégral. Pour les jeux, la messagerie instantanée et les scénarios qui nécessitent des mises à jour en temps quasi réel dans les deux sens, il est plus intéressant d'avoir un canal bidirectionnel.

Cependant, dans certains cas, il n'est pas nécessaire d'envoyer des données depuis le client. Et vous avez juste besoin de quelques mises à jour pour les opérations du serveur. Par exemple, les messages sur site, le nombre de messages non lus, les mises à jour de statut, les cotations boursières, le suivi des quantités, etc., SEEsont plus avantageux en termes de facilité de mise en œuvre et de coût. De plus, SSE possède WebSocketsplusieurs fonctionnalités qui manquent par conception, telles que la possibilité de : 自动重新连接, 事件IDet 发送任意事件.

Le frontal n'a besoin que de faire une requête HTTP, d'apporter un identifiant unique, d'ouvrir le flux d'événements et d'écouter les événements poussés par le serveur.

<script>
    let source = null;
    let userId = 7777
    if (window.EventSource) {
        // 建立连接
        source = new EventSource('http://localhost:7777/sse/sub/'+userId);
        setMessageInnerHTML("连接用户=" + userId);
        /**
         * 连接一旦建立,就会触发open事件
         * 另一种写法:source.onopen = function (event) {}
         */
        source.addEventListener('open', function (e) {
            setMessageInnerHTML("建立连接。。。");
        }, false);
        /**
         * 客户端收到服务器发来的数据
         * 另一种写法:source.onmessage = function (event) {}
         */
        source.addEventListener('message', function (e) {
            setMessageInnerHTML(e.data);
        });
    } else {
        setMessageInnerHTML("你的浏览器不支持SSE");
    }
</script>

La mise en place du serveur est plus simple, créer un SseEmitterobjet et le mettre en sseEmitterMapgestion

private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();

/**
 * 创建连接
 *
 * @date: 2022/7/12 14:51
 * @auther: 公众号:程序员小富
 */
public static SseEmitter connect(String userId) {
    try {
        // 设置超时时间,0表示不过期。默认30秒
        SseEmitter sseEmitter = new SseEmitter(0L);
        // 注册回调
        sseEmitter.onCompletion(completionCallBack(userId));
        sseEmitter.onError(errorCallBack(userId));
        sseEmitter.onTimeout(timeoutCallBack(userId));
        sseEmitterMap.put(userId, sseEmitter);
        count.getAndIncrement();
        return sseEmitter;
    } catch (Exception e) {
        log.info("创建新的sse连接异常,当前用户:{}", userId);
    }
    return null;
}

/**
 * 给指定用户发送消息
 *
 * @date: 2022/7/12 14:51
 * @auther: 公众号:程序员小富
 */
public static void sendMessage(String userId, String message) {

    if (sseEmitterMap.containsKey(userId)) {
        try {
            sseEmitterMap.get(userId).send(message);
        } catch (IOException e) {
            log.error("用户[{}]推送异常:{}", userId, e.getMessage());
            removeUser(userId);
        }
    }
}

Nous simulons le serveur pour pousser le message et voyons que le client reçoit le message, ce qui est cohérent avec notre effet attendu.

Remarque : SSE ne prend pas en charge les IEnavigateurs et fait un bon travail de compatibilité avec les autres principaux navigateurs.

MQTT

Qu'est-ce que le protocole MQTT ?

MQTTNom complet (Message Queue Telemetry Transport) : protocole de communication basé sur le mode publication/abonnement ( publish/ subscribe) 轻量级, qui obtient des messages en s'abonnant au sujet correspondant, et est un protocole de transport standard dans l'Internet des objets ( Internet of Thing).

Ce protocole sépare l'éditeur de message ( publisher) de l'abonné ( subscriber), de sorte qu'il peut fournir des services de messagerie fiables pour les appareils connectés à distance dans un environnement réseau non fiable. L'utilisation est quelque peu similaire à MQ traditionnel.

TCPLe protocole est situé au niveau de la couche transport, le MQTTprotocole est situé au niveau de la couche application et le MQTTprotocole est construit sur le protocole, ce qui signifie que tant que la pile de protocoles TCP/IPest prise en charge , le protocole peut être utilisé .TCP/IPMQTT

Pourquoi utiliser le protocole MQTT ?

MQTTPourquoi les protocoles sont-ils si privilégiés dans l'Internet des objets (IoT) ? Au lieu d'autres HTTPprotocoles ?

  • Tout d'abord HTTP, le protocole est un protocole synchrone et le client doit attendre la réponse du serveur après avoir demandé. Dans l'environnement Internet des objets (IOT), les appareils sont très affectés par l'environnement, comme une faible bande passante, une latence réseau élevée, une communication réseau instable, etc. De toute évidence, les protocoles de messagerie asynchrones sont plus adaptés aux IOTapplications.

  • HTTPC'est à sens unique, le client doit établir une connexion pour recevoir un message, et dans les applications de l'Internet des objets (IOT), les appareils ou les capteurs sont souvent des clients, ce qui signifie qu'ils ne peuvent pas recevoir passivement des commandes du réseau.

  • Habituellement, une commande ou un message doit être envoyé à tous les appareils du réseau. HTTPRéaliser une telle fonction est non seulement difficile, mais aussi extrêmement coûteux.

L'introduction et la pratique spécifiques au protocole MQTT, je ne les reprendrai pas ici, vous pouvez vous référer à mes deux articles précédents, eux aussi très détaillés.

Présentation du protocole MQTT

Je ne m'attendais pas à ce que springboot + rabbitmq fasse de la maison intelligente, ce serait si simple

MQTT implémente le message push

Messages non lus (petits points rouges), la pratique du push de messages en temps réel entre le front-end et RabbitMQ, le voleur est simple ~

Websocket

websocketCela devrait être une méthode que tout le monde connaît pour implémenter le message push.Nous l'avons également comparé avec websocket lorsque nous avons parlé de SSE ci-dessus.

WebSocket est un TCPprotocole de communication en duplex intégral sur une connexion, établissant un canal de communication entre un client et un serveur. Le navigateur et le serveur n'ont besoin que d'une seule prise de contact, et une connexion persistante peut être créée directement entre les deux, et une transmission de données bidirectionnelle peut être effectuée.

Des photos d'Internet

Springboot intègre le websocket et introduit websocketd'abord les kits d'outils associés, ce qui nécessite des coûts de développement supplémentaires par rapport à SSE.

<!-- 引入websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Le serveur utilise @ServerEndpointdes annotations pour marquer la classe actuelle en tant que serveur WebSocket, via lequel le client peut ws://localhost:7777/webSocket/10086se connecter au serveur WebSocket.

@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
    // 用来存在线连接数
    private static final Map<String, Session> sessionPool = new HashMap<String, Session>();
    /**
     * 公众号:程序员小富
     * 链接成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userId") String userId) {
        try {
            this.session = session;
            webSockets.add(this);
            sessionPool.put(userId, session);
            log.info("websocket消息: 有新的连接,总数为:" + webSockets.size());
        } catch (Exception e) {
        }
    }
    /**
     * 公众号:程序员小富
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message) {
        log.info("websocket消息: 收到客户端消息:" + message);
    }
    /**
     * 公众号:程序员小富
     * 此为单点消息
     */
    public void sendOneMessage(String userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("websocket消: 单点消息:" + message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Le frontal s'initialise pour ouvrir la connexion WebSocket, surveille l'état de la connexion, reçoit les données du serveur ou envoie des données au serveur.

<script>
    var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
    // 获取连接状态
    console.log('ws连接状态:' + ws.readyState);
    //监听是否连接成功
    ws.onopen = function () {
        console.log('ws连接状态:' + ws.readyState);
        //连接成功则发送一个数据
        ws.send('test1');
    }
    // 接听服务器发回的信息并处理展示
    ws.onmessage = function (data) {
        console.log('接收到来自服务器的消息:');
        console.log(data);
        //完成通信后关闭WebSocket连接
        ws.close();
    }
    // 监听连接关闭事件
    ws.onclose = function () {
        // 监听整个过程中websocket的状态
        console.log('ws连接状态:' + ws.readyState);
    }
    // 监听并处理error事件
    ws.onerror = function (error) {
        console.log(error);
    }
    function sendMessage() {
        var content = $("#message").val();
        $.ajax({
            url: '/socket/publish?userId=10086&message=' + content,
            type: 'GET',
            data: { "id": "7777", "content": content },
            success: function (data) {
                console.log(data)
            }
        })
    }
</script>

La page est initialisée pour établir une connexion Websocket, puis une communication bidirectionnelle peut être effectuée, et l'effet n'est pas mauvais.

poussée personnalisée

Ci-dessus, nous m'avons donné les principes et les implémentations de code de 6 solutions. Cependant, dans le processus de développement commercial réel, nous ne pouvons pas les utiliser aveuglément directement. Nous devons choisir la solution appropriée en fonction des caractéristiques de notre propre entreprise système et des scénarios réels.

Le moyen le plus direct de pousser est d'utiliser la troisième plate-forme de poussée. Après tout , la demande que l'argent peut résoudre n'est pas un problème . Il peut être utilisé directement sans développement, exploitation et maintenance compliqués. Cela permet d'économiser du temps, des efforts et des soucis. Comme goEasy et Jiguang push sont de très bons fournisseurs de services tiers.

En règle générale, les grandes entreprises ont développé elles-mêmes des plates-formes de diffusion de messages. Par exemple, le message du site Web que nous avons mis en œuvre cette fois n'est qu'un point de contact sur la plate-forme. Les SMS, les e-mails, le compte public WeChat et les petits programmes sont accessibles via n'importe quel canal qui peut atteindre les utilisateurs. entrez.

La photo vient d'internet

L'intérieur du système de push de message est assez complexe, comme la maintenance et l'examen du contenu du message, la délimitation de la foule de push, le filtrage et l'interception (la fréquence des règles de push, la période, la quantité, les listes noires et blanches, les mots-clés, etc. .), et des modules avec beaucoup de compensation d'échec de poussée. , Il existe également de nombreux scénarios qui impliquent techniquement de grandes quantités de données et une forte simultanéité. Donc, la façon dont nous l'implémentons aujourd'hui n'est qu'un petit jeu devant cet énorme système.

Adresse Github

J'ai implémenté tous les cas mentionnés dans l'article un par un, et les ai mis Githubsur la liste. Si vous le trouvez utile, il suffit de le mettre en vedette !

Portail : https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-realtime-data

Que vous soyez un nouveau venu ou un programmeur avec plusieurs années d'expérience, je pense que ce plan d'entretien vous aidera beaucoup, appuyez longuement sur le code QR pour suivre " Programmeur Xiaofu ", répondez " offre " pour l'obtenir par vous-même, je Je vous souhaite à tous L'offre est douce !

Des centaines d'e-books techniques divers ont été triés. Les étudiants qui en ont besoin peuvent suivre le compte officiel pour répondre [ 666 ] pour le récupérer. Il y a aussi des étudiants qui veulent rejoindre le groupe technique, vous pouvez m'ajouter en ami, parler technologie avec les grands, pousser en interne de temps en temps, et avoir tous les points internes des programmeurs.

{{o.name}}
{{m.name}}

Je suppose que tu aimes

Origine my.oschina.net/u/4455409/blog/5555830
conseillé
Classement