Seven solutions to realize real-time message push on the web!

I have a friend ~

I have built a small broken website, and now I want to implement a function of pushing web messages within the website. Yes, it is the little red dot in the picture below, a very commonly used function.

picture

But he hasn't figured out how to do it yet. Here I helped him sort out several solutions and implement them simply.

picture

What is push message?

There are many push scenarios. For example, if someone follows my official account, I will receive a push message to attract me to click to open the app.

Message push ( push) usually refers to the active message push by website operators and other personnel to the user's current web page or mobile device APP through some kind of tool.

Message push is generally divided into web端消息推送and 移动端消息推送.

picture

The above type of message push belongs to the mobile terminal. Common message push messages on the web terminal include site messages, the number of unread emails, the number of monitoring alarms, etc., and are also widely used.

picture

Before the specific implementation, let's analyze the previous requirements. In fact, the function is very simple. As long as a certain event is triggered (actively sharing resources or actively pushing messages in the background), the notification red dot on the web page will be real-time +1.

Usually there are several message push tables on the server to record different types of messages pushed by users triggering different events. The front end actively queries (pull) or passively receives (push) the number of all unread messages from users.

picture

There are two forms of message push: push ( push) and pull ( pull). Let’s learn about them one by one below.

 

short polling

Polling( polling) should be the simplest method to implement message push. Here we will divide polling into 短轮询sum 长轮询.

Short polling is easy to understand. At a specified time interval, the browser sends HTTPa request to the server, and the server returns unread message data to the client in real time, and the browser renders and displays it.

A simple JS timer can be used to request the unread message count interface once every second and display the returned data.

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

 The effect is still good. Although short polling is simple to implement, the shortcomings are also obvious. Since the push data does not change frequently, the client will make a request regardless of whether there are new messages generated in the backend at this time, which will inevitably cause a lot of trouble to the server. Great pressure, waste of bandwidth and server resources.

picture

 

long polling

Long polling is an improved version of the short polling above, which ensures the relative real-time nature of messages while minimizing the waste of server resources. Long polling is widely used in middleware, such as Nacosconfiguration apollocenter, message queue kafka, RocketMQetc. Long polling is used in middleware.

This time I used apollothe configuration center to implement long polling and applied a class DeferredResult, which is servelet3.0an asynchronous request mechanism provided by Spring encapsulation. The purpose is to delay the results.

picture

DeferredResultIt allows the container thread to quickly release the occupied resources without blocking the request thread, thereby accepting more requests to improve the system throughput, and then starts the asynchronous worker thread to process the real business logic, and completes the call to submit the response result DeferredResult.setResult(200).

Next we use long polling to implement message push.

Because an ID may be monitored by multiple long polling requests, I used the structure guavaprovided by the package Multimapto store the long polling. One key can correspond to multiple values. Once the key changes are detected, all corresponding long polls will respond. The front end gets the status code of non-request timeout, knows the data changes, actively queries the unread message count interface, and updates the page data.

@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";
    }

 When the request exceeds the set timeout, AsyncRequestTimeoutExceptionan exception will be thrown. Here, you can directly use @ControllerAdviceglobal capture to return uniformly. After the front-end obtains the agreed status code, it initiates a long polling request again, and so on.

@ControllerAdvice
public class AsyncRequestTimeoutHandler {

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

Let's test it. First, the page initiates a long polling request /polling/watch/10086to monitor message changes. The request is suspended and the data is not changed until timeout. Then the long polling request is initiated again. Then the data is changed manually, the long /polling/publish/10086polling is responded to, and the front-end processes it. After the business logic is completed, the request is initiated again, and the cycle repeats.

picture

Compared with short polling, long polling has improved performance a lot, but it still generates more requests, which is its imperfection.

iframe style

The iframe flow is to insert a hidden <iframe>tag into the page, and by srcrequesting the number of messages in the API interface, a long connection is created between the server and the client, and the server continues to iframetransmit data.

The transmitted data is usually HTML, or an embedded javascriptscript, to achieve the effect of updating the page in real time.

picture

This method is simple to implement, and only one <iframe>label is needed on the front end.

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

The server can directly assemble the html and js script data and responsewrite it to

@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>");
        }
    }
}

But I personally don’t recommend it, because it will show that the request has not been loaded on the browser, and the icon will keep rotating, which is simply a killer of obsessive-compulsive disorder.

picture

SSE (my way)

WebSocketMany people may not know that in addition to the familiar mechanism that can be used to push messages from the server to the client, there is also a server-sent event ( Server-sent events), referred to as SSE.

SSEIt is based on HTTPa protocol. We know that the HTTP protocol in a general sense cannot enable the server to actively push messages to the client, but SSE is an exception. It changes a way of thinking.

picture

SSE opens a one-way channel between the server and the client. The server responds no longer with one-time data packets but with text/event-streamtyped data flow information, which is streamed from the server to the client when there are data changes.

The overall implementation idea is somewhat similar to online video playback. The video stream will be continuously pushed to the browser. You can also understand that the client is completing a download that takes a long time (the network is not smooth).

picture

SSESimilar to WebSocketthe function, communication between the server and the browser can be established to push messages from the server to the client, but there are still some differences:

  • SSE is based on the HTTP protocol, and they do not require a special protocol or server implementation to work; a WebSocketseparate server is required to handle the protocol.

  • SSE one-way communication can only communicate one-way from the server to the client; webSocket full-duplex communication means that both parties in the communication can send and receive information at the same time.

  • SSE is simple to implement and has low development costs, and there is no need to introduce other components; WebSocket transmission data requires secondary analysis, and the development threshold is higher.

  • SSE supports disconnection and reconnection by default; WebSocket needs to be implemented by yourself.

  • SSE can only transmit text messages, and binary data needs to be encoded before transmission; WebSocket supports the transmission of binary data by default.

How to choose between SSE and WebSocket?

There is no good or bad technology, only which one is more suitable

SSE seems to have not been well-known by everyone, partly because of the emergence of WebSockets, which provides a richer protocol to perform two-way, full-duplex communication. For gaming, instant messaging, and scenarios that require near real-time updates in both directions, having a two-way channel is more attractive.

However, in some cases, sending data from the client is not required. And you just need some updates for server operations. For example: scenarios such as site messages, number of unread messages, status updates, stock quotes, monitoring quantity, etc. are SEEmore advantageous in terms of ease of implementation and cost. Additionally, SSE has WebSocketsseveral features that it lacks by design, such as: 自动重新连接the ability to , 事件IDand 发送任意事件.

The front end only needs to make an HTTP request, bring a unique ID, open the event stream, and listen to the events pushed by the server.

<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>

The implementation on the server side is simpler, create an SseEmitterobject and put it in sseEmitterMapfor management

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

/**
 * 创建连接
 *
 * @date: 2022/7/12 14:51
 */
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
 */
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);
        }
    }
}

We simulate the server pushing a message and see if the client receives the message, which is consistent with our expected effect.

picture

Note:  SSE does not support IEbrowsers, but its compatibility with other mainstream browsers is pretty good.

picture

MQTT

What is the MQTT protocol?

MQTT Full name (Message Queue Telemetry Transport): A communication protocol based on the publish/subscribe ( publish/ subscribe) model 轻量级, which obtains messages by subscribing to the corresponding topic. It is Internet of Thinga standard transmission protocol in the Internet of Things ( ).

This protocol separates message publishers ( publisher) and subscribers ( subscriber), so it can provide reliable message services for remotely connected devices in unreliable network environments. The usage method is somewhat similar to traditional MQ.

picture

TCPThe protocol is located at the transport layer, MQTT the protocol is located at the application layer, and MQTT the protocol is built TCP/IPon the protocol. That is to say, as long as TCP/IPthe protocol stack is supported, the protocol can be used MQTT.

Why use MQTT protocol?

MQTTWhy are protocols so preferred in the Internet of Things (IoT)? Instead of other protocols, such as  HTTPthe protocols we are more familiar with?

  • First of all, HTTPthe protocol is a synchronous protocol. The client needs to wait for the server's response after requesting. In the Internet of Things (IOT) environment, devices will be very affected by the environment, such as low bandwidth, high network latency, unstable network communication, etc. Obviously, asynchronous messaging protocols are more suitable for applications IOT.

  • HTTPIt is one-way, and if you want to get messages, the client must initiate a connection. In Internet of Things (IOT) applications, devices or sensors are often clients, which means that they cannot passively receive commands from the network.

  • Usually a command or message needs to be sent to all devices on the network. HTTPIt is not only difficult to implement such a function, but also extremely costly.

I won’t go into details about the specific introduction and practice of the MQTT protocol here. You can refer to my two previous articles, which are also very detailed.

Introduction to MQTT protocol

I didn’t expect that using springboot + rabbitmq to make a smart home would be so simple.

MQTT implements message push

Unread messages (little red dots), front-end and RabbitMQ real-time message push practice, extremely simple~

Websocket

websocketIt should be a method that everyone is familiar with to implement message push. We also compared it with websocket when talking about SSE above.

WebSocket is a TCPprotocol for full-duplex communication over a connection, establishing a communication channel between the client and server. The browser and server only need one handshake, and a persistent connection can be created directly between the two for bidirectional data transmission.

picture

Pictures from the Internet

Springboot integrates websocket and introduces websocketrelated toolkits first, which has additional development costs compared with SSE.

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

The server uses @ServerEndpointannotations to mark the current class as a websocket server, through which the client can ws://localhost:7777/webSocket/10086connect to the WebSocket server.

@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();
            }
        }
    }
}

The front-end initializes and opens the WebSocket connection, monitors the connection status, receives server data or sends data to the server.

<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>

The page is initialized to establish a websocket connection. After that, two-way communication can be carried out, and the effect is not bad.

picture

picture

Custom push

We have given me the principles and code implementations of 6 solutions above. However, in the actual business development process, you cannot blindly use them directly. You still have to choose the appropriate solution based on the characteristics of your own system business and actual scenarios.

The most direct way to push is to use a third-party push platform. After all, the needs that money can solve are not a problem  . There is no need for complicated development and operation and maintenance. It can be used directly, saving time, effort, and worry. Like goEasy and Jiguang Push are very good third-party service providers.

Generally, large companies have self-developed message push platforms. The web message we implemented this time is just a touch point on the platform. SMS, email, WeChat official account, and mini-programs are all channels that can reach users. Come in.

picture

Pictures come from the Internet

The internals of the message push system are quite complicated, such as the maintenance and review of message content, delineation of push groups, reach filtering and interception (push rule frequency, time period, quantity, black and white lists, keywords, etc.), and many modules for compensation for push failure. , there are many technically involving large data volumes and high concurrency scenarios. So our implementation today is just a small step in the face of this huge system.

Github address

I have implemented all the cases mentioned in the article one by one and put them in order Github. If you find it useful,  just star  it!

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

Guess you like

Origin blog.csdn.net/yyongsheng/article/details/132425983