學(xué)習(xí)鏈接
Spring WebSocket整合Stomp源碼詳解 PDF版本
Spring & SpringBoot官方文檔資料
- spring5.1.9官方文檔關(guān)于websocket的介紹
- spring5.3.29官方文檔關(guān)于websocket的介紹
WebSocket入門教程示例代碼,代碼地址已fork至本地gitee,原github代碼地址,源老外的代碼地址
- [WebSocket入門]手把手搭建WebSocket多人在線聊天室(SpringBoot+WebSocket)
- [WebSocket]第二章:WebSocket集群分布式改造——實(shí)現(xiàn)多人在線聊天室
- [WebSocket]使用WebSocket實(shí)現(xiàn)實(shí)時(shí)多人答題對(duì)戰(zhàn)游戲
其它可參考
-
手把手搭建WebSocket多人在線聊天室(SpringBoot+WebSocket),這個(gè)比較詳細(xì),排版看上去比較舒服
-
springboot集成websocket小案例 bilibili視頻
-
SpringBoot+STOMP 實(shí)現(xiàn)聊天室(單聊+多聊)及群發(fā)消息詳解
-
springboot+websocket構(gòu)建在線聊天室(群聊+單聊)
-
基于STOMP協(xié)議的WebSocket
-
spring websocket + stomp 實(shí)現(xiàn)廣播通信和一對(duì)一通信 ,這個(gè)用法很詳細(xì)
深入使用
-
SpringBoot——整合WebSocket(STOMP協(xié)議) 原創(chuàng)
-
SpringBoot——整合WebSocket(基于STOMP協(xié)議)
-
點(diǎn)對(duì)點(diǎn)通信
補(bǔ)充學(xué)習(xí)
-
【Springboot WebSocket STOMP使用 1】Springboot最小化配置啟用STOMP,并實(shí)現(xiàn)瀏覽器JS通信
-
【Springboot WebSocket STOMP使用 2】STOMP使用@SendToUser實(shí)現(xiàn)用戶個(gè)人請(qǐng)求-響應(yīng)
-
WebSocket的那些事(4-Spring中的STOMP支持詳解),前面還有3篇
-
SpringBoot Websocket Stomp 實(shí)現(xiàn)單設(shè)備登錄(頂號(hào)) ①
-
[WebSocket]之上層協(xié)議STOMP
-
SpringBoot 集成 STOMP 實(shí)現(xiàn)一對(duì)一聊天的兩種方法
-
SpringBoot 配置基于 wss 和 STOMP 的 WebSocket
-
STOMP 客戶端 API 整理
-
Spring websocket+Stomp+SockJS 實(shí)現(xiàn)實(shí)時(shí)通信 詳解
-
spring+socket+stomp 實(shí)現(xiàn)消息推送
-
Spring Websocket+Stomp 防踩坑實(shí)戰(zhàn)
-
【Springboot實(shí)例】WebSocket即時(shí)聊天室設(shè)計(jì)與實(shí)現(xiàn)
-
Stomp on Spring WebSocket項(xiàng)目源碼分析
-
(二) WebSocket客戶端/服務(wù)端代碼
-
Springboot項(xiàng)目整合WebSocket源碼分析
-
Spring websocket+Stomp+SockJS 實(shí)現(xiàn)實(shí)時(shí)通信 詳解
后續(xù)使用rabbimtmq作為消息代理實(shí)現(xiàn)時(shí),參考的文章
- Docker容器添加映射端口的兩種實(shí)現(xiàn)方法,因?yàn)樾枰猺abbitmq需要開啟rabbitmq_web_stomp插件、rabbitmq_web_stomp_examples插件,開啟方式參考下面這個(gè)鏈接,然后需要在docker和主機(jī)之間開啟端口映射
- Rabbitmq報(bào)錯(cuò):Connection refused: no further information: /ip:61613,使用rabbitmq作為消息代理,需要讓我們的服務(wù)連接到rabbitmq,并且mq要開啟rabbitmq_web_stomp插件、rabbitmq_web_stomp_examples插件
示例1
pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.timeless</groupId>
<artifactId>timeless-chat-websocket</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--<dependency>-->
<!-- <groupId>com.itheima</groupId>-->
<!-- <artifactId>pd-tools-swagger2</artifactId>-->
<!-- <version>1.0-SNAPSHOT</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- RabbitMQ Starter Dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Following additional dependency is required for Full Featured STOMP Broker Relay -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8888
spring:
application:
name: timeless-chat-websocket
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/timeless_chat_websocket?serverTimeZone=UTC
username: root
password: root
mvc:
pathmatch:
# Springboot2.6以后將SpringMVC 默認(rèn)路徑匹配策略從AntPathMatcher 更改為PathPatternParser
#
matching-strategy: ANT_PATH_MATCHER
rabbitmq:
host: ${rabbitmq.host}
port: ${rabbitmq.port}
username: ${rabbitmq.username}
password: ${rabbitmq.password}
virtual-host: ${rabbitmq.virtual-host}
#pinda:
# swagger:
# enabled: true
# title: timeless文檔
# base-package: com.timeless.controller
mybatis-plus:
configuration:
log-impl: com.timeless.utils.NoLog
WebSocketConfig
@Configuration
@Slf4j
@EnableWebSocketMessageBroker
@EnableConfigurationProperties(RabbitMQProperties.class)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private RabbitMQProperties rabbitMQProperties;
public WebSocketConfig(RabbitMQProperties rabbitMQProperties) {
this.rabbitMQProperties = rabbitMQProperties;
log.info("連接rabbitmq, host: {}", rabbitMQProperties.getHost());
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
// 這個(gè)和客戶端創(chuàng)建連接時(shí)的url有關(guān),后面在客戶端的代碼中可以看到
.addEndpoint("/ws")
.addInterceptors(new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
log.info("客戶端握手即將開始===================【開始】");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
log.info("請(qǐng)求路徑: {}", ((ServletServerHttpRequest) request).getServletRequest().getRequestURL());
log.info("校驗(yàn)請(qǐng)求頭,以驗(yàn)證用戶身份: {}", JsonUtil.obj2Json(servletRequest.getHeaders()));
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
return true;
}
log.info("客戶端握手結(jié)束=================== 【失敗】");
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
log.info("客戶端握手結(jié)束=================== 【成功】");
}
})
// .setAllowedOrigins("http://localhost:8080")
// 當(dāng)傳入*時(shí), 使用該方法, 而不要使用setAllowedOrigins("*")
.setAllowedOriginPatterns("*")
.withSockJS()
;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 1. 當(dāng)客戶端發(fā)送消息或訂閱消息時(shí),url路徑開頭如果是/app/xxx 時(shí),會(huì)先解析stomp協(xié)議,然后路由到@controller的@MessageMapping("/xxx")的方法上執(zhí)行。
// 如果不設(shè)置,客戶端所有發(fā)送消息或訂閱消息時(shí)、都將去匹配@messageMapping。所以最好還是配置上。
// 2. 這句表示客戶端向服務(wù)端發(fā)送時(shí)的主題上面需要加"/app"作為前綴
registry.setApplicationDestinationPrefixes("/app");
// 1. 基于內(nèi)存的消息代理
// 2. 聲明消息中間件Broker的主題名稱,當(dāng)向這個(gè)主題下發(fā)送消息時(shí)(js: stompclient.send("/topic/target1",{},"hello")),訂閱當(dāng)前主題的客戶端都可以收到消息。
// 注意:js 客戶端如果發(fā)送時(shí)、直接是/topic/xxx,spring收到消息會(huì)直接發(fā)送給broker中。
// 點(diǎn)對(duì)點(diǎn)發(fā)送時(shí):enableSimpleBroker 中要配置 /user才可以用: template.convertAndSendToUser("zhangsan","/aaa/hello","111"),否則收不到消息
// 3. 這句表示在topic和user這兩個(gè)域上可以向客戶端發(fā)消息
registry.enableSimpleBroker("/topic", "/user");
// 1. 點(diǎn)對(duì)點(diǎn)發(fā)送前綴
// 2. 這句表示給指定用戶發(fā)送(一對(duì)一)的主題前綴是 /user
registry.setUserDestinationPrefix("/user");
// Use this for enabling a Full featured broker like RabbitMQ
/*
// 基于mq的消息代理
registry.enableStompBrokerRelay("/topic")
.setVirtualHost(rabbitMQProperties.getVirtualHost())
.setRelayHost(rabbitMQProperties.getHost())
.setRelayPort(61613)
.setClientLogin(rabbitMQProperties.getUsername())
.setClientPasscode(rabbitMQProperties.getPassword())
.setSystemLogin(rabbitMQProperties.getUsername())
.setSystemPasscode(rabbitMQProperties.getPassword())
.setSystemHeartbeatSendInterval(5000)
.setSystemHeartbeatReceiveInterval(5000);
*/
}
}
PrivateController
@Slf4j
@RestController
public class PrivateController {
@Autowired
private WebSocketService ws;
// 1. 這個(gè)注解其實(shí)就是用來定義接受客戶端發(fā)送消息的url(不能是topic開頭,如果是topic直接發(fā)送給broker了,要用/app/privateChat)
// 如果有返回值,則會(huì)將返回的內(nèi)容轉(zhuǎn)換成stomp協(xié)議格式發(fā)送給broker(主題名:/topic/privateChat)。如果要換主題名可使用@sendTo
// @SubscribeMapping注解和@messageMapping差不多,但不會(huì)再把內(nèi)容發(fā)給broker,而是直接將內(nèi)容響應(yīng)給客戶端,
@MessageMapping("/privateChat")
public void privateChat(PrivateMessage message) {
// 使用發(fā)布訂閱的方式變相的實(shí)現(xiàn)私聊(并不是真正意義上的點(diǎn)對(duì)點(diǎn))
ws.sendChatMessage(message);
}
/**
* 問候信息處理
* <p>{@link MessageMapping}方法的返回值會(huì)被轉(zhuǎn)發(fā)到Broker對(duì)應(yīng)的主題中</p>
* <p>比如向/app/greetings發(fā)送的消息,其響應(yīng)會(huì)被轉(zhuǎn)發(fā)到/topic/greetings主題中</p>
*/
@MessageMapping("/greetings")
public String greetings(String content) {
return String.format("Server response: %s", content);
}
// 客戶端向 /app/broadcastMsg 發(fā)送消息, 將會(huì)使用該方法處理,
// 并且因?yàn)榇朔椒ㄓ蟹祷刂? 所以將結(jié)果又發(fā)送到/topic/broadcastMsg, 因此訂閱了/topic/broadcastMsg的客戶端將會(huì)收到此消息
@MessageMapping("/broadcastMsg")
@SendTo("/topic/broadcastMsg")
public BroadcastMessage broadcastMsg(@Payload BroadcastMessage message,
SimpMessageHeaderAccessor headerAccessor) {
// 理解為會(huì)話添加屬性標(biāo)識(shí)
headerAccessor.getSessionAttributes().put("extraInfo", message.getFromUsername());
message.setContent("廣播消息>>> " + message.getContent());
return message;
}
@Autowired
private SimpMessagingTemplate template;
// 廣播推送消息
// (向此接口發(fā)送請(qǐng)求, 將會(huì)向所有的訂閱了 /topic/broadcastMsg的客戶端發(fā)送消息)
@RequestMapping("/sendTopicMessage")
public void sendTopicMessage(String content) {
template.convertAndSend("/topic/broadcastMsg", content);
}
// 點(diǎn)對(duì)點(diǎn)消息
@RequestMapping("/sendPointMessage")
// (向此接口發(fā)送請(qǐng)求, 將會(huì)向所有的訂閱了 /user/{targetUsername}/singleUserMsg 的客戶端發(fā)送消息。
// 這種方式調(diào)用的前提是需要registry.enableSimpleBroker("/topic", "/user"); 里面指定/user的前綴時(shí)才能使用的
// (但覺得這并不是真正意義上的點(diǎn)對(duì)點(diǎn),因?yàn)橹灰锌蛻舳擞嗛喠诉@個(gè)/user/{targetUsername}/singleUserMsg主題, 就能收到這個(gè)主題下的消息,
public void sendQueueMessage(String targetUsername, String content) {
this.template.convertAndSendToUser(targetUsername, "/singleUserMsg", content);
}
}
WebSocketService
@Service
public class WebSocketService {
@Autowired
private SimpMessagingTemplate template;
@Autowired
private PrivateMessageService privateMessageService;
/**
* 簡(jiǎn)單點(diǎn)對(duì)點(diǎn)聊天室(使用發(fā)布訂閱的方式變相的實(shí)現(xiàn)私聊(并不是真正意義上的點(diǎn)對(duì)點(diǎn)))
*/
public void sendChatMessage(PrivateMessage message) {
message.setMessage(message.getFromUsername() + " 發(fā)送:" + message.getMessage());
// 消息存儲(chǔ)到數(shù)據(jù)庫
boolean save = privateMessageService.save(message);
//可以看出template最大的靈活就是我們可以獲取前端傳來的參數(shù)來指定訂閱地址, 前面參數(shù)是訂閱地址,后面參數(shù)是消息信息
template.convertAndSend("/topic/ServerToClient.private." + message.getToUsername(), message);
if(!save){
throw new SystemException(AppHttpCodeEnum.SYSTEM_ERROR);
}
}
}
WebSocketEventListener
也可以實(shí)現(xiàn)ApplicationListener<T>接口,泛型T,即為感興趣的事件類型。支持如下事件監(jiān)聽:文章來源:http://www.zghlxwxcb.cn/news/detail-661330.html
- SessionConnectedEvent
- SessionConnectEvent
- SessionDisconnectEvent
- SessionSubscribeEvent
- SessionUnsubscribeEvent
@Component
public class WebSocketEventListener {
@Autowired
private SimpMessageSendingOperations messagingTemplate;
public static AtomicInteger userNumber = new AtomicInteger(0);
@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
userNumber.incrementAndGet();
messagingTemplate.convertAndSend("/topic/ServerToClient.showUserNumber", userNumber);
System.out.println("我來了哦~");
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
userNumber.decrementAndGet();
messagingTemplate.convertAndSend("/topic/ServerToClient.showUserNumber", userNumber);
System.out.println("我走了哦~");
}
}
CorsFilter
@WebFilter
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(req, res);
}
}
package.json
{
"name": "timeless-chat-websocket-front",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.17.1",
"core-js": "^3.8.3",
"element-ui": "^2.15.3",
"net": "^1.0.2",
"nprogress": "^0.2.0",
"sockjs-client": "^1.6.1",
"stompjs": "^2.3.3",
"vue": "^2.6.14",
"vue-router": "^3.5.3"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Room.vue
<template>
<div>
<h3 style="text-align: center">當(dāng)前用戶:{{ this.username }}</h3>
<h3 style="text-align: center">在線人數(shù):{{ this.userNumber }}</h3>
<!-- <h3 style="text-align: center">在線用戶:-->
<!-- <div v-for="user in usernameOnlineList" :key="user">{{ user }}</div>-->
<!-- </h3>-->
<div class="container">
<div class="left">
<h2 style="text-align: center">用戶列表</h2>
<ul>
<li v-for="user in userList" :key="user.id" :class="{ selected: user.selected }" title="點(diǎn)擊選擇用戶聊天">
<div class="user-info">
<span @click="selectUser(user)">
{{ user.toUsername }}
</span>
<!-- <div class="button-container">
<el-button
v-if="user.isFriend === 0"
type="primary"
size="mini"
@click="sendFriendRequest(user)"
>
申請(qǐng)加好友
</el-button>
<el-button
v-if="user.isFriend === 1"
type="success"
@click="sendMessage(user)"
>
好友
</el-button>
<el-button v-if="user.isFriend === 2" type="danger" disabled>
申請(qǐng)中
</el-button>
</div> -->
</div>
</li>
</ul>
</div>
<div class="right">
<div v-if="selectedUser">
<h2 style="text-align: center">
正在與{{ selectedUser.toUsername }}聊天
</h2>
</div>
<div v-if="selectedUser">
<ul>
<li v-for="message in messageList[username + selectedUser.toUsername]" :key="message.id">
{{ message }}
</li>
</ul>
</div>
<div v-if="selectedUser">
<div class="message-input">
<el-input v-model="selectedUserMessage.message" placeholder="請(qǐng)輸入內(nèi)容" @keyup.enter.native="sendMsg"></el-input>
<div class="button-container">
<el-button type="primary" @click="sendMsg">發(fā)送消息</el-button>
<el-button type="danger" @click="deleteAllMsgs">刪除所有消息</el-button>
</div>
</div>
<div class="message-input">
<el-input v-model="broadcastMsgContent" placeholder="請(qǐng)輸入廣播消息內(nèi)容" @keyup.enter.native="sendMsg"></el-input>
<div class="button-container">
<el-button type="primary" @click="sendBroadcastMsg">發(fā)送廣播消息</el-button>
</div>
</div>
<div class="message-input">
<el-input v-model="toTopicMsgContent" placeholder="請(qǐng)輸入ToTopicMsg" @keyup.enter.native="sendMsg"></el-input>
<div class="button-container">
<el-button type="primary" @click="sendToTopicMsg">發(fā)送ToTopicMsg</el-button>
</div>
</div>
<div class="message-input">
<el-input v-model="greetingsMsgContent" placeholder="請(qǐng)輸入greetings消息" @keyup.enter.native="sendMsg"></el-input>
<div class="button-container">
<el-button type="primary" @click="sendGreetingsMsg">發(fā)送GreetingsMsg</el-button>
</div>
</div>
</div>
</div>
</div>
<div>
<h1 class="bottom" style="text-align: center">好友申請(qǐng)</h1>
<h2 style="text-align: center; color: rgb(57, 29, 216)">
功能開發(fā)中......
</h2>
</div>
</div>
</template>
<script>
import { getAllUsers, listPrivateMessages, deleteAllMsg } from "@/api";
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import { Message } from "element-ui";
export default {
name: "Room",
data() {
return {
userList: [],
groupList: [],
selectedUser: null,
message: "",
stompClient: null,
messageList: {}, // 使用對(duì)象來存儲(chǔ)每個(gè)用戶的聊天記錄
username: "",
usernameOnlineList: [],
userNumber: 1,
selectedUserMessage: {
user: null,
message: "",
},
broadcastMsgContent: '',
userMsgContent: '',
toTopicMsgContent: '',
greetingsMsgContent:'',
};
},
methods: {
listAllUsers() {
getAllUsers(this.username).then((response) => {
this.userNumber = ++response.data.userNumber;
this.userList = response.data.friends.filter(
(user) => user.toUsername !== this.username
);
});
},
selectUser(user) {
if (!this.messageList[this.username + user.toUsername]) {
console.log(2222222)
this.$set(this.messageList, this.username + user.toUsername, []);
}
// TODO 展示數(shù)據(jù)庫中存在的信息,也就是聊天記錄
listPrivateMessages(this.username, user.toUsername).then((response) => {
this.$set(this.messageList, this.username + user.toUsername, response.data);
});
this.selectedUser = user;
this.selectedUserMessage.user = user;
this.selectedUserMessage.message = ""; // 清空輸入框內(nèi)容
this.userList.forEach((u) => {
u.selected = false;
});
user.selected = true;
},
sendMsg() {
if (this.stompClient !== null && this.selectedUserMessage.message !== "") {
// 發(fā)送私聊消息給服務(wù)端
this.stompClient.send(
"/app/privateChat",
{},
JSON.stringify({
fromUsername: this.username,
message: this.selectedUserMessage.message,
toUsername: this.selectedUserMessage.user.toUsername,
})
);
this.messageList[this.username + this.selectedUserMessage.user.toUsername].push(
this.username + " 發(fā)送:" + this.selectedUserMessage.message
);
this.selectedUserMessage.message = ""; // 清空輸入框內(nèi)容
} else {
Message.info("請(qǐng)輸入消息");
}
},
sendBroadcastMsg() {
if (this.stompClient !== null) {
// 發(fā)送私聊消息給服務(wù)端
this.stompClient.send(
"/app/broadcastMsg",
{},
JSON.stringify({
fromUsername: this.username,
content: this.broadcastMsgContent
})
);
}
},
// 客戶端也可以發(fā)送 /topic/xx, 這樣訂閱了 /topic/xx的客戶端也會(huì)收到消息
sendToTopicMsg() {
if (this.stompClient !== null) {
// 發(fā)送私聊消息給服務(wù)端
this.stompClient.send(
"/topic/broadcastMsg",
{},
JSON.stringify({
fromUsername: this.username,
content: this.toTopicMsgContent
})
);
}
},
// 客戶端發(fā)送/app/greetings消息, 將會(huì)被@MessageMapping處理, 驗(yàn)證方法的返回值將會(huì)被發(fā)送到/topic/greetings主題
sendGreetingsMsg() {
if (this.stompClient !== null) {
// 發(fā)送私聊消息給服務(wù)端
this.stompClient.send(
"/app/greetings",
{},
JSON.stringify({
fromUsername: this.username,
content: this.greetingsMsgContent,
})
);
}
},
deleteAllMsgs() {
if (this.messageList[this.username + this.selectedUserMessage.user.toUsername] == "") {
Message.error("當(dāng)前沒有聊天記錄");
return;
}
deleteAllMsg(this.username, this.selectedUser.toUsername).then(
(response) => {
this.messageList[this.username + this.selectedUserMessage.user.toUsername] = [];
Message.success("刪除成功");
}
);
},
connect() {
//建立連接對(duì)象(還未發(fā)起連接)
const socket = new SockJS("/api/ws");
// 獲取 STOMP 子協(xié)議的客戶端對(duì)象
this.stompClient = Stomp.over(socket);
window.stompClient = this.stompClient
// 向服務(wù)器發(fā)起websocket連接并發(fā)送CONNECT幀
this.stompClient.connect(
{},
(frame) => { // 連接成功時(shí)(服務(wù)器響應(yīng) CONNECTED 幀)的回調(diào)方法
console.log("建立連接: " + frame);
// 訂閱當(dāng)前個(gè)人用戶消息
this.stompClient.subscribe(`/user/${this.username}/singleUserMsg`, (response) => {
console.log('收到當(dāng)前點(diǎn)對(duì)點(diǎn)用戶消息: ', response.body);
})
// 訂閱當(dāng)前個(gè)人用戶消息2
this.stompClient.subscribe(`/user/singleUserMsg`, (response) => {
console.log('收到當(dāng)前點(diǎn)對(duì)點(diǎn)用戶消息2: ', response.body);
})
// 訂閱廣播消息
this.stompClient.subscribe(`/topic/broadcastMsg`, (response) => {
console.log('收到廣播消息: ', response.body);
})
// 訂閱/topic/greetings消息
this.stompClient.subscribe(`/topic/greetings`, (response) => {
console.log('收到greetings消息: ', response.body);
})
// 訂閱 服務(wù)端發(fā)送給客戶端 的私聊消息
//(疑問: 訂閱的范圍如何限制?當(dāng)前用戶應(yīng)該不能訂閱別的用戶吧?
// 嘗試: 每進(jìn)來一個(gè)用戶動(dòng)態(tài)生成這個(gè)用戶對(duì)應(yīng)的一個(gè)標(biāo)識(shí), 然后, 這個(gè)用戶訂閱當(dāng)前這個(gè)標(biāo)識(shí),
// 其它沒用如果想發(fā)消息給這個(gè)用戶, 后臺(tái)先查詢這個(gè)用戶標(biāo)識(shí), 然后發(fā)消息給這個(gè)用戶,
// 這樣這個(gè)訂閱路徑就是動(dòng)態(tài)的, 其它不是好友的用戶就無法獲取到這個(gè)動(dòng)態(tài)生成的用戶標(biāo)識(shí)。)
this.stompClient.subscribe(
"/topic/ServerToClient.private." + this.username,
(result) => {
this.showContent(
JSON.parse(result.body).message,
JSON.parse(result.body).fromUsername,
JSON.parse(result.body).toUsername,
)
});
// 訂閱 服務(wù)端發(fā)送給客戶端刪除所有聊天內(nèi)容 的消息
this.stompClient.subscribe("/topic/ServerToClient.deleteMsg", (result) => {
const res = JSON.parse(result.body);
this.messageList[res.toUsername + res.fromUsername] = [];
});
// 訂閱 服務(wù)端發(fā)送給客戶端在線用戶數(shù)量 的消息
this.stompClient.subscribe("/topic/ServerToClient.showUserNumber", (result) => {
this.userNumber = result.body;
});
});
},
disconnect() {
if (this.stompClient !== null) {
// 斷開連接
this.stompClient.disconnect();
}
console.log("斷開連接...");
},
showContent(body, from, to) {
// 處理接收到的消息
// 示例代碼,根據(jù)實(shí)際需求進(jìn)行修改
if (!this.messageList[to + from]) {
this.$set(this.messageList, to + from, []); // 初始化選定用戶的聊天記錄數(shù)組
}
this.messageList[to + from].push(body); // 將接收到的消息添加到選定用戶的聊天記錄數(shù)組
},
},
created() {
},
mounted() {
// 從sessionStorage中獲取用戶名
this.username = sessionStorage.getItem("username");
console.log('username', this.username);
if (!this.username) {
this.$router.push('/login')
return
}
this.connect();
this.listAllUsers();
// console.log(this.username);
},
beforeDestroy() {
this.disconnect();
},
};
</script>
<style scoped>
.container {
display: flex;
justify-content: space-between;
margin: 10px;
}
.left,
.middle,
.right {
flex: 0.5;
margin: 5px;
padding: 10px;
background-color: lightgray;
}
.right {
flex: 2;
}
.bottom {
margin-top: 20px;
text-align: center;
}
li {
cursor: pointer;
transition: color 0.3s ease;
}
li:hover {
color: blue;
}
li.selected {
color: blue;
font-weight: bold;
}
.send-button {
display: flex;
justify-content: flex-end;
}
.message-input {
display: flex;
align-items: center;
}
.button-container {
margin-left: 10px;
/* 調(diào)整間距大小 */
}
.message-container {
display: flex;
justify-content: flex-end;
}
.button-container {
display: flex;
justify-content: flex-end;
}
.user-info {
display: flex;
align-items: center;
}
.button-container {
margin-left: auto;
}
</style>
示例2
引入依賴
<?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 http://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.6.4</version>
</parent>
<groupId>com.zzhua</groupId>
<artifactId>ws-demo2</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- RabbitMQ Starter Dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
</dependencies>
</project>
WebsocketMessageBrokerConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/websocket") // WebSocket握手端口
.addInterceptors(new HttpSessionHandshakeInterceptor())
.addInterceptors(new HandshakeInterceptor() {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 此處可作認(rèn)證, 因?yàn)橹挥形帐殖晒χ? 才會(huì)建立websocket連接
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
})
.setHandshakeHandler(new DefaultHandshakeHandler(){ // 設(shè)置默認(rèn)的握手處理器(可重寫其中的方法,比如determineUser)
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 此處可作認(rèn)證, 因?yàn)橹挥形帐殖晒χ? 才會(huì)建立websocket連接
// 這里可以返回Principal對(duì)象, Principal#
return super.determineUser(request, wsHandler, attributes);
}
})
.setAllowedOriginPatterns("*") // 設(shè)置跨域
.withSockJS(); // 開啟SockJS回退機(jī)制
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 攔截器配置
registration
.interceptors(new UserAuthenticationChannelInterceptor());
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
// 這里我們?cè)O(shè)置入站消息最大為8K
registry
.setMessageSizeLimit(8 * 1024);
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry
.setApplicationDestinationPrefixes("/app") // 發(fā)送到服務(wù)端目的地前綴
.enableSimpleBroker("/topic");// 開啟簡(jiǎn)單消息代理,指定消息訂閱前綴
}
}
StompAuthenticatedUser
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StompAuthenticatedUser implements Principal {
/**
* 用戶唯一ID
*/
private String userId;
/**
* 用戶昵稱
*/
private String nickName;
/**
* 用于指定用戶消息推送的標(biāo)識(shí)
* @return
*/
@Override
public String getName() {
return this.userId;
}
}
UserAuthenticationChannelInterceptor
@Slf4j
public class UserAuthenticationChannelInterceptor implements ChannelInterceptor {
private static final String USER_ID = "User-ID";
private static final String USER_NAME = "User-Name";
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
// 如果是連接請(qǐng)求,記錄userId
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 此處可以拿到stomp客戶端請(qǐng)求連接時(shí),傳入的請(qǐng)求頭
String userID = accessor.getFirstNativeHeader(USER_ID);
String username = accessor.getFirstNativeHeader(USER_NAME);
log.info("Stomp User-Related headers found, userID: {}, username:{}", userID, username);
/*
為什么保存認(rèn)證信息使用 setUser 方法?
- 該方法表示會(huì)話的擁有者,即存儲(chǔ)該會(huì)話擁有者信息。
每次建立連接都會(huì)創(chuàng)建一個(gè) WebSocketSession 會(huì)話信息類,在該會(huì)話進(jìn)行消息傳遞每次都會(huì)把 SessionId ,SessionAttributes 和
Principal(即我們setUser()保存的信息) 賦值到 Message 中,而 Principal 就是專門存儲(chǔ)身份認(rèn)證信息的。
- SessionId: 初始隨機(jī)分配的,用于確定唯一的會(huì)話
SessionAttributes: 用于給 WebSocketSession 設(shè)置一些額外記錄屬性,結(jié)構(gòu)是 Map
Principal: 用于設(shè)置 WebSocketSession 的身份認(rèn)證信息
*/
accessor.setUser(new StompAuthenticatedUser(userID, username));
// 如果沒有權(quán)限, 則直接在此處拋出異常即可
}
// 此處也可以根據(jù)stomp客戶端不同的命令作權(quán)限控制, 比如是否有權(quán)限訂閱某個(gè)路徑
return message;
}
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
log.info("ChannelInterceptor#postSend: {}", message);
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
log.info("ChannelInterceptor#afterSendCompletion: {}", message);
}
@Override
public boolean preReceive(MessageChannel channel) {
log.info("ChannelInterceptor#preReceive: {}", channel);
return true;
}
@Override
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
log.info("ChannelInterceptor#preReceive: {}", message);
return message;
}
@Override
public void afterReceiveCompletion(Message<?> message, MessageChannel channel, Exception ex) {
log.info("ChannelInterceptor#afterReceiveCompletion: {}", message);
}
}
StompSessionEventListener
@Slf4j
@Component
public class StompSessionEventListener implements ApplicationListener<AbstractSubProtocolEvent> {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@Override
public void onApplicationEvent(AbstractSubProtocolEvent event) {
log.debug("監(jiān)聽到事件: {}", event);
if (event instanceof SessionConnectEvent) {
handleSessionConnect((SessionConnectEvent)event);
} else if (event instanceof SessionConnectedEvent) {
handleSessionConnected((SessionConnectedEvent)event);
} else if (event instanceof SessionDisconnectEvent) {
handleSessionDisconnect((SessionDisconnectEvent)event);
} else if (event instanceof SessionSubscribeEvent) {
handleSessionSubscribe((SessionSubscribeEvent)event);
} else if (event instanceof SessionUnsubscribeEvent) {
handleSessionUnSubscribe((SessionUnsubscribeEvent)event);
}
}
private void handleSessionUnSubscribe(SessionUnsubscribeEvent event) {
log.info("{}取消訂閱{}", event.getUser(),StompHeaderAccessor.wrap(event.getMessage()).getDestination());
}
private void handleSessionSubscribe(SessionSubscribeEvent event) {
log.info("{}訂閱了{(lán)}", event.getUser(), StompHeaderAccessor.wrap(event.getMessage()).getDestination());
}
private void handleSessionDisconnect(SessionDisconnectEvent event) {
log.info("{}下線了", event.getUser());
simpMessagingTemplate.convertAndSend(
"/topic/chat/group",
new WebSocketMsgVO(event.getUser() + "下線了")
);
}
private void handleSessionConnected(SessionConnectedEvent event) {
log.info("{}上線了", event.getUser());
simpMessagingTemplate.convertAndSend(
"/topic/chat/group",
new WebSocketMsgVO(event.getUser() + "上線了")
);
}
private void handleSessionConnect(SessionConnectEvent event) {
log.info("{}請(qǐng)求建立stomp連接", event.getUser());
}
}
ChatController
@Slf4j
@Controller
@EnableScheduling
@RequiredArgsConstructor
public class ChatController {
private final SimpUserRegistry simpUserRegistry;
private final SimpMessagingTemplate simpMessagingTemplate;
@GetMapping("/page/chat")
public ModelAndView turnToChatPage() {
return new ModelAndView("chat");
}
/**
* 群聊消息處理
* 這里我們通過@SendTo注解指定消息目的地為"/topic/chat/group",如果不加該注解則會(huì)自動(dòng)發(fā)送到"/topic" + "/chat/group"
* @param webSocketMsgDTO 請(qǐng)求參數(shù),消息處理器會(huì)自動(dòng)將JSON字符串轉(zhuǎn)換為對(duì)象
* @return 消息內(nèi)容,方法返回值將會(huì)廣播給所有訂閱"/topic/chat/group"的客戶端
*/
@MessageMapping("/chat/group")
@SendTo("/topic/chat/group")
public WebSocketMsgVO groupChat(WebSocketMsgDTO webSocketMsgDTO) {
log.info("Group chat message received: {}", JSONObject.toJSONString(webSocketMsgDTO));
String content = String.format("來自[%s]的群聊消息: %s", webSocketMsgDTO.getName(), webSocketMsgDTO.getContent());
return WebSocketMsgVO.builder().content(content).build();
}
/**
* 私聊消息處理(客戶端向/app/chat/private發(fā)送消息, 會(huì)觸發(fā)該方法, 該方法的返回值將只會(huì)發(fā)送給當(dāng)前用戶自己)
* 這里我們通過@SendToUser注解指定消息目的地為"/topic/chat/private",發(fā)送目的地默認(rèn)會(huì)拼接上"/user/"前綴
* 實(shí)際發(fā)送目的地為"/user/topic/chat/private"
* @param webSocketMsgDTO 請(qǐng)求參數(shù),消息處理器會(huì)自動(dòng)將JSON字符串轉(zhuǎn)換為對(duì)象
* @return 消息內(nèi)容,方法返回值將會(huì)基于SessionID單播給指定用戶
*/
@MessageMapping("/chat/private")
@SendToUser("/topic/chat/private")
public WebSocketMsgVO privateChat(WebSocketMsgDTO webSocketMsgDTO,
Principal principal,
Message message,
MessageHeaderAccessor messageHeaderAccessor) {
log.info("Private chat message received: {}, ", JSONObject.toJSONString(webSocketMsgDTO), principal);
log.info("Private chat message principal:{}", principal);
log.info("Private chat message message:{}", message);
log.info("Private chat message messageHeaderAccessor:{}", messageHeaderAccessor);
StompHeaderAccessor stompHeaderAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (stompHeaderAccessor != null) {
log.info("Private chat message sessionId:{}", stompHeaderAccessor.getSessionId());
}
log.info("Private chat message sessionId:{}", StompHeaderAccessor.wrap(message).getSessionId());
String content = "私聊消息回復(fù):" + webSocketMsgDTO.getContent();
return WebSocketMsgVO.builder().content(content).build();
}
/**
* 后臺(tái)發(fā)送消息給指定的用戶
* 條件: 1. 客戶端訂閱stompClient.subscribe(`/user/topic/chat/toPrivate`,message=>{...})
* 2. 請(qǐng)求http://localhost:9090/toPrivateChat?userId=1693137128824&content=halo111接口即可
* (其中, userId為UserAuthenticationChannelInterceptor 在連接時(shí)記錄的userId)
*/
@GetMapping("/toPrivateChat")
@ResponseBody
public String toPrivateChat(String userId,String content) {
messagingTemplate.convertAndSendToUser(userId, "/topic/chat/toPrivate", content);
return "ok";
}
/**
* 發(fā)送(廣播)消息到/topic/chat/group
*/
@GetMapping("/toPublicChat")
@ResponseBody
public String toPublicChat(String content) {
messagingTemplate.convertAndSend("/topic/chat/group",new WebSocketMsgVO(content));
return "ok";
}
/**
* 定時(shí)消息推送,這里我們會(huì)列舉所有在線的用戶,然后單播給指定用戶。
* 通過SimpMessagingTemplate實(shí)例可以在任何地方推送消息。
*/
@Scheduled(fixedRate = 10 * 1000)
public void pushMessageAtFixedRate() {
log.info("當(dāng)前在線人數(shù): {}", simpUserRegistry.getUserCount());
if (simpUserRegistry.getUserCount() <= 0) {
return;
}
// 這里的Principal為StompAuthenticatedUser實(shí)例
Set<StompAuthenticatedUser> users = simpUserRegistry.getUsers().stream()
.map(simpUser -> StompAuthenticatedUser.class.cast(simpUser.getPrincipal()))
.collect(Collectors.toSet());
users.forEach(authenticatedUser -> {
String userId = authenticatedUser.getUserId();
String nickName = authenticatedUser.getNickName();
WebSocketMsgVO webSocketMsgVO = new WebSocketMsgVO();
webSocketMsgVO.setContent(String.format("定時(shí)推送的私聊消息, 接收人: %s, 時(shí)間: %s", nickName, LocalDateTime.now()));
log.info("開始推送消息給指定用戶, userId: {}, 消息內(nèi)容:{}", userId, JSONObject.toJSONString(webSocketMsgVO));
simpMessagingTemplate.convertAndSendToUser(userId, "/topic/chat/push", webSocketMsgVO);
});
}
}
WebSocketMsgDTO
@Data
public class WebSocketMsgDTO {
private String name;
private String content;
}
WebSocketMsgVO
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketMsgVO {
private String content;
}
chat.html
引入下面3個(gè)js文件(可以使用cdn,此處我把它們下載到了本地,放在了resources/static/js下)文章來源地址http://www.zghlxwxcb.cn/news/detail-661330.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>greeting</title>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js"></script>-->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>-->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>-->
<script src="/js/sockjs.min.js"></script>
<script src="/js/stomp.min.js"></script>
<script src="/js/jquery.min.js"></script>
<style>
#mainWrapper {
width: 600px;
margin: auto;
}
</style>
</head>
<body>
<div id="mainWrapper">
<div>
<label for="username" style="margin-right: 5px">姓名:</label><input id="username" type="text"/>
</div>
<div id="msgWrapper">
<p style="vertical-align: top">發(fā)送的消息:</p>
<textarea id="msgSent" style="width: 600px;height: 100px"></textarea>
<p style="vertical-align: top">收到的群聊消息:</p>
<textarea id="groupMsgReceived" style="width: 600px;height: 100px"></textarea>
<p style="vertical-align: top">收到的私聊消息:</p>
<textarea id="privateMsgReceived" style="width: 600px;height: 200px"></textarea>
</div>
<div style="margin-top: 5px;">
<button onclick="connect()">連接</button>
<button onclick="sendGroupMessage()">發(fā)送群聊消息</button>
<button onclick="sendPrivateMessage()">發(fā)送私聊消息</button>
<button onclick="disconnect()">斷開連接</button>
</div>
</div>
<script type="text/javascript">
$(() => {
$('#msgSent').val('');
$("#groupMsgReceived").val('');
$("#privateMsgReceived").val('');
});
let stompClient = null;
// 連接服務(wù)器
const connect = () => {
const header = {"User-ID": new Date().getTime().toString(), "User-Name": $('#username').val()};
const ws = new SockJS('http://localhost:8080/websocket');
stompClient = Stomp.over(ws);
// 連接時(shí), 傳入請(qǐng)求頭
stompClient.connect(header, () => subscribeTopic());
}
// 訂閱主題
const subscribeTopic = () => {
alert("連接成功!");
// 訂閱廣播消息
stompClient.subscribe('/topic/chat/group', function (message) {
console.log(`Group message received : ${message.body}`);
const resp = JSON.parse(message.body);
const previousMsg = $("#groupMsgReceived").val();
$("#groupMsgReceived").val(`${previousMsg}${resp.content}\n`);
}
);
// 訂閱單播消息
stompClient.subscribe('/user/topic/chat/private', message => {
console.log(`Private message received : ${message.body}`);
const resp = JSON.parse(message.body);
const previousMsg = $("#privateMsgReceived").val();
$("#privateMsgReceived").val(`${previousMsg}${resp.content}\n`);
}
);
// 訂閱定時(shí)推送的單播消息
stompClient.subscribe(`/user/topic/chat/push`, message => {
console.log(`Private message received : ${message.body}`);
const resp = JSON.parse(message.body);
const previousMsg = $("#privateMsgReceived").val();
$("#privateMsgReceived").val(`${previousMsg}${resp.content}\n`);
}
);
// 訂閱發(fā)送到當(dāng)前用戶的消息
stompClient.subscribe(`/user/topic/chat/toPrivate`, message => {
console.log(`ToPrivate message received : ${message.body}`);
console.log(message.body)
}
);
};
// 斷連
const disconnect = () => {
stompClient.disconnect(() => {
$("#msgReceived").val('Disconnected from WebSocket server');
});
}
// 發(fā)送群聊消息
const sendGroupMessage = () => {
const msg = {name: $('#username').val(), content: $('#msgSent').val()};
stompClient.send('/app/chat/group', {}, JSON.stringify(msg));
}
// 發(fā)送私聊消息
const sendPrivateMessage = () => {
const msg = {name: $('#username').val(), content: $('#msgSent').val()};
stompClient.send('/app/chat/private', {}, JSON.stringify(msg));
}
</script>
</body>
</html>
StompApp 啟動(dòng)類
@SpringBootApplication
public class StompApp {
public static void main(String[] args) {
SpringApplication.run(StompApp.class, args);
}
}
application.yml配置文件
server:
port: 8080
spring:
thymeleaf:
enabled: true
prefix: classpath:/templates/
cache: false
到了這里,關(guān)于websocket + stomp + sockjs學(xué)習(xí)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!