一、整体架构介绍
本示例实现了一个基于spring boot stomp协议的websocket服务,支持:
- 公共频道:无需认证,直接连接
- 私有频道:需要http会话认证
- 心跳机制:保持连接活跃
- 线程池优化:高性能消息处理
二、后端spring boot配置
1. 核心依赖 (pom.xml)
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-websocket</artifactid>
</dependency>
<!-- 安全认证(如需) -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-security</artifactid>
</dependency>
<!-- lombok -->
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<optional>true</optional>
</dependency>
</dependencies>
2. websocket主配置类
@configuration
@requiredargsconstructor
@enablewebsocketmessagebroker
public class websocketconfig implements websocketmessagebrokerconfigurer {
private final websockethandshakeinterceptor handshakeinterceptor;
private final websocketauthinterceptor authinterceptor;
/**
* 配置消息代理
*/
@override
public void configuremessagebroker(messagebrokerregistry config) {
// 启用简单内存消息代理,处理订阅前缀
config.enablesimplebroker("/public", "/private")
.setheartbeatvalue(new long[]{10000, 10000}); // 心跳:10秒
// 客户端发送消息的前缀
config.setapplicationdestinationprefixes("/hanhan");
// 用户目的地前缀(用于点对点消息)
config.setuserdestinationprefix("/user");
}
/**
* 配置入站通道(客户端→服务器)
*/
@override
public void configureclientinboundchannel(channelregistration registration) {
registration.taskexecutor()
.corepoolsize(10) // 核心线程数
.maxpoolsize(20) // 最大线程数
.queuecapacity(100) // 队列容量
.keepaliveseconds(60); // 线程空闲时间
registration.interceptors(authinterceptor); // 添加认证拦截器
}
/**
* 配置出站通道(服务器→客户端)
*/
@override
public void configureclientoutboundchannel(channelregistration registration) {
registration.taskexecutor()
.corepoolsize(10)
.maxpoolsize(20)
.queuecapacity(100)
.keepaliveseconds(60);
}
/**
* 注册stomp端点
*/
@override
public void registerstompendpoints(stompendpointregistry registry) {
registry.addendpoint("/ws/hanhan")
.addinterceptors(handshakeinterceptor) // 握手拦截器
.setallowedoriginpatterns(
"http://localhost:5173",
"https://hanhanys.cpolar.cn"
)
.withsockjs() // 支持sockjs降级
.setdisconnectdelay(30 * 1000); // 断开延迟30秒
}
}
3. 握手拦截器(handshakeinterceptor)
@slf4j
@component
public class websockethandshakeinterceptor implements handshakeinterceptor {
private static final string connect_ws_prefix = "/ws/hanhan";
private static final string public_ws_prefix = "/ws/public/";
private static final string private_ws_prefix = "/ws/private/";
@override
public boolean beforehandshake(serverhttprequest request,
serverhttpresponse response,
websockethandler wshandler,
map<string, object> attributes) {
try {
if (!(request instanceof servletserverhttprequest)) {
log.warn("非servlet请求,拒绝握手");
return false;
}
string requestpath = request.geturi().getpath();
log.info("websocket握手请求路径: {}", requestpath);
// 1. 处理公共连接端点(/ws/hanhan)
if (requestpath.startswith(connect_ws_prefix)) {
string channelid = requestpath.substring(connect_ws_prefix.length());
attributes.put("channelid", channelid);
attributes.put("channeltype", "public");
attributes.put("authentication", null);
log.info("公共连接端点: channelid={}", channelid);
return true;
}
// 2. 处理公共频道(/ws/public/{channelid})
if (requestpath.startswith(public_ws_prefix)) {
string channelid = requestpath.substring(public_ws_prefix.length());
attributes.put("channelid", channelid);
attributes.put("channeltype", "public");
attributes.put("authentication", null);
log.info("公共频道连接: channelid={}", channelid);
return true;
}
// 3. 处理私有频道(/ws/private/{channelid})需要认证
if (requestpath.startswith(private_ws_prefix)) {
servletserverhttprequest servletrequest = (servletserverhttprequest) request;
httpservletrequest httprequest = servletrequest.getservletrequest();
// 从现有http会话获取认证信息
httpsession httpsession = httprequest.getsession(false);
if (httpsession == null) {
log.warn("私有频道需要认证,但无http会话");
return false;
}
authentication auth = (authentication) httpsession.getattribute("authentication");
if (auth == null || !auth.isauthenticated()) {
log.warn("用户未认证或认证已过期");
return false;
}
string channelid = requestpath.substring(private_ws_prefix.length());
attributes.put("channelid", channelid);
attributes.put("channeltype", "private");
attributes.put("authentication", auth);
log.info("私有频道握手成功: 用户={}, channelid={}",
auth.getname(), channelid);
return true;
}
log.warn("未知的websocket路径: {}", requestpath);
return false;
} catch (exception e) {
log.error("握手过程异常: {}", e.getmessage(), e);
return false;
}
}
@override
public void afterhandshake(serverhttprequest request,
serverhttpresponse response,
websockethandler wshandler,
exception exception) {
// 握手后处理,可记录日志等
if (exception != null) {
log.error("握手后处理异常: {}", exception.getmessage());
}
}
}
4. 认证拦截器(channelinterceptor)
@slf4j
@component
public class websocketauthinterceptor implements channelinterceptor {
@override
public message<?> presend(message<?> message, messagechannel channel) {
stompheaderaccessor accessor = messageheaderaccessor.getaccessor(
message, stompheaderaccessor.class
);
if (accessor == null) {
return message;
}
// 处理connect命令(连接建立)
if (stompcommand.connect.equals(accessor.getcommand())) {
map<string, object> sessionattrs = accessor.getsessionattributes();
if (sessionattrs == null) {
log.warn("websocket连接被拒绝:无session属性");
return null; // 拒绝连接
}
// 检查是否为公共频道
if ("public".equals(sessionattrs.get("channeltype"))) {
string channelid = (string) sessionattrs.get("channelid");
log.info("公共频道连接成功: channelid={}", channelid);
return message;
}
// 检查私有频道的认证信息
authentication auth = (authentication) sessionattrs.get("authentication");
if (auth != null && auth.isauthenticated()) {
// 设置安全上下文
securitycontextholder.getcontext().setauthentication(auth);
// 设置stomp用户
accessor.setuser(auth);
log.info("私有频道连接成功: 用户={}", auth.getname());
return message;
}
log.warn("websocket连接被拒绝:认证失败");
return null; // 拒绝连接
}
return message;
}
}
5. 消息控制器示例
@controller
@slf4j
public class websocketcontroller {
/**
* 处理发送到 /hanhan/message 的消息
*/
@messagemapping("/message")
@sendto("/public/chat") // 广播到所有订阅者
public chatmessage handlemessage(chatmessage message) {
log.info("收到消息: {}", message);
message.settimestamp(new date());
return message;
}
/**
* 点对点消息
*/
@messagemapping("/private")
public void sendprivatemessage(@payload privatemessage message,
@header("simpsessionid") string sessionid) {
log.info("私密消息: from={}, to={}", message.getfrom(), message.getto());
// 通过消息模板发送给特定用户
simpmessagingtemplate.convertandsendtouser(
message.getto(),
"/queue/private",
message
);
}
}
@data
class chatmessage {
private string from;
private string content;
private date timestamp;
}
@data
class privatemessage {
private string from;
private string to;
private string content;
}
三、前端实现(vue 3 + sockjs + stomp)
1. 安装依赖
npm install sockjs-client @stomp/stompjs 或 yarn add sockjs-client @stomp/stompjs
2. websocket工具类
// utils/websocket.js
import sockjs from 'sockjs-client';
import { client } from '@stomp/stompjs';
class websocketservice {
constructor() {
this.stompclient = null;
this.subscriptions = new map(); // 保存订阅引用
this.reconnectattempts = 0;
this.maxreconnectattempts = 5;
}
/**
* 连接公共频道(无需认证)
* @param {string} channelid - 频道id
* @param {function} onmessage - 消息回调
*/
connecttopublicchannel(channelid, onmessage) {
const socketurl = 'http://localhost:8080/ws/hanhan';
const socket = new sockjs(socketurl);
this.stompclient = new client({
websocketfactory: () => socket,
reconnectdelay: 5000,
heartbeatincoming: 10000,
heartbeatoutgoing: 10000,
onconnect: (frame) => {
console.log('公共频道连接成功', frame);
// 订阅公共频道
const subscription = this.stompclient.subscribe(
`/public/${channelid}`,
(message) => {
const parsed = json.parse(message.body);
onmessage(parsed);
}
);
this.subscriptions.set(`public_${channelid}`, subscription);
},
onstomperror: (frame) => {
console.error('stomp协议错误:', frame);
},
onwebsocketerror: (event) => {
console.error('websocket连接错误:', event);
this.handlereconnect();
}
});
this.stompclient.activate();
}
/**
* 连接私有频道(需要认证)
* @param {string} channelid - 频道id
* @param {string} token - 认证令牌
* @param {function} onmessage - 消息回调
*/
connecttoprivatechannel(channelid, token, onmessage) {
const socketurl = `http://localhost:8080/ws/private/${channelid}`;
// 添加认证头
const socket = new sockjs(socketurl, null, {
headers: {
'authorization': `bearer ${token}`
}
});
this.stompclient = new client({
websocketfactory: () => socket,
reconnectdelay: 5000,
heartbeatincoming: 10000,
heartbeatoutgoing: 10000,
onconnect: (frame) => {
console.log('私有频道连接成功', frame);
// 订阅私有频道
const subscription = this.stompclient.subscribe(
`/private/${channelid}`,
(message) => {
const parsed = json.parse(message.body);
onmessage(parsed);
}
);
// 订阅用户专属队列(点对点消息)
const usersubscription = this.stompclient.subscribe(
`/user/queue/private`,
(message) => {
const parsed = json.parse(message.body);
console.log('收到私信:', parsed);
}
);
this.subscriptions.set(`private_${channelid}`, subscription);
this.subscriptions.set('user_queue', usersubscription);
},
ondisconnect: () => {
console.log('websocket连接断开');
}
});
this.stompclient.activate();
}
/**
* 发送消息到服务器
* @param {string} destination - 目标地址
* @param {object} payload - 消息内容
*/
sendmessage(destination, payload) {
if (this.stompclient && this.stompclient.connected) {
this.stompclient.publish({
destination: `/hanhan${destination}`,
body: json.stringify(payload)
});
} else {
console.warn('websocket未连接,无法发送消息');
}
}
/**
* 发送私信
* @param {string} touser - 目标用户
* @param {string} content - 消息内容
*/
sendprivatemessage(touser, content) {
this.sendmessage('/private', {
from: 'currentuser',
to: touser,
content: content,
timestamp: new date().toisostring()
});
}
/**
* 取消订阅
* @param {string} subscriptionkey - 订阅键值
*/
unsubscribe(subscriptionkey) {
const subscription = this.subscriptions.get(subscriptionkey);
if (subscription) {
subscription.unsubscribe();
this.subscriptions.delete(subscriptionkey);
}
}
/**
* 断开连接
*/
disconnect() {
if (this.stompclient) {
// 取消所有订阅
this.subscriptions.foreach(sub => sub.unsubscribe());
this.subscriptions.clear();
this.stompclient.deactivate();
this.stompclient = null;
console.log('websocket已断开连接');
}
}
/**
* 重连处理
*/
handlereconnect() {
if (this.reconnectattempts < this.maxreconnectattempts) {
this.reconnectattempts++;
console.log(`尝试重连 (${this.reconnectattempts}/${this.maxreconnectattempts})`);
settimeout(() => {
if (this.stompclient) {
this.stompclient.activate();
}
}, 3000);
}
}
}
export default new websocketservice();
3. vue组件使用示例
<template>
<div class="chat-room">
<div :class="['status', { connected: isconnected }]">
{{ isconnected ? '已连接' : '未连接' }}
</div>
<!-- 频道选择 -->
<div class="channel-selector">
<button @click="connectpublic('general')">连接公共聊天室</button>
<button @click="connectprivate('private-room', usertoken)">
连接私有房间
</button>
<button @click="disconnect" :disabled="!isconnected">断开连接</button>
</div>
<!-- 消息列表 -->
<div class="message-list">
<div v-for="(msg, index) in messages" :key="index" class="message">
<span class="sender">{{ msg.from }}:</span>
<span class="content">{{ msg.content }}</span>
<span class="time">{{ formattime(msg.timestamp) }}</span>
</div>
</div>
<!-- 消息发送 -->
<div class="message-input">
<input
v-model="inputmessage"
@keyup.enter="sendmessage"
placeholder="输入消息..."
:disabled="!isconnected"
/>
<button @click="sendmessage" :disabled="!isconnected">发送</button>
</div>
<!-- 私信发送 -->
<div class="private-message">
<input v-model="privateto" placeholder="目标用户" />
<input v-model="privatecontent" placeholder="私信内容" />
<button @click="sendprivate">发送私信</button>
</div>
</div>
</template>
<script setup>
import { ref, onmounted, onunmounted } from 'vue';
import websocket from '@/utils/websocket';
const isconnected = ref(false);
const messages = ref([]);
const inputmessage = ref('');
const privateto = ref('');
const privatecontent = ref('');
const usertoken = ref('your-auth-token'); // 从登录状态获取
// 连接公共频道
const connectpublic = (channelid) => {
websocket.connecttopublicchannel(channelid, (message) => {
messages.value.push(message);
console.log('收到公共消息:', message);
});
isconnected.value = true;
};
// 连接私有频道
const connectprivate = (channelid, token) => {
websocket.connecttoprivatechannel(channelid, token, (message) => {
messages.value.push(message);
console.log('收到私有消息:', message);
});
isconnected.value = true;
};
// 发送公共消息
const sendmessage = () => {
if (!inputmessage.value.trim()) return;
const message = {
from: '当前用户',
content: inputmessage.value,
type: 'public'
};
websocket.sendmessage('/message', message);
inputmessage.value = '';
};
// 发送私信
const sendprivate = () => {
if (!privateto.value || !privatecontent.value) return;
websocket.sendprivatemessage(privateto.value, privatecontent.value);
privatecontent.value = '';
};
// 断开连接
const disconnect = () => {
websocket.disconnect();
isconnected.value = false;
messages.value = [];
};
// 格式化时间
const formattime = (timestamp) => {
return new date(timestamp).tolocaletimestring();
};
// 组件生命周期
onmounted(() => {
console.log('聊天室组件已加载');
});
onunmounted(() => {
disconnect();
});
</script>
<style scoped>
.chat-room {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.status {
padding: 8px 16px;
border-radius: 4px;
margin-bottom: 20px;
background: #ff6b6b;
color: white;
}
.status.connected {
background: #51cf66;
}
.channel-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.channel-selector button {
padding: 10px 20px;
background: #339af0;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.channel-selector button:disabled {
background: #ccc;
cursor: not-allowed;
}
.message-list {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 20px;
}
.message {
padding: 8px;
border-bottom: 1px solid #eee;
}
.message .sender {
font-weight: bold;
margin-right: 10px;
color: #339af0;
}
.message .time {
float: right;
color: #999;
font-size: 0.8em;
}
.message-input, .private-message {
display: flex;
gap: 10px;
margin-top: 10px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #51cf66;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
4. 主应用入口
// main.js
import { createapp } from 'vue';
import app from './app.vue';
// 全局挂载websocket服务
const app = createapp(app);
// 可选:提供全局websocket实例
app.config.globalproperties.$websocket = websocket;
app.mount('#app');总结
到此这篇关于spring boot+stomp+sockjs+vue实现websocket连接的文章就介绍到这了,更多相关springboot连接websocket服务内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论