SpringBoot坚持学习第四天:集成WebSocket

一、WebSocket第一次使用

        首先要掌握的是webSocket的4个事件。

open  eventSokcket @OnOpen连接建立时触发
message eventSokcket @OnMessage客户端接收服务端数据时触发
error eventSokcket @OnError通讯发生错误时触发
close eventSokcket @OnClose链接关闭时触发

先把结论说在最前面:

        (1)前端JS里面 在new WebSocket(url)的时候,与后台某个@ServerEndpoint注解类建立连接。此类触发@OnOpen注解方法。方法上可以编写一个javax.websocket.Session作为接收参数,是有值的,必须将其存放到一个全局对象中。我跟踪代码后发现,每次建立连接都创建@ServerEndpoint注解类的一个对象,所以session放到全局对象中就很有必要。

        (2)前端向后台发送消息。 在JS中,WebSocket对象调用 ws.send("message");  在后台代码中,@OnMessage注解方法就会执行。一般情况下,前端期望得到后台的响应。后台可以使用javax.websocket.Session的相关方法,向所有其它用户(session)发送消息。如果对某个session发送消息,可视为"私聊"。原来私聊的实现就是这么简单。

        (3)前端在接收到后台的响应信息的时候,将会触发message事件。一般在message 事件中,将后台发送的消息呈现在大屏幕上。

        (4)断开连接。前端JS在执行ws.close()方法的时候,将会让后台@OnClose注解方法执行。此方法能够获取到javax.websocket.Session,从而将其从全局变量中移除,并调用此session对象的close()方法。

        在JavaScript中使用WebSocket

var ws = new WebSocket("ws://localhost:8080/url");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  //前端向后台发送消息
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  //前端接收后台消息
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};   

二、页面学习

        今天学习了如何使用WebSocket。同时也学习了如何使用Bootstrap搭建较为漂亮的页面。

        原来,在页面中使用bootstrap非常的简单,只需要引入JQuery.js与bootstrap.css就行了。

        以后我将尽量把页面弄的漂亮一点,原因是知识只有越用越活。

        原来,body是用container修饰的,form表单使用 form-group修饰的,input标签使用 form - control 修饰的,按钮是可以带颜色的btn btn-success。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>聊天室</title>
    <link rel="stylesheet" href="bootstrap.min.css">
    <script src="jquery-3.2.1.min.js"></script>
</head>
<body class="container" style="width: 60%">
<div class="form-group"></br>
    <h5>聊天室</h5>
    <textarea id="message_content" class="form-control" readonly cols="50" rows="10"></textarea>
</div>

<div class="form-group">
    <label for="user_name">你的名字</label>
    <input class="form-control" id="user_name"><br>
    <button id="btn_join" class="btn btn-success">加入聊天室</button>
    <button id="btn_left" class="btn btn-warning">离开聊天室</button>
</div>

<!-- 表单样式:form-group -->
<!-- 普通组件样式:form-contorl -->
<!-- 按钮颜色 btn btn-success -->
<div class="form-group">
    <label for="user_message">发送消息</label>
    <input class="form-control" id="user_message">
    <button id="send_all_message" class="btn btn-info">发送消息</button>
</div>
</body>

<script>
    //页面一加载就执行的JS
    $(document).ready(
        function () {
            //需要以ws开头,后台Controller代码需要监听此请求
            var urlPrefix = 'ws://localhost:8080/chatroom/';
            //定义全局变量ws
            var ws = null;
            $('#btn_join').click(function () {
                var username = $('#user_name').val();
                if (username == '') {
                    alert("请输入用户名!");
                    return;
                }
                //拼接URL,Controller中监听此请求  @ServerEndpoint("/chat-room/{username}")
                var url = urlPrefix + username;
                //new 一个url的时候,就会向后台发送一个URL请求
                //会被@OnOpen注解捕获到
                ws = new WebSocket(url);
                //为此对象配置全局监听器
                ws.onopen = function () {
                    console.log("建立连接");
                };

                //核心:当后台发送给前端消息的时候,触发此事件
                ws.onmessage = function (event) {
                    debugger;
                    $("#message_content").append(event.data + '\n')
                }

                //当ws对象关闭的时候触发
                ws.onclose = function () {
                    $('#message_content').append('用户[' + username + '] 已经离开聊天室!');
                    console.log("关闭 websocket 连接...");
                }
            });

            $("#send_all_message").click(function () {
                var msg = $('#user_message').val();
                if(msg==''){
                    alert("请填写消息!");
                    return;
                }
                //获取全局变量WebSocket
                if(ws && msg!=''){
                    //核心:使用ws向后台发送消息,触发后台的@OnMessage注解代码
                    ws.send(msg);
                }
                
            })


            //退出聊天室
            $('#btn_left').click(function(){
                if(ws){
                    debugger;
                    ws.close();
                }
            });


        }
    )
</script>
</html>

 

三、如何在SpringBoot中使用WebSocket

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

         (1)在启动类上新增一个@EnableWebSocket注解,并配置一个@Bean。后来我发现这个@Bean正是配置在其它Controller上的类似注解。

@SpringBootApplication  
@EnableWebSocket  //启用WebSocket
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean   //监听WebSocket的Controller类上的注解与这个Bean长得很像
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

        我在跟踪代码后发现,每次生成的webSocket后台对象都是一个独立的,所以我之前尝试将存放Map<username,session>的map设置为使用@ServerEndpoint("/chatroom/{username}") 注解的类的私有变量,发现每次这个map打断点进来都是size=0。最终解决办法就是创建一个全局类,使用static修饰。

package com.websocket.demo;

import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author jay.zhou
 * @date 2019/1/5
 * @time 10:27
 */
public class WebSocketUtils {
    /**
     * 使用ConcurrentHashMap可以提高并发效率,
     * https://blog.csdn.net/yanluandai1985/article/details/83051643
     */
    public static Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();

    private WebSocketUtils() {
    }

    public static void sendMessageAll(String message) {
        //这里提供了一种快速遍历Map集合的代码,值得学习
        ONLINE_USER_SESSIONS.forEach((username, session) -> sendMessage(session, message));
    }

    /**
     * 核心:后端服务器向前端发送数据
     *
     * @param session 用户的session
     * @param message 发送的消息
     */
    public static void sendMessage(Session session, String message) {
        if (session == null) {
            return;
        }
        //核心:通过session向前端页面发送数据,前端将会触发message事件
        final RemoteEndpoint.Basic basic = session.getBasicRemote();
        try {
            //核心:后台发送数据到前台,触发前台的Message事件
            basic.sendText(message);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

        下面就是核心的类了,每次创建一个前端的websocket对象,后台也会创建一个使用@ServerEndpoint注解类的对象,前后端websocket对象一一对应。后台@ServerEndpoint注解类的4个注解方法需要掌握。

        本例也是第一次在实例中使用并发包下的ConcurrentHashMap,终于把储备的知识用起来了。同时也掌握了快速遍历Map集合的方法,前几天一直困惑遍历Map集合很麻烦该怎么解决,这次终于见到大牛的代码,很佩服。

package com.websocket.demo.web;

import org.springframework.web.bind.annotation.RestController;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

import static com.websocket.demo.WebSocketUtils.ONLINE_USER_SESSIONS;
import static com.websocket.demo.WebSocketUtils.sendMessageAll;

/**
 * @author jay.zhou
 * @date 2019/1/4
 * @time 19:06
 */
@RestController
@ServerEndpoint("/chatroom/{username}")
public class WsController {

    /**
     * 核心:javax.websocket.Session通过WEB环境自动注入
     * 注意注解:@PathParam 不是@PathVariable
     *
     * @param username 从URL路径中解析的数据
     * @param session  由WEB环境生成的websocket的session
     */
    @OnOpen
    public void openSession(@PathParam("username") String username, Session session) {
        //存入map
        ONLINE_USER_SESSIONS.put(username, session);
        String message = "欢迎用户[" + username + "] 来到聊天室!";
        sendMessageAll(message);
    }

    /**
     * 核心:捕获前端ws发送给后台服务器的数据
     * 前端:var ws = new WebSocket(url); ws.send(message);
     *
     * @param username 从URL路径中解析的数据
     * @param message  捕获前台的消息
     */
    @OnMessage
    public void onMessage(@PathParam("username") String username, String message) {
        //basic由session生成,为每个session都发消息
        //basic.sendText(message);
        sendMessageAll("用户[" + username + "] : " + message);
    }

    @OnClose
    public void onClose(@PathParam("username") String username, Session session) {
        //当前的Session 移除
        ONLINE_USER_SESSIONS.remove(username);
        //通知所有的session
        sendMessageAll("用户[" + username + "] 已经离开聊天室了!");
        //还需要关闭session
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
        }
    }


}

posted @ 2022-07-17 12:15  小大宇  阅读(268)  评论(0编辑  收藏  举报