spring-boot整合websocket

WebSocket簡介

??隨著互聯(lián)網(wǎng)的發(fā)展,傳統(tǒng)的HTTP協(xié)議已經(jīng)很難滿足Web應(yīng)用日益復(fù)雜的需求了。近年來,隨著HTML5的誕生,WebSocket協(xié)議被提出,它實現(xiàn)了瀏覽器與服務(wù)器的全雙工通信,擴展了瀏覽器與服務(wù)端的通信功能,使服務(wù)端也能主動向客戶端發(fā)送數(shù)據(jù)。

??我們知道,傳統(tǒng)的HTTP協(xié)議是無狀態(tài)的,每次請求(request)都要由客戶端(如 瀏覽器)主動發(fā)起,服務(wù)端進行處理后返回response結(jié)果,而服務(wù)端很難主動向客戶端發(fā)送數(shù)據(jù);這種客戶端是主動方,服務(wù)端是被動方的傳統(tǒng)Web模式。對于信息變化不頻繁的Web應(yīng)用來說造成的麻煩較小,而對于涉及實時信息的Web應(yīng)用卻帶來了很大的不便,如帶有即時通信、實時數(shù)據(jù)、訂閱推送等功能的應(yīng)用。在WebSocket規(guī)范提出之前,開發(fā)人員若要實現(xiàn)這些實時性較強的功能,經(jīng)常會使用折衷的解決方法:輪詢(polling)和Comet技術(shù)。其實后者本質(zhì)上也是一種輪詢,只不過有所改進。

??輪詢是最原始的實現(xiàn)實時Web應(yīng)用的解決方案。輪詢技術(shù)要求客戶端以設(shè)定的時間間隔周期性地向服務(wù)端發(fā)送請求,頻繁地查詢是否有新的數(shù)據(jù)改動。明顯地,這種方法會導(dǎo)致過多不必要的請求,浪費流量和服務(wù)器資源。

??這兩種技術(shù)都是基于請求-應(yīng)答模式,都不算是真正意義上的實時技術(shù);它們的每一次請求、應(yīng)答,都浪費了一定流量在相同的頭部信息上,并且開發(fā)復(fù)雜度也較大。

??伴隨著HTML5推出的WebSocket,真正實現(xiàn)了Web的實時通信,使B/S模式具備了C/S模式的實時通信能力。WebSocket的工作流程是這 樣的:瀏覽器通過JavaScript向服務(wù)端發(fā)出建立WebSocket連接的請求,在WebSocket連接建立成功后,客戶端和服務(wù)端就可以通過 TCP連接傳輸數(shù)據(jù)。因為WebSocket連接本質(zhì)上是TCP連接,不需要每次傳輸都帶上重復(fù)的頭部數(shù)據(jù),所以它的數(shù)據(jù)傳輸量比輪詢和Comet技術(shù)小 了很多。本文不詳細地介紹WebSocket規(guī)范,主要介紹下WebSocket在Java Web中的實現(xiàn)。

??本文主要介紹了websocket在spring-boot上的搭建原理,已經(jīng)部分demo的參考。

引入websocket依賴

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

配置WebSocketConfig

/*
 * File: WebSocketConfig.java
 * Created By: fengtao.xue@gausscode.com
 * Date: 2019-03-20
 */

package cn.feng.dev.websocket.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;


/**
 * @author fengtao.xue
 */


@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocketServer

/*
 * File: WebSocketServer.java
 * Created By: fengtao.xue@gausscode.com
 * Date: 2019-03-21
 */

package cn.gausscode.calo.frontend.rest.websocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務(wù)器端,
 * 注解的值將被用于監(jiān)聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務(wù)器端
 * @author fengtao.xue
 */
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
    static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
    //靜態(tài)變量,用來記錄當(dāng)前在線連接數(shù)。應(yīng)該把它設(shè)計成線程安全的。
    private static final AtomicInteger OnlineCount = new AtomicInteger(0);
    //concurrent包的線程安全Set,用來存放每個客戶端對應(yīng)的MyWebSocket對象。若要實現(xiàn)服務(wù)端與單一客戶端通信的話,可以使用Map來存放,其中Key可以為用戶標(biāo)識
    private static ConcurrentHashMap<String, WebSocketServer> webSocketSet = new ConcurrentHashMap<String,WebSocketServer>();
    //與某個客戶端的連接會話,需要通過它來給客戶端發(fā)送數(shù)據(jù)
    private Session WebSocketsession;
    //當(dāng)前發(fā)消息的人員userId
    private String userId = "";

    /**
     * 連接建立成功調(diào)用的方法*/
    @OnOpen
    public void onOpen(@PathParam(value = "userId") String param, Session WebSocketsession, EndpointConfig config) {
        userId = param;
        //log.info("authKey:{}",authKey);
        this.WebSocketsession = WebSocketsession;
        webSocketSet.put(param, this);//加入map中
        int cnt = OnlineCount.incrementAndGet(); // 在線數(shù)加1
        logger.info("有連接加入,當(dāng)前連接數(shù)為:{}", cnt);
        sendMessage(this.WebSocketsession, "連接成功");
    }

    /**
     * 連接關(guān)閉調(diào)用的方法
     */
    @OnClose
    public void onClose() {
        if (!userId.equals("")){
            webSocketSet.remove(userId);//從set中刪除
            int cnt = OnlineCount.decrementAndGet();
            logger.info("有連接關(guān)閉,當(dāng)前連接數(shù)為:{}", cnt);
        }
    }

    /**
     * 收到客戶端消息后調(diào)用的方法
     *
     * @param message 客戶端發(fā)送過來的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info("來自客戶端的消息:{}",message);
        sendMessage(session, "收到消息,消息內(nèi)容:"+message);
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("發(fā)生錯誤:{},Session ID: {}",error.getMessage(),session.getId());
        error.printStackTrace();
    }

    /**
     * 發(fā)送消息,實踐表明,每次瀏覽器刷新,session會發(fā)生變化。
     * @param message
     */
    public void sendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
            //session.getBasicRemote().sendText(String.format("%s",message));
        } catch (IOException e) {
            logger.error("發(fā)送消息出錯:{}", e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 群發(fā)消息
     * @param message
     * @throws IOException
     */
    public void broadCastInfo(String message) {
        for (String key : webSocketSet.keySet()) {
            Session session = webSocketSet.get(key).WebSocketsession;
            if(session != null && session.isOpen() && !userId.equals(key)){
                sendMessage(session, message);
            }
        }
    }

    /**
     * 指定Session發(fā)送消息
     * @param message
     * @throws IOException
     */
    public void sendToUser(String userId, String message) {
        WebSocketServer webSocketServer = webSocketSet.get(userId);
        if ( webSocketServer != null && webSocketServer.WebSocketsession.isOpen()){
            sendMessage(webSocketServer.WebSocketsession, message);
        }
        else{
            logger.warn("當(dāng)前用戶不在線:{}",userId);
        }
    }
}

Controller

/*
 * File: Controller.java
 * Created By: fengtao.xue@gausscode.com
 * Date: 2019-03-20
 */

package cn.feng.dev.websocket.web;

import cn.feng.dev.websocket.service.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;


/**
 * @author fengtao.xue
 */
@RestController
public class Controller {
    static Logger logger = LoggerFactory.getLogger(Controller.class);

    @Autowired
    WebSocketServer webSocketServer;

    /**
     * 群發(fā)消息內(nèi)容
     *
     * @param message
     * @return
     */
    @RequestMapping(value = "/ws/sendAll", method = RequestMethod.GET)
    public String sendAllMessage(@RequestParam(required = true) String message) {
        try {
            webSocketServer.broadCastInfo(message);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "success";
    }

    /**
     * 指定會話ID發(fā)消息
     *
     * @param message 消息內(nèi)容
     * @param userId      連接會話ID
     * @return
     */
    @RequestMapping(value = "/ws/sendOne", method = RequestMethod.GET)
    public String sendOneMessage(@RequestParam(required = true) String message,
                                 @RequestParam(required = true) String userId) {
        try {
            webSocketServer.sendToUser(userId, message);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "success";
    }
}

index.html

<!DOCTYPE html>
<!--
功能:WebSocket使用示例
 -->
<html>
<head>
    <!--<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="/main.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>-->
    <meta charset="UTF-8">
    <title>websocket測試</title>
    <style type="text/css">
        h3,h4{
            text-align:center;
        }
    </style>
</head>
<body>

<h3>WebSocket測試,在<span style="color:red">控制臺</span>查看測試信息輸出!</h3>
<h4>http://okcdev.cn</h4>
<h4>
    [url=/ws/sendOne?message=單發(fā)消息內(nèi)容&id=none]單發(fā)消息鏈接[/url]
    [url=/ws/sendAll?message=群發(fā)消息內(nèi)容]群發(fā)消息鏈接[/url]
</h4>
<div class="row">
    <div class="col-md-12">
        <table id="conversation" class="table table-striped">
            <thead>
            <tr>
                <th>Greetings</th>
            </tr>
            </thead>
            <tbody id="greetings">
            </tbody>
        </table>
    </div>
</div>


<script type="text/javascript">

    function setMessageInnerHTML(sendMessage) {
        document.getElementById('greetings').innerHTML += sendMessage + '<br/>';
    }

    var socket;
    var userId = 11111111111;
    if (typeof (WebSocket) == "undefined") {
        console.log("遺憾:您的瀏覽器不支持WebSocket");
    } else {
        console.log("恭喜:您的瀏覽器支持WebSocket");

        //實現(xiàn)化WebSocket對象
        //指定要連接的服務(wù)器地址與端口建立連接
        //注意ws、wss使用不同的端口。我使用自簽名的證書測試,
        //無法使用wss,瀏覽器打開WebSocket時報錯
        //ws對應(yīng)http、wss對應(yīng)https。
        socket = new WebSocket("ws://localhost:8080/websocket/" + userId);
        //連接打開事件
        socket.onopen = function() {
            console.log("Socket 已打開");
            //socket.send("消息發(fā)送測試(From Client)");
        };
        //收到消息事件
        socket.onmessage = function(msg) {
            console.log(msg.data);
            setMessageInnerHTML(msg.data);
            //$("#greetings").append("<tr><td>" + msg.data + "</td></tr>");
        };
        //連接關(guān)閉事件
        socket.onclose = function() {
            console.log("Socket已關(guān)閉");
        };
        //發(fā)生了錯誤事件
        socket.onerror = function() {
            alert("Socket發(fā)生了錯誤");
        }

        //監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時,主動去關(guān)閉websocket連接,防止連接還沒斷開就關(guān)閉窗口,server端會拋異常。
        window.onbeforeunload = function () {
            closeWebSocket();
        }

        //關(guān)閉WebSocket連接
        function closeWebSocket() {
            socket.close();
        }

        //窗口關(guān)閉時,關(guān)閉連接
        /*window.unload=function() {
            socket.close();
        };*/
    }
</script>

</body>
</html>

工程結(jié)構(gòu)路徑

image.png

運行效果

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

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