文章目录

  • WebSocket 聊天室开发
  • 1. 介绍
  • 1.1 概念
  • 2. websocket
  • 2.1 环境准备
  • 2.2 代码
  • 2.3 测试
  • 3.websocket+SpringBoot
  • 3.1 环境准备
  • 项目结构
  • pom 文件
  • 3.2 代码
  • 3.2.1 实体类
  • 3.2.2 socket 配置类
  • 3.2.3 springMVC配置类
  • 3.2.4 Controller
  • 3.2.5 主类
  • 3.2.6 前端页面
  • 3.3 测试
  • 配置类
  • 3.4 解释
  • 4.websocket+rabbitmq+stomp
  • 4.1 环境准备
  • rabbitmq 安装stomp
  • 4.2 代码
  • 4.3 测试
  • 4.4 解释
  • 4.1. /exchange/exchangename/[routing_key]
  • 4.2. /queue/queuename
  • 4.3. /amq/queue/queuename
  • 4.4. /topic/routing_key

1. 介绍

1.1 概念

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

2. websocket

2.1 环境准备

2.2 代码

2.3 测试

3.websocket+SpringBoot

3.1 环境准备

js下载链接

https://www.bootcdn.cn/

参考文章

https://blog.csdn.net/liyongzhi1992/article/details/81221103

项目结构

音视频开发-websocket教程_java

pom 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ak</groupId>
    <artifactId>socket+springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>socket+springboot</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-amqp</artifactId>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <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>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.67</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.amqp</groupId>-->
<!--            <artifactId>spring-rabbit-test</artifactId>-->
<!--            <scope>test</scope>-->
<!--        </dependency>-->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.2 代码

3.2.1 实体类

package com.ak.socketmq.entity;


import lombok.Data;

@Data
public class User {
    private String name;
    private int age;
    private String id;
    private String to;
    private String message;
}

3.2.2 socket 配置类

package com.ak.socketmq.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * 配置WebSocket
 * //注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping
 * 一样
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * //注册STOMP协议的节点(endpoint),并映射指定的url
     * @param registry
     */
	@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {

        //注册一个STOMP的endpoint,并指定使用SockJS协议
        registry.addEndpoint("/chat").setAllowedOriginPatterns("*").withSockJS();
    }

    /**
     * 配置消息代理(Message Broker)
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
        // topic 是由服务端推送信息--群体消息
        // user 由服务端 推动1对1 的消息
        // mass 聊天室 前端推送  --群体消息
        // alone 聊天室 前端推动--1对1 聊天消息
        registry.enableSimpleBroker("/topic","/user","/mass","/alone");

        //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
        registry.setUserDestinationPrefix("/user");
    }



}

3.2.3 springMVC配置类

package com.ak.socketmq.config;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
@Configuration
public class MyMvcConfig extends WebMvcConfigurationSupport {
 
    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
//        registry.addViewController("/").setViewName("login");
    }
 
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/resources/")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/public/");
 
        super.addResourceHandlers(registry);
    }
 
}

3.2.4 Controller

package com.ak.socketmq.controller;


import com.ak.socketmq.entity.User;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 这里有前端进行推送
 */
@Controller
@Slf4j
public class JsController {

    @Autowired
    private SimpMessagingTemplate template;

    /**
     * 群发
     *
     * @param
     * @return
     * @throws Exception
     */
    @MessageMapping("/massRequest")//当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似于@RequestMapping
    @SendTo("/mass/getResponse")//当服务端有消息时,监听了/topic/getResponse的客户端会接收消息
    public String say(User user) throws Exception {
        log.info("群发消息" + user.toString());

        return JSONObject.toJSONString(user);
    }

    /**
     * 单发
     * @param user
     * @return
     */
    @MessageMapping("/aloneRequest")
    public String alone(User user) {
        log.info("前端单独发" + user.toString());
        template.convertAndSendToUser(user.getTo() + "", "/alone/getResponse", user.getMessage());
        return JSONObject.toJSONString(user);
    }
}

后端

package com.ak.socketmq.controller;

import com.ak.socketmq.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Random;


/**
 * 服务端推动的消息
 */
@Controller
@Slf4j
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate template;


    /**
     * 广播推送消息
     */
    @Scheduled(fixedRate = 10000)
    public void sendTopicMessage() {
        log.info("web 后台广播推送!");
        User user = new User();
        user.setName("joker");
        user.setAge(22);
        template.convertAndSend("/topic/getResponse", user);
    }

    /**
     * 一对一推送消息 10秒钟
     */
    @Scheduled(fixedRate = 10000)
    public void sendQueueMessage() {
        // 每次随机向
        int i=new Random().nextInt()%2;
        User user=new User();
        user.setId(String.valueOf(i));
        user.setName("joker");
        user.setAge(10);
        /**
         * 参数一指定客户端接收的用户标识
         * 参数二客户端监听指定通道时,设定的访问服务器的URL
         * 参数三向目标发送消息体(实体、字符串等等)
         */
        log.info("web 一对一推送!"+user.toString());
        template.convertAndSendToUser(user.getId()+"","/queue/getResponse",user);
    }

}

3.2.5 主类

package com.ak.socketmq;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SocketmqApplication {

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

}

3.2.6 前端页面

<!DOCTYPE html>
<html>

<head>
    <title>websocket.html</title>
    <meta name="keywords" content="keyword1,keyword2,keyword3">
    <meta name="description" content="this is my page">
    <meta name="content-type" content="text/html" charset="UTF-8">
    <!--<link rel="stylesheet" type="text/css" href="./styles.css">-->
    <style>
        .blue {
            color: blue;
        }

        .red {
            color: red;
        }
        .left{
            float: left;
            margin-left: 50px;
        }
    </style>
</head>

<body>
    <div class="left">
        web 后端推送的全局消息
        <p id="response" class="blue"></p>

        <br>
    
    </div>
    <div class="left">
        web 后端推动的1 对 1 消息,因为后端 random%2 ,所以有有1 
        <select id="select">
            <option value="0">0</option>
            <option value="1">1</option>
            <option value="2">2</option>
        </select>
        <div class="red" id="one"></div>
        <button value="开始连接1对1" onclick="start()">开始开始连接1对1</button>
    </div>

    <!-- 独立JS -->
    <script type="text/javascript" src="../static/jquery-3.5.1.min.js" charset="utf-8"></script>
    <script type="text/javascript" src="../static/sockjs.min.js" charset="utf-8"></script>
    <script type="text/javascript" src="../static/stomp.min.js" charset="utf-8"></script>
    <script>
        var stompClient = null;

        function start() {
            var id = $("#select").val();
            console.log(id);
            if (id != null) {
                stompClient.subscribe("/user/"+id+"/queue/getResponse", function (response) {
                    $("#one").append("<p>"+JSON.parse(response.body)+"</p>")
                });
            }
        }

        //加载完浏览器后  调用connect(),打开双通道
        $(function () {
            //打开双通道
            connect()
        })

        //强制关闭浏览器  调用websocket.close(),进行正常关闭
        window.onunload = function () {
            disconnect()
        }

        function connect() {

            var socket = new SockJS('http://localhost:8080/chat'); //连接SockJS的endpoint名称为"endpointOyzc"
            stompClient = Stomp.over(socket); //使用STMOP子协议的WebSocket客户端

            // 连接成功后
            stompClient.connect({}, function (frame) { //连接WebSocket服务端
                console.log('Connected:' + frame);

                //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息
                stompClient.subscribe('/topic/getResponse', function (response) {
                    console.log(response);
                    showResponse(JSON.parse(response.body));
                });
            });
        }

        // function sendName() {
        //     var name = $('#name').val();
        //     stompClient.send("/welcome", {}, JSON.stringify({
        //         'name': name
        //     }));
        //     //通过stompClient.send 向/welcome目标(destination)发送消息,这个实在控制器的@MessageMapping中定义的
        // }
        //关闭双通道
        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            console.log("Disconnected");
        }

        function showResponse(message) {
            var response = $("#response");
            response.append("<p>" + message + "</p>");
        }
    </script>
</body>


</html>

kk.html

<!DOCTYPE html>
<html>

<head>
    <title>login.html</title>

    <meta name="keywords" content="keyword1,keyword2,keyword3">
    <meta name="description" content="this is my page">
    <meta name="content-type" content="text/html" charset="UTF-8">
    <!--<link rel="stylesheet" type="text/css" href="./styles.css">-->
    <!-- 独立css -->
    <link rel="stylesheet" type="text/css" href="chatroom.css">
</head>

<body>
<!-- 这个是 由服务端来推送-->
    <div>
        <div style="float:left;width:40%">
            <p>请选择你是谁:</p>
            <select id="selectName" onchange="sendAloneUser();">
                <option value="1">请选择</option>
                <option value="ALong">ALong</option>
                <option value="AKan">AKan</option>
                <option value="AYuan">AYuan</option>
                <option value="ALai">ALai</option>
                <option value="ASheng">ASheng</option>
            </select>
            <div class="chatWindow">
                <p style="color:darkgrey">群聊:</p>
                <section id="chatRecord" class="chatRecord">
                    <p id="titleval" style="color:#CD2626;"></p>
                </section>
                <section class="sendWindow">
                    <textarea name="sendChatValue" id="sendChatValue" class="sendChatValue"></textarea>
                    <input type="button" name="sendMessage" id="sendMessage" class="sendMessage"
                        onclick="sendMassMessage()" value="发送">
                </section>
            </div>
        </div>

        <div style="float:right; width:40%">
            <p>请选择你要发给谁:</p>
            <select id="selectName2">
                <option value="1">请选择</option>
                <option value="ALong">ALong</option>
                <option value="AKan">AKan</option>
                <option value="AYuan">AYuan</option>
                <option value="ALai">ALai</option>
                <option value="ASheng">ASheng</option>
            </select>
            <div class="chatWindow">
                <p style="color:darkgrey">单独聊:</p>
                <section id="chatRecord2" class="chatRecord">
                    <p id="titleval1" style="color:#CD2626;"></p>
                </section>
                <section class="sendWindow">
                    <textarea name="sendChatValue2" id="sendChatValue2" class="sendChatValue"></textarea>
                    <input type="button" name="sendMessage" id="sendMessage" class="sendMessage"
                        onclick="sendAloneMessage()" value="发送">
                </section>
            </div>
        </div>
    </div>
    <!-- 独立JS -->
    <script type="text/javascript" src="jquery-3.5.1.min.js" charset="utf-8"></script>

    <script type="text/javascript" src="sockjs.min.js" charset="utf-8"></script>
    <script type="text/javascript" src="stomp.min.js" charset="utf-8"></script>
    <script>
        var stompClient = null;

        //加载完浏览器后  调用connect(),打开双通道
        $(function () {
            //打开双通道
            connect()
        })

        //强制关闭浏览器  调用websocket.close(),进行正常关闭
        window.onunload = function () {
            disconnect()
        }

        //打开双通道
        function connect() {
            var socket = new SockJS('http://localhost:8080/chat'); //连接SockJS的endpoint名称为""
            stompClient = Stomp.over(socket); //使用STMOP子协议的WebSocket客户端
            stompClient.connect({}, function (frame) { //连接WebSocket服务端
                console.log('Connected:' + frame);
                //广播接收信息
                stompTopic();
            });
        }

        //关闭双通道
        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            console.log("Disconnected");
        }

        //广播(一对多)
        function stompTopic() {

            //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息(广播接收信息)
            stompClient.subscribe('/mass/getResponse', function (response) {
                var message = JSON.parse(response.body);

                //展示广播的接收的内容接收
                var response = $("#chatRecord");
                response.append("<p><span>" + message.name + ":</span><span>" + message.chatValue +
                    "</span></p>");
            });
        }

        //列队(一对一)
        function stompQueue() {

            var userId = $("#selectName").val();
            alert("监听:" + userId)
            //通过stompClient.subscribe订阅/topic/getResponse 目标(destination)发送的消息(队列接收信息)
            stompClient.subscribe('/user/' + userId + '/alone/getResponse', function (response) {
                
                var message = JSON.parse(response.body);
                //展示一对一的接收的内容接收
                var response = $("#chatRecord2");
                response.append("<p><span>" + message.name + ":</span><span>" + message.chatValue +
                    "</span></p>");
            });
        }

        //选择发送给谁的时候触发连接服务器
        function sendAloneUser() {
            stompQueue();
        }

        //群发
        function sendMassMessage() {
            var postValue = {};
            var chatValue = $("#sendChatValue");
            var userName = $("#selectName").val();
            postValue.name = userName;
            postValue.chatValue = chatValue.val();
            if (userName == 1 || userName == null) {
                alert("请选择你是谁!");
                return;
            }
            if (chatValue == "" || userName == null) {
                alert("不能发送空消息!");
                return;
            }
            stompClient.send("/massRequest", {}, JSON.stringify(postValue));
            chatValue.val("");
        }
        //单独发
        function sendAloneMessage() {
            var postValue = {};
            var chatValue = $("#sendChatValue2");
            var userName = $("#selectName").val();
            var sendToId = $("#selectName2").val();
            var response = $("#chatRecord2");
            postValue.name = userName;
            postValue.message = chatValue.val();
            postValue.to = sendToId;
            if (userName == 1 || userName == null) {
                alert("请选择你是谁!");
                return;
            }
            if (sendToId == 1 || sendToId == null) {
                alert("请选择你要发给谁!");
                return;
            }
            if (chatValue == "" || userName == null) {
                alert("不能发送空消息!");
                return;
            }
            stompClient.send("/aloneRequest", {}, JSON.stringify(postValue));
            response.append("<p><span>" + userName + ":</span><span>" + chatValue.val() + "</span></p>");
            chatValue.val("");
        }
    </script>
</body>

</html>

3.3 测试

配置类

package com.ak.socketmq.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * 配置WebSocket
 * //注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping
 * 一样
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	//注册STOMP协议的节点(endpoint),并映射指定的url
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //注册一个STOMP的endpoint,并指定使用SockJS协议
        registry.addEndpoint("/endpointOyzc").setAllowedOrigins("*").withSockJS();
    }
    @Override
    //配置消息代理(Message Broker)
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //点对点应配置一个/user消息代理,广播式应配置一个/topic消息代理
        registry.enableSimpleBroker("/topic","/user");
        //点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/
        registry.setUserDestinationPrefix("/user");
    }
}

3.4 解释

注解参考

@EnableWebSocketMessageBroker:开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。
AbstractWebSocketMessageBrokerConfigurer:继承WebSocket消息代理的类,配置相关信息。
registry.addEndpoint("/endpointOyzc").setAllowedOrigins("*").withSockJS(); 添加一个访问端点“/endpointGym”,客户端打开双通道时需要的url,允许所有的域名跨域访问,指定使用SockJS协议。
registry.enableSimpleBroker("/topic","/user"); 配置一个/topic广播消息代理和“/user”一对一消息代理
registry.setUserDestinationPrefix("/user");点对点使用的订阅前缀(客户端订阅路径上会体现出来),不设置的话,默认也是/user/

4.websocket+rabbitmq+stomp

4.1 环境准备

音视频开发-websocket教程_spring boot_02

rabbitmq 安装stomp

rabbitmq-plugins enable rabbitmq_stomp
rabbitmq-plugins enable rabbitmq_web_stomp
systemctl restart rabbitmq-server

音视频开发-websocket教程_spring boot_03

音视频开发-websocket教程_java_04

4.2 代码

4.3 测试

4.4 解释

4.1. /exchange/exchangename/[routing_key]

通过交换机订阅/发布消息,交换机需要手动创建,参数说明 a. /exchange:固定值 b. exchangename:交换机名称 c. [routing_key]:路由键,可选

对于接收者端,该 destination 会创建一个唯一的、自动删除的随机queue, 并根据 routing_key将该 queue 绑定到所给的 exchangename,实现对该队列的消息订阅。 对于发送者端,消息就会被发送到定义的 exchangename中,并且指定了 routing_key。

4.2. /queue/queuename

使用默认交换机订阅/发布消息,默认由stomp自动创建一个持久化队列,参数说明 a. /queue:固定值 b. queuename:自动创建一个持久化队列

对于接收者端,订阅队列queuename的消息 对于接收者端,向queuename发送消息 [对于 SEND frame,destination 只会在第一次发送消息的时候会定义的共享 queue]

4.3. /amq/queue/queuename

和上文的"/queue/queuename"相似,两者的区别是 a. 与/queue/queuename的区别在于队列不由stomp自动进行创建,队列不存在失败

这种情况下无论是发送者还是接收者都不会产生队列。 但如果该队列不存在,接收者会报错。

4.4. /topic/routing_key

通过amq.topic交换机订阅/发布消息,订阅时默认创建一个临时队列,通过routing_key与topic进行绑定 a. /topic:固定前缀 b. routing_key:路由键

对于发送者端,会创建出自动删除的、非持久的队列并根据 routing_key路由键绑定到 amq.topic 交换机 上,同时实现对该队列的订阅。 对于发送者端,消息会被发送到 amq.topic 交换机中。

测试: 打开两个页面,其中一个页面发送4次,这4个消息同时被两个都收到