WebSocket 聊天室开发
https://gitee.com/fakerlove/websocket
WebSocket 聊天室开发
https://edu.csdn.net/learn/28785 课程介绍45块钱
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
项目结构
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 环境准备
rabbitmq 安装stomp
rabbitmq-plugins enable rabbitmq_stomp
rabbitmq-plugins enable rabbitmq_web_stomp
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个消息同时被两个都收到