Spring Boot 中使用 WebSocket 實現一對多聊天及一對一聊天


為什么需要WebSocket?

我們已經有了http協議,為什么還需要另外一個協議?有什么好處?

比如我想得到價格變化,只能是客戶端想服務端發起請求,服務器返回結果,HTTP協議做不到服務器主動向客戶端推送消息,

這種單向請求的特點,如果服務器有連續的狀態變化,客戶端要獲取指定只能輪詢,每隔一段時間,調一次接口,了解服務器有沒有新的價格信息

輪詢的效率低,且非常浪費資源,因此工程師們一直在思考,有沒有更好的方法。websocket就是這樣發明的。

是什么?

WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。

WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。

websocket協議特點

1 推送功能

支持服務器端向客戶端推送功能。服務器可以直接發送數據而不用等待客戶端的請求。

2 減少通信量

只要建立起websocket連接,就一直保持連接,在此期間可以源源不斷的傳送消息,直到關閉請求。也就避免了HTTP的非狀態性。
和http相比,不但每次連接時的總開銷減少了,而且websocket的首部信息量也小 ,通信量也減少了。

3 減少資源消耗

那么為什么他會解決服務器上消耗資源的問題呢?
其實我們所用的程序是要經過兩層代理的,即HTTP協議在Nginx等服務器的解析下,然后再傳送給相應的Handler(PHP等)來處理

簡單地說,我們有一個非常快速的接線員(Nginx),他負責把問題轉交給相應的客服(Handler)。本身接線員基本上速度是足夠的,但是每次都卡在客服(Handler)了,老有客服處理速度太慢。導致客服不夠。Websocket就解決了這樣一個難題,建立后,可以直接跟接線員建立持久連接,有信息的時候客服想辦法通知接線員,然后接線員在統一轉交給客戶。這樣就可以解決客服處理速度過慢的問題了。

概念說完了 開整

第一步 導包

      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>27.1-jre</version>
        </dependency>

其中guava包提供了一些集合Map的簡單創建等,可以不用導

第二步 配置類

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

第三步 控制層

@Slf4j
@Controller
public class SocketController {

    @RequestMapping("/websocket/{name}")
    public String webSocket(@PathVariable String name, Model model){
        try{
            log.info("跳轉到websocket的頁面上");
            model.addAttribute("username",name);
            return "index";
        }
        catch (Exception e){
            log.info("跳轉到websocket的頁面上發生異常,異常信息是:"+e.getMessage());
            return "error";
        }
    }



}

此處注意用的是@Controller而不是@RestController 

因為RestController多了一個注解 @ResponseBody  ,這注解個干啥我就不解釋了

第四步

@Slf4j
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {

    /**
     * 在線人數
     */
    public static volatile AtomicInteger onlineNumber = new AtomicInteger(0);

    /**
     * 以用戶的姓名為key,WebSocket為對象保存起來
     */
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<>();

    /**
     * 會話
     */
    private Session session;

    /**
     * 用戶名稱
     */
    private String username;

    /**
     * 建立連接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session)
    {
        System.out.println(this);
        onlineNumber.addAndGet(1);
        log.info("現在來連接的客戶id:"+session.getId()+"用戶名:"+username);
        this.username = username;
        this.session = session;
        log.info("有新連接加入! 當前在線人數" + onlineNumber);
        try {
            //messageType 1代表上線 2代表下線 3代表在線名單 4代表普通消息
            //先給所有人發送通知,說我上線了
            Map<String,Object> map1 = Maps.newHashMap();
            map1.put("messageType",1);
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);

            //把自己的信息加入到map當中去
            clients.put(username, this);
            //給自己發一條消息:告訴自己現在都有誰在線
            Map<String,Object> map2 = Maps.newHashMap();
            map2.put("messageType",3);
            //移除掉自己
            Set<String> set = clients.keySet();
            map2.put("onlineUsers",set);
            sendMessageTo(JSON.toJSONString(map2),username);
        }
        catch (IOException e){
            log.info(username+"上線的時候通知所有人發生了錯誤");
        }



    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.info("服務端發生了錯誤"+error.getMessage());
        //error.printStackTrace();
    }
    /**
     * 連接關閉
     */
    @OnClose
    public void onClose()
    {
        onlineNumber.addAndGet(-1);
        //webSockets.remove(this);
        clients.remove(username);
        try {
            //messageType 1代表上線 2代表下線 3代表在線名單  4代表普通消息
            Map<String,Object> map1 = Maps.newHashMap();
            map1.put("messageType",2);
            map1.put("onlineUsers",clients.keySet());
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);
        }
        catch (IOException e){
            log.info(username+"下線的時候通知所有人發生了錯誤");
        }
        log.info("有連接關閉! 當前在線人數" + onlineNumber);
    }

    /**
     * 收到客戶端的消息
     *
     * @param message 消息
     * @param session 會話
     */
    @OnMessage
    public void onMessage(String message, Session session)
    {
        try {
            log.info("來自客戶端消息:" + message+"客戶端的id是:"+session.getId());
            JSONObject jsonObject = JSON.parseObject(message);
            String textMessage = jsonObject.getString("message");
            String fromusername = jsonObject.getString("username");
            String tousername = jsonObject.getString("to");
            //messageType 1代表上線 2代表下線 3代表在線名單  4代表普通消息
            Map<String,Object> map1 = Maps.newHashMap();
            map1.put("messageType",4);
            map1.put("textMessage",textMessage);
            map1.put("fromusername",fromusername);
            if(tousername.equals("All")){
                map1.put("tousername","所有人");
                sendMessageAll(JSON.toJSONString(map1),fromusername);
            }
            else{
                map1.put("tousername",tousername);
                sendMessageTo(JSON.toJSONString(map1),tousername);
            }
        }
        catch (Exception e){
            log.info("發生了錯誤了");
        }

    }

    public void sendMessageTo(String message, String ToUserName) throws IOException {
        WebSocket webSocket = clients.get(ToUserName);
        webSocket.session.getBasicRemote().sendText(message);
//        for (WebSocket item : clients.values()) {
//            if (item.username.equals(ToUserName) ) {
//                item.session.getAsyncRemote().sendText(message);
//                break;
//            }
//        }
    }

    public void sendMessageAll(String message,String FromUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            item.session.getAsyncRemote().sendText(message);
        }
    }

    public static synchronized AtomicInteger getOnlineCount() {
        return onlineNumber;
    }

}

看完代碼有沒有發現我在map中放的是this 為什么?

因為@ServerEndpoint不是單例模式

onlineNumber這個是線程安全的計數類 如果想深入了解就看看CAS吧

5 頁面目錄

放置resources/remplates下 

6 頁面代碼

<!DOCTYPE html>
<html>
<head>
    <title>websocket</title>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
    <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
</head>

<body>
<div style="margin: auto;text-align: center">
    <h1>Welcome to websocket</h1>
</div>
<br/>
<div style="margin: auto;text-align: center">
    <select id="onLineUser">
        <option>--所有--</option>
    </select>
    <input id="text" type="text" />
    <button onclick="send()">發送消息</button>
</div>
<br>
<div style="margin-right: 10px;text-align: right">
    <button onclick="closeWebSocket()">關閉連接</button>
</div>
<hr/>
<div id="message" style="text-align: center;"></div>
<input  type="text" th:value="${username}" id="username" style="display: none" />
</body>


<script type="text/javascript">
    var webSocket;
    var commWebSocket;
    if ("WebSocket" in window)
    {
        webSocket = new WebSocket("ws://localhost:8080/websocket/"+document.getElementById('username').value);

        //連通之后的回調事件
        webSocket.onopen = function()
        {
            //webSocket.send( document.getElementById('username').value+"已經上線了");
            console.log("已經連通了websocket");
            setMessageInnerHTML("已經連通了websocket");
        };

        //接收后台服務端的消息
        webSocket.onmessage = function (evt)
        {
            var received_msg = evt.data;
            console.log("數據已接收:" +received_msg);
            var obj = JSON.parse(received_msg);
            console.log("可以解析成json:"+obj.messageType);
            //1代表上線 2代表下線 3代表在線名單 4代表普通消息
            if(obj.messageType==1){
                //把名稱放入到selection當中供選擇
                var onlineName = obj.username;
                var option = "<option>"+onlineName+"</option>";
                $("#onLineUser").append(option);
                setMessageInnerHTML(onlineName+"上線了");
            }
            else if(obj.messageType==2){
                $("#onLineUser").empty();
                var onlineName = obj.onlineUsers;
                var offlineName = obj.username;
                var option = "<option>"+"--所有--"+"</option>";
                for(var i=0;i<onlineName.length;i++){
                    if(!(onlineName[i]==document.getElementById('username').value)){
                        option+="<option>"+onlineName[i]+"</option>"
                    }
                }
                $("#onLineUser").append(option);

                setMessageInnerHTML(offlineName+"下線了");
            }
            else if(obj.messageType==3){
                var onlineName = obj.onlineUsers;
                var option = null;
                for(var i=0;i<onlineName.length;i++){
                    if(!(onlineName[i]==document.getElementById('username').value)){
                        option+="<option>"+onlineName[i]+"</option>"
                    }
                }
                $("#onLineUser").append(option);
                console.log("獲取了在線的名單"+onlineName.toString());
            }
            else{
                setMessageInnerHTML(obj.fromusername+""+obj.tousername+"說:"+obj.textMessage);
            }
        };

        //連接關閉的回調事件
        webSocket.onclose = function()
        {
            console.log("連接已關閉...");
            setMessageInnerHTML("連接已經關閉....");
        };
    }
    else{
        // 瀏覽器不支持 WebSocket
        alert("您的瀏覽器不支持 WebSocket!");
    }
    //將消息顯示在網頁上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    function closeWebSocket() {
        //直接關閉websocket的連接
        webSocket.close();
    }

    function send() {
        var selectText = $("#onLineUser").find("option:selected").text();
        if(selectText=="--所有--"){
            selectText = "All";
        }
        else{
            setMessageInnerHTML(document.getElementById('username').value+""+selectText+"說:"+ $("#text").val());
        }
        var message = {
            "message":document.getElementById('text').value,
            "username":document.getElementById('username').value,
            "to":selectText
        };
        webSocket.send(JSON.stringify(message));
        $("#text").val("");

    }
</script>

</html>

 總結一下

此處注意用的是@Controller而不是@RestController 

因為RestController多了一個注解 @ResponseBody

看完代碼有沒有發現我在map中放的是this 為什么?

因為@ServerEndpoint不是單例模式

onlineNumber這個是線程安全的計數類 如果想深入了解就看看CAS吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM