第16章 Spring Boot整合WebSocket

飞一样的编程
飞一样的编程
擅长邻域:Java,MySQL,Linux,nginx,springboot,mongodb,微信小程序,vue

分类: springboot 专栏: springboot3.0新教材 标签: websocket

2024-02-28 20:16:34 1252浏览

Spring Boot整合WebSocket

前言

实时聊天怎么实现????

客服也有一个聊天端

没有websocket怎么实时通信

采用轮询的方法,客户端定期向服务器发送请求,这样会有啥弊端?思考一下

WebSocket简介

基本概念

WebSocket是一种协议,设计用于提供低延迟全双工长期运行的连接。

全双工:通信的两个参与方可以同时发送和接收数据,不需要等待对方的响应或传输完成。

websocket的优势

  • 实时性: WebSockets 允许服务器和客户端之间建立持久的连接,可以实现实时的双向通信。这意味着服务器可以主动向客户端推送数据,而无需客户端发起请求。
  • 低延迟: 由于建立了持久连接,数据的传输过程中不需要频繁地建立和关闭连接,因此能够降低通信过程中的延迟时间,使得实时性更高。
  • 减少数据传输量: 相较于传统的轮询方式(如长轮询),WebSockets 使用更少的数据传输量来维持连接,因为它避免了每次通信都要发送 HTTP 请求的开销。
  • 支持双向通信: WebSockets 支持全双工通信,即客户端和服务器可以同时发送和接收数据,可以更灵活地处理复杂的交互逻辑。
  • 跨平台支持: WebSockets 是一种标准化的协议,得到了广泛的支持,可以在各种现代浏览器和服务器环境中使用,使得跨平台开发更加方便。
  • 更好的性能: 相对于传统的基于 HTTP 的轮询方式,WebSockets 的性能更好,能够更有效地传输数据并减少网络开销。

websocket原理

WebSocket协议本质上是一个基于 TCP,先通过 HTTP/HTTPS 协议发起一条特殊的 HTTP 请求进行握手后创建一个用于交换数据的 TCP 连接。只需要要做一个握手的动作,就可以创建持久性 的连接,双方可以在任意时刻,相互推送信息,进行双向数据传输。 传统的 HTTP 是单向通讯协议,请求只能是客户端发起,而 WebSocket 是双向通讯协议,可以由服务器发起也可以是客户端发起。

STOMP 子协议

STOMP 是 WebSocket 的一个子协议,中文为: 面向消息的简单文本协议 websocket 定义了两种传输信息类型:文本信息和二进制信息。websocket 定义虽然确定了信息类型,但没有规定传输体,所以需要用一种简单的文本传输类型来规定传输内容,作为通 讯中的文本传输协议,STOMP 子协议因此产生。

image.png

使用 STOMP 协议有两个重点:

一是服务端向客户端发送信息,使用 SimpMessagingTemplate 对象的 convertAndSend 方 法,语法是:convertAndSend(发送到客户端的目标 URL,消息)。区分群发与单发的办法是客户端目标 URL 的前缀,如果前缀是 /topic 则表示群发,如果前缀是/queue 代表点对点单发。这需要先进行配置,具体配置方法见下面有案例。客户端目标 URL 是客户端在连接服务端时指定的。

二是客户端向服务器发送消息,使用 stompClient 的 send 方法,客户端需要导入 sockjs.min.js 和 stomp.min.js 才能使用 stompClient 对象。语法:send(服务端目标 URL,{},消息内容 JSON 格式的字符串),服务端的目标 URL 在服务端的控制器中用 MessageMapping 注解指定。客户端发送给服务端的消息通常并不是到达服务端就完事,一 般要经过加工或不经加工就转发(群发或单发)给其他客户端。客户端不能直接发消息给其 他客户端。

在线群聊聊天室实战

依赖

 <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>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.3.1</version>
        </dependency>

这里除了 websocket 有关依赖外,还导入了 sockjs,stomp-websocket 等前端框架。

实体类 Message

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String name;
    private String content;
}

配置类 WebSocketConfig。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry){
        //配置消息代理
        //前缀为/topic的消息将转发给消息代理再广播给当前连接的客户端
        registry.enableSimpleBroker("/topic");
        //凡是客户端发送消息的路径的前缀为/app的都交给控制器中@MessageMapping注解的方法进行处理
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override  //创建WebScocket端点,供客户端连接用,
    // 该端点的完整URL将是http://localhost:8080/webSocket
    public void registerStompEndpoints(StompEndpointRegistry registry){
        registry.addEndpoint("/webSocket").withSockJS();
    }
}

这里一是创建了一个 WebScocket 端点,供客户端连接用,二是配置了消息代理。

控制器 MessageController

@Controller
public class MessageController {

    //发送广播消息的第一种方法
    //凡是客户端请求路径的前缀为/app的都交给控制器中@MessageMapping注解的方法进行处理
    //如客户端请求/app/sendAll,交由此方法进行处理
    //处理结果发送到 url:/topic/receiveAll,所有订阅了此url的客户端将收到此信息
//    @MessageMapping("/sendAll ")
//    @SendTo("/topic/receiveAll ")
//    public Message say(Message message){
//        return message;
//    }

    //发送广播消息的第二种用法
    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/sendAll") //客户端发送消息的路径如果是/app/sendAll,将交由此方法进行处理
    public void sendAll(Message message) { //接收到客户端发送过来的消息
        //发送消息目标地址为/topic/receiveAll,所有订阅了/topic/receiveAll的客户端将收到此消息(广播)
        // 以/topic为前缀的代表广播
        messagingTemplate.convertAndSend("/topic/receiveAll", message);
    }

}

这个方法的作用是接收某个客户端发送过来的目标 URL 为/app/sendAll 的消息,然后 再发送到目标 URL:/topic/receiveAll,由于/topic 前缀代表广播,所以实际上将发送给所 有订阅了/topic/receiveAll 的客户端。注意,客户端要接收消息,要进行订阅,订阅时机是 一旦连接成功时,订阅时必须指定一个 URL(如/topic/receiveAll)。 发送广播消息还有另一种方法,见上述代码的注释处。

测试页面

static 下创建前端 chatAll.html 页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>群聊</title>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script type="text/javascript">
        var stompClient = null;
        //切换各个组件的显示与隐藏
        function show(flag) {
            $("#connect").prop("disabled", flag);
            $("#disconnect").prop("disabled", !flag);
            if (flag==true) {
                $("#chatRoom").show();//显示聊天室
                $("#user").text($("#name").val()); //显示用户名
            }else {
                $("#chatRoom").hide();//隐藏聊天室
            }
        }
        //连接到WebSocket服务器
        function connectWebSocket() {
            if (!$("#name").val()) {
                alert("请输入用户名!")
                return;
            }
            var url = "http://localhost:8080/webSocket";
            var socket = new SockJS(url);
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) { //连接成功的回调函数
                show(true);
                //本次连接的订阅消息,订阅服务端发送目标URL为/topic/receiveAll的消息
                stompClient.subscribe('/topic/receiveAll', function (data) { //data代表接收服务端发送过来的消息
                    showMsg(JSON.parse(data.body)); //展示接收到的消息
                });
            });
        }

        function showMsg(message) { //展示消息
            $("#chating").append("<div>" + message.name+":"+message.content + "</div>");
        }

        function closeWebSocket() { //关闭连接
            if (stompClient !== null) {
                stompClient.disconnect();
            }
            show(false);
        }
        function sendMsg() { //客户端发送消息
            stompClient.send("/app/sendAll",{}, //注意该路径必须要以/app为前缀
                //发送内容是JSON格式的字符串
                JSON.stringify({'name': $("#name").val(),'content':$("#content").val()}));
            $("#content").val("");
        }


        $(function () {
            $( "#connect" ).click(function() { connectWebSocket(); });
            $( "#disconnect" ).click(function() { closeWebSocket(); });
            $( "#send" ).click(function() { sendMsg(); });
        });
    </script>

</head>
<body>
<div>
    <label for="name">用户名:</label>
    <input type="text" id="name" placeholder="用户名">
    <button id="connect" type="button">进入聊天室</button>
    <button id="disconnect" type="button" disabled="disabled">退出聊天室</button>
</div>
<br/>
<div id="chatRoom" style="display: none;">

    <div id="chating" style="border:1px solid #ccc;width:400px;height:400px;">

    </div>
    <br/>
    <div>
        <label for="name"><span id="user"></span>说:</label>
        <input type="text" id="content" placeholder="请输入聊天内容">
        <button id="send" type="button">发送</button>
    </div>
</div>
</body>
</html>

运行测试

再打开一个窗口,登录另一个账号

一对一聊天实战

示例:有一个在线用户列表可以看到当前有哪些用户在线,并可以点击选择其中一个用 户 进 行 聊 天 , 一 旦 有 新 用 户 上 线 或 用 户 下 线 , 在 线 用 户 列 表 都 将 自 动 更 新 。 SimpMessagingTemplate 模板用于发送消息,可以群发也可以指定目标用户单发,指定目标 用户单发即能实现一对一单聊的功能。为了区分不同用户,这里用了 Security 进行认证。

加依赖

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

security配置类

@Configuration
public class SecurityConfig  {
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager= new InMemoryUserDetailsManager();
        UserDetails user1= User.withUsername("user1").password(passwordEncoder().encode("123")).roles("admin").build();
        UserDetails user2= User.withUsername("user2").password(passwordEncoder().encode("123")).roles("user").build();
        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
        security.authorizeHttpRequests().anyRequest().authenticated();
        security.formLogin().permitAll();
        //会话管理配置,设置一个用户只能建立一个会话
        security.sessionManagement()
                // 设置 Session 会话失效时重定向路径,默认为 loginPage()
                // .invalidSessionUrl("/login")
                // 配置使用自定义的 Session 会话失效处理策略
                //.invalidSessionStrategy(invalidSessionStrategy)
                // 设置单用户的 Session 最大并发会话数量,-1 表示不受限制
                .maximumSessions(1)
                // 设置为 true,表示某用户达到最大会话并发数后,新会话请求会被拒绝登录
                //.maxSessionsPreventsLogin(true)
                // 设置所要使用的 sessionRegistry,默认为 SessionRegistryImpl 实现类
                .sessionRegistry(sessionRegistry());

        return security.build();

    }




    @Bean //此顶配置后面用来获取所有登录用户
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }



}

实体类Message

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
    private String to;
    private String from;
    private String content;
}

配置类 WebSocketConfig

创建配置类 WebSocketConfig,添加 “/topic”和“/queue”路径,这样既然能广播又 能点对点单发,此外配置了一个监听器,用来监听连接断开。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry){
        //配置消息代理
        //前缀为/topic的消息将转发给消息代理再广播给当前连接的客户端
        //前缀为/queue代表点对点消息
        registry.enableSimpleBroker("/topic","/queue");
        //凡是客户端发送消息的路径的前缀为/app的都交给控制器中@MessageMapping注解的方法进行处理
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override  //创建WebScocket端点,供客户端连接用,
    // 该端点的完整URL将是http://localhost:81/webSocket
    public void registerStompEndpoints(StompEndpointRegistry registry){
        registry.addEndpoint("/webSocket").withSockJS();
    }



    @Bean   //断开连接监听器
    public StompDisconnectEventListener STOMPDisconnectEventListener(){
        return new StompDisconnectEventListener();
    }

}

MessageController

创建 MessageController,注入 SimpMessagingTemplate,使用它进行针对目标用户发 送信息,以及针对所有用户广播登录用户信息。下面分功能说明:一旦有用户登录,需要广而告之,让所有客户端都获取最新所有在线用户并更新列表,服务端实现代码:

 @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    SessionRegistry sessionRegistry;

    //广播消息
    @MessageMapping("/sendAll") //客户端发送消息的路径如果是/app/sendAll,将交由此方法进行处理
    public void sendAll(String message) throws JsonProcessingException { //接收到客户端发送过来的消息
        System.out.println(message);
        List<Object> list = sessionRegistry.getAllPrincipals();//获取所有登录用户对象集合
        List<String> users=new ArrayList<>();//只需登录用户名称字符串集合
        for (Object o : list) {
            User u= (User) o;
            users.add(u.getUsername());
        }
        //发送消息目标地址为/topic/receive,所有订阅了/topic/receiveAll的客户端将收到此消息(广播)
        // 以/topic为前缀的代表广播
        //将所有登录用户的信息广播给全体客户端
        messagingTemplate.convertAndSend("/topic/receiveAll",users);
    }

每个用户登录后需要获取用户名,服务端实现代码:

    //获取当前用户
    @GetMapping("/getCurrentUser")
    @ResponseBody
    public String getCurrentUser(Principal principal){
        return principal.getName();
    }

一个客户端想发信息到另一个客户端,服务端服务代码:

  //点对点发送消息
    @MessageMapping("/sendTo")
    public void sendTo (Principal principal, Message message){
        message.setFrom(principal.getName());
        messagingTemplate.convertAndSendToUser(message.getTo(),"/queue/receiveOne",message);
        //如果有需要,也可同时发一份消息给自己
        //messagingTemplate.convertAndSendToUser(message.getFrom(),"/queue/receiveOne",message);
    }

断开监听器

连接断开监听器。如果有连接断开,监听器将可以监听到,并广播最新的用户列表 给所有客户端,客户端接收到消息后用回调函数更新用户列表。监听器的代码如下:

//监听用户连接断开
public class StompDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {


    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    SessionRegistry sessionRegistry;


    //监听用户断开连接,一旦监听到有用户断开连接,就获取最新用户名单广播给所有客户端
    //以便客户端更新用户列表
    @Override
    public void onApplicationEvent(SessionDisconnectEvent event) {
        StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
        Principal principal = sha.getUser();
        List<Object> list = sessionRegistry.getAllPrincipals();//获取所有登录用户对象集合
        List<String> users=new ArrayList<>();//只需登录用户名称字符串集合
        for (Object o : list) {
            User u= (User) o;
            if(u.getUsername().equals(principal.getName())){ //排除掉刚刚下线的用户。
                 continue;//当if条件满足时跳过当前循环中continue之后的代码,执行下一次循环。
          
            }
            users.add(u.getUsername());
        }
        //发送消息目标地址为/topic/receive,所有订阅了/topic/receiveAll的客户端将收到此消息(广播)
        // 以/topic为前缀的代表广播
        //将所有登录用户的信息广播给全体客户端
        messagingTemplate.convertAndSend("/topic/receiveAll",users);
    }
}


前端页面

static 下创建 chatOne.html

基本思路是:当页面加载时,连接 webscoket,连接成功后

①订阅一个点对点消息;

② 再订阅一个群发消息,用于接收广播更新用户列表;

③然后群发一个消息告诉大家 Ta 上线了;其中群发消息发送到服务器后,服务器的操作是获取到最新的所有登录用户的数据然后群发给所有客户端,客户端再更新用户列表,后面只要有用户上线就会更新数据。发送消息的流程是:先获取到用户输入的信息,以及目标用户,封装后发送给服务器,服务器再转发给目标用户。 断开连接的流程是:先调用 stompClient 的 disconnect 方法,连接会中断,然后服务器的监听器会监听到有连接断开,就会查找最新的用户列表再广播给所有客户端,从而客户端会及时更新用户列表。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>单聊</title>
    <style>
        #chats{
            width:400px;height:300px;border: 1px solid #ccc;float: left;
        }

        #online{
            width:150px;height:300px;border: 1px solid #ccc;background-color:#ccc;float:left;margin-left:20px;
        }

    </style>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script type="text/javascript">
        var stompClient = null;
        function connectWebSocket() { //连接WebSocket
            var url = "http://localhost:81/webSocket"
            var socket = new SockJS(url);
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function (frame) {
                //订阅单个用户发送过来的信息,必须要/user/queue做前缀
                stompClient.subscribe('/user/queue/receiveOne', function (msg) {
                    showMsg(JSON.parse(msg.body));//展示信息
                });
                //同时订阅群发消息
                stompClient.subscribe('/topic/receiveAll', function (data) { //data代表接收服务端发送过来的消息
                    updateUsers(data);//更新用户列表
                });
                //同时发送一条消息给服务器,服务器将广播此条消息,让人知道Ta上线了
                stompClient.send("/app/sendAll",{}, "我来啦");

            });
        }

        function showMsg(message) {//展示信息
            $("#chats").append("<div>" + message.from+":"+message.content + "</div>");
        }

        function updateUsers(data){ //更新用户列表
            var users=JSON.parse(data.body);//转换为JSON对象
            var html="";
            for(var i=0;i<users.length;i++){
                html+="<div onclick='setTo(this)'>"+users[i]+"</div>";
            }
            $("#users").html(html);
        }

        function sendMsg() {//发送信息
            stompClient.send("/app/sendTo", {},
                JSON.stringify({'content':$("#content").val(),'to':$("#to").val()}));
        }

        function setTo(obj){ //设置发送目标,当点击列表中的用户名时触发
            $("#to").val(obj.innerText);
        }

        function checkData(){ //检查发送的数据等是否正确
            if($("#content").val()==""){
                alert("聊天内容不能为空");
                return;
            }
            if($("#to").val()==""){
                alert("发送目标不能为空");
                return;
            }
            //发送目标必须是列表中有的用户
            var listUser=$("#users").text();//列表中有的用户
            var toUser=$("#to").val();//发送目标
            if(listUser.indexOf(toUser)==-1){//列表中是否包含发送目标
                alert("发送目标必须是列表中有的用户");
                return;
            }
        }

        function setCurrentUser(){ //从后台获取当前用户信息
            $.ajax({
                url:"getCurrentUser",
                success:function (data){
                    $("#currentUser").text(data);//用户名显示到标签上
                }
            })
        }

        function closeWebSocket() { //关闭连接
            if (stompClient !== null) {
                stompClient.disconnect();
                //当前用户连接关闭后,后台会监听到并更新最新的用户列表广播给其他用户
            }
            window.location.href="/logout";
        }

        $(function () {
            //连接WebSocket
            connectWebSocket();
            //绑定发送按钮的点击事件
            $("#send").click(function() {checkData(); sendMsg(); $("#content").val("");});
            //设置好当前用户信息
            setCurrentUser();
         });



    </script>
</head>
<body>

    <div>
        用户【<span id="currentUser"></span>】说:
        <input type="text" id="content" placeholder="请输入聊天内容">
        发送给:
        <input type="text" id="to" placeholder="目标用户">
        <button id="send" type="button" >发送</button>
        &nbsp; <input type="button" id="disconnect" value="退出聊天" onclick="closeWebSocket()"/>
    </div>
    <br/>
    <div id="chats">
    </div>
    <div id="online">
        当前在线用户:
        <div id="users">

        </div>
    </div>



</body>
</html>

运行测试

弹出登录界面,输入用户名 user1,密码 123,进入聊天界面

输入用户名 user2,密码 123,然后 user1 就可以单独发送信息给 user2,user2 也可单独发信息给 user1


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695