服務(wù)端主動通知Web前端的一些探索--Spring boot集成WebSocket

知識背景

隨著物聯(lián)網(wǎng)的發(fā)展促進(jìn)傳統(tǒng)行業(yè)不斷轉(zhuǎn)型,在設(shè)備間通信的業(yè)務(wù)場景越來越多。其中很大一部分在于移動端和設(shè)備或服務(wù)端與設(shè)備的通信,例如已成主流的共享單車。但存在一個這樣小問題,當(dāng)指令下發(fā)完畢之后,設(shè)備不會同步返回指令執(zhí)行是否成功,而是異步通知或是服務(wù)端去主動查詢設(shè)備指令是否發(fā)送成功,這樣一來客戶端(前端)也無法同步獲取指令執(zhí)行情況,只能通過服務(wù)端異步通知來接收該狀態(tài)了。這也就引出了這篇博客想要探索的一項(xiàng)技術(shù):如何實(shí)現(xiàn)服務(wù)端主動通知前端? 其實(shí),這樣的業(yè)務(wù)場景還有很多,但這樣的解決方案卻不是非常成熟,方案概括過來就兩個大類。1.前端定時請求輪詢 2.前端和服務(wù)端保持長連接,以持續(xù)進(jìn)行數(shù)據(jù)交互,這個可以包括較為成熟的WebSocket。我們可以看看張小龍在知乎問題 如何在大型 Web 應(yīng)用中保持?jǐn)?shù)據(jù)的同步更新? 的回答,更加清楚的認(rèn)識這個過程。

這個問題在10年前已經(jīng)被解決過無數(shù)次了,最簡單的例子就是網(wǎng)頁聊天室。題主的需求稍微復(fù)雜些,需要支持的數(shù)據(jù)格式更多,然而只要定義好了通訊規(guī)范,多出來的也只是搬磚的活兒了。
整個過程可以分為5個環(huán)節(jié):1 包裝數(shù)據(jù)、2 觸發(fā)通知、3 通訊傳輸、4 解析數(shù)據(jù)、5 渲染數(shù)據(jù)。這5個環(huán)節(jié)中有三點(diǎn)很關(guān)鍵:1 通訊通道選擇、2 數(shù)據(jù)格式定義、3 渲染數(shù)據(jù)。

1 通訊通道選擇:這個很多前端高手已經(jīng)回答了,基本就是兩種方式:輪詢和長連接,這種情況通常的解決方式是長連接,Web端可以用WebSocket來解決,這也是業(yè)界普遍采用的方案,比如環(huán)信、用友有信、融云等等。通訊環(huán)節(jié)是相當(dāng)耗費(fèi)服務(wù)器資源的一個環(huán)節(jié),而且開發(fā)成本偏高,建議將這些第三方的平臺直接集成到自己的項(xiàng)目中,以降低開發(fā)的成本。

2 數(shù)據(jù)格式定義:數(shù)據(jù)格式可以定義得五花八門,不過為了前端的解析,建議外層統(tǒng)一數(shù)據(jù)格式,定義一個類似type的屬性來標(biāo)記數(shù)據(jù)屬性(是IM消息、微博數(shù)據(jù)還是發(fā)貨通知),然后定義一個data屬性來記錄數(shù)據(jù)的內(nèi)容(一般對應(yīng)數(shù)據(jù)表中的一行數(shù)據(jù))。統(tǒng)一數(shù)據(jù)格式后,前端解析數(shù)據(jù)的成本會大大降低。

3 渲染數(shù)據(jù)渲染數(shù)據(jù)是關(guān)系到前端架構(gòu)的,比如是React、Vue還是Angular(BTW:不要用Angular,個人認(rèn)為Angular在走向滅亡)。這些框架都用到了數(shù)據(jù)綁定,這已經(jīng)成為業(yè)界的共識了(只需要對數(shù)據(jù)進(jìn)行操作,不需要操作DOM),這點(diǎn)不再論述。在此種需求場景下,數(shù)據(jù)流會是一個比較大的問題,因?yàn)榭赡苊恳粭l新數(shù)據(jù)都需要尋找對應(yīng)的組件去傳遞數(shù)據(jù),這個過程會特別惡心。所以選擇單一樹的數(shù)據(jù)流應(yīng)該會很合適,這樣只需要對一棵樹的節(jié)點(diǎn)進(jìn)行操作即可:定義好type和樹節(jié)點(diǎn)的對應(yīng)關(guān)系,然后直接定位到對應(yīng)的節(jié)點(diǎn)對數(shù)據(jù)增刪改就可以,例如Redux。

以上三點(diǎn)是最核心的環(huán)節(jié),涉及到前后端的數(shù)據(jù)傳輸、前端數(shù)據(jù)渲染,其他的內(nèi)容就比較簡單了,也簡單說下。

后端:包裝數(shù)據(jù)、觸發(fā)通知這個對后端來說就很Easy了,建一個隊(duì)列池,不斷的往池子里丟任務(wù),讓池子去觸發(fā)通知。

前端:解析數(shù)據(jù)解析數(shù)據(jù)就是多出來的搬磚的活兒,過濾type、取data。技術(shù)難度并不大,主要點(diǎn)還是在于如何能低開發(fā)成本、低維護(hù)成本地達(dá)到目的,上面是一種比較綜合的低成本的解決方案。

對于對實(shí)時性要求較高的業(yè)務(wù)場景,輪詢顯然是無法滿足需求的,而長連接的缺點(diǎn)在于長期占了服務(wù)端的連接資源,當(dāng)前端用戶數(shù)量指數(shù)增長到一定數(shù)量時,服務(wù)端的分布式須另辟蹊徑來處理WebSocket的連接匹配問題。它的優(yōu)點(diǎn)也很明顯,對于傳輸內(nèi)容不大的情況下,有非??斓慕换ニ俣?,因?yàn)樗皇腔?code>HTTP請求的,而是瀏覽器端擴(kuò)展的Socket通信。

Spring boot接入WebSocket

Maven Dependencies

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

Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        // 添加服務(wù)端點(diǎn),可以理解為某一服務(wù)的唯一key值
        stompEndpointRegistry.addEndpoint("/chatApp");
        //當(dāng)瀏覽器支持sockjs時執(zhí)行該配置
        stompEndpointRegistry.addEndpoint("/chatApp").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 配置接受訂閱消息地址前綴為topic的消息
        config.enableSimpleBroker("/topic");
        // Broker接收消息地址前綴
        config.setApplicationDestinationPrefixes("/app");
    }
}

MessageMapping

    @Autowired
    private SimpMessagingTemplate template;

    //接收客戶端"/app/chat"的消息,并發(fā)送給所有訂閱了"/topic/messages"的用戶
    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public OutputMessage receiveAndSend(InputMessage inputMessage) throws Exception {
        System.out.println("get message (" + inputMessage.getText() + ") from client!");
        System.out.println("send messages to all subscribers!");
        String time = new SimpleDateFormat("HH:mm").format(new Date());
        return new OutputMessage(inputMessage.getFrom(), inputMessage.getText(), time);
    }

    //或者直接從服務(wù)端發(fā)送消息給指定客戶端
    @MessageMapping("/chat_user")
    public void sendToSpecifiedUser(@Payload InputMessage inputMessage, SimpMessageHeaderAccessor headerAccessor) throws Exception {
        System.out.println("get message from client (" + inputMessage.getFrom() + ")");
        System.out.println("send messages to the specified subscriber!");
        String time = new SimpleDateFormat("HH:mm").format(new Date());
        this.template.convertAndSend("/topic/" + inputMessage.getFrom(), new OutputMessage(inputMessage.getFrom(), inputMessage.getText(), time));
    }

clients

<!DOCTYPE html>
<!DOCTYPE html>
<html>

    <head>
        <title>Chat WebSocket</title>
        <script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
        <script src="js/stomp.js"></script>
        <script type="text/javascript">
            var apiUrlPre = "http://10.200.0.126:9041/discovery";
            var stompClient = null;

            function setConnected(connected) {
                document.getElementById('connect').disabled = connected;
                document.getElementById('disconnect').disabled = !connected;
                document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
                document.getElementById('response').innerHTML = '';
            }

            function connect() {
                var socket = new SockJS('http://localhost:9041/discovery/chatApp');
        var from = document.getElementById('from').value;
                stompClient = Stomp.over(socket);
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    console.log('Connected: ' + frame);
          //stompClient.subscribe('/topic/' + from, function(messageOutput) {
                    stompClient.subscribe('/topic/messages', function(messageOutput) {
                        //                      alert(messageOutput.body);
                        showMessageOutput(JSON.parse(messageOutput.body));
                    });
                });
            }

            function disconnect() {
                if(stompClient != null) {
                    stompClient.disconnect();
                }
                setConnected(false);
                console.log("Disconnected");
            }

            function sendMessage() {
                var from = document.getElementById('from').value;
                var text = document.getElementById('text').value;
                //stompClient.send("/app/chat_user", {},
                stompClient.send("/app/chat", {},
                    JSON.stringify({
                        'from': from,
                        'text': text
                    })
                );
            }

            function showMessageOutput(messageOutput) {
                var response = document.getElementById('response');
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';
                p.appendChild(document.createTextNode(messageOutput.from + ": " +
                    messageOutput.text + " (" + messageOutput.time + ")"));
                response.appendChild(p);
            }
        </script>
    </head>

    <body onload="disconnect()">
        <div>
            <div>
                <input type="text" id="from" placeholder="Choose a nickname" />
            </div>
            <br />
            <div>
                <button id="connect" onclick="connect();">Connect</button>
                <button id="disconnect" disabled="disabled" onclick="disconnect();">
                    Disconnect
                </button>
            </div>
            <br />
            <div id="conversationDiv">
                <input type="text" id="text" placeholder="Write a message..." />
                <button id="sendMessage" onclick="sendMessage();">Send</button>
                <p id="response"></p>
            </div>
        </div>

    </body>

</html>

結(jié)果

send to all subscribers
send to the specified subscriber

總結(jié)

這是spring-boot接入WebSocket最簡單的方法了,很直觀的表現(xiàn)了socket在瀏覽器段通信的便利,但根據(jù)不同的業(yè)務(wù)場景,對該技術(shù)的使用還需要斟酌,例如如何使WebSocket在分布式服務(wù)端保持服務(wù),如何在連接上集群后下發(fā)消息找到長連接的服務(wù)端機(jī)器。我也在為這個問題苦苦思考,思路雖有,實(shí)踐起來卻舉步維艱,特別是網(wǎng)上談到比較多的將連接序列化到緩存中,統(tǒng)一管理讀取分配,分享幾個好思路,也希望自己能給找到較好的方案再分享一篇博客。
來自Push notifications with websockets in a distributed Node.js app

  1. Configure Nginx to send websocket requests from each browser to all the server in the cluster. I could not figure out how to do it. Load balancing does not support broadcasting.
  2. Store websocket connections in the databse, so that all servers had access to it. I am not sure how to serialize the websocket connection object to store it in MongoDB.
  3. Set up a communication mechanism among the servers in the cluster (some kind message bus) and whenever event happens, have all the servers notify the websocket clients they are tracking. This somewhat complicates the system and requires the nodes to know the addresses of each other. Which package is most suitable for such a solution?
    再分享幾個討論:
    springsession如何對spring的WebSocketSession進(jìn)行分布式配置?
    websocket多臺服務(wù)器之間怎么共享websocketSession?

參考

WebSocket Support

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,578評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評論 25 709
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,275評論 6 342
  • 我真想最后死的時候慘烈一點(diǎn),最好皮開肉綻倒在你懷里,好讓你看看我混沌的瞳子里到底映著誰的模樣,血肉模糊的心臟里又刺...
    抖抖不是斗斗閱讀 248評論 0 0

友情鏈接更多精彩內(nèi)容