目录
1.2.添加session会话管理WebSocketSessionHolder
1.3.添加建立session会话连接到会话管理及监听下线MapSessionWebSocketHandlerDecorator
1.4.添加断开session会话连接前请求处理UserAttributeHandshakeInterceptor
1.6xxljob定时任务推送后展示的消息(同一用户同一科室不同电脑展示)
2.3接收到消息处理方法(这里不唯一根据自己业务逻辑来处理)
1.后端:
1.1引入依赖
com.pig4cloud.plugin
websocket-spring-boot-starter
${websocket-spring-boot-starter.version}
1.2.添加session会话管理WebSocketSessionHolder
package com.pig4cloud.plugin.websocket.holder;
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public final class WebSocketSessionHolder {
private static final Map USER_SESSION_MAP = new ConcurrentHashMap();
private WebSocketSessionHolder() {
}
public static void addSession(Object sessionKey, String sessionId, WebSocketSession session) {
USER_SESSION_MAP.put(sessionKey.toString() + ":" + sessionId, session);
}
public static void removeSession(Object sessionKey, String sessionId) {
USER_SESSION_MAP.remove(sessionKey.toString() + ":" + sessionId);
}
public static List getSession(Object sessionKey) {
return getMatchingKeys(USER_SESSION_MAP, sessionKey + ":");
}
public static Collection getSessions() {
return USER_SESSION_MAP.values();
}
public static Set getSessionKeys() {
return USER_SESSION_MAP.keySet();
}
/**
* 获取匹配的keys *
* 因当前系统允许多地同账号登录 所以需要兼容多地登录同账号都能接受到消息 *
*
* @param map USER_SESSION_MAP
* @param keyword addSession时是拼接的sessionKey + ":" + sessionId
* @return List
*/
public static List getMatchingKeys(Map map, String keyword) {
return map.keySet().stream()
.filter(key -> key.contains(keyword)) // 你可以使用 contains, startsWith, endsWith 等
.map(map::get).collect(Collectors.toList());
}
}
map.keySet().stream()
.filter(key -> key.contains(keyword)) // 你可以使用 contains, startsWith, endsWith 等
.map(map::get).collect(Collectors.toList());.map(map::get)用到了lmabda表达式的方法引用,就是在拿参数做操作,
map::get相当于写法val -> map.get(val)
方法引用事例:
package MethodReference02;
/*
需求:
1、定义一个接口(Printable):里面定义一个抽象方法:void printInt(int i);
2、定义一个测试类(PrintableDemo),在测试类中提供两个方法
一个方法是:usePrintable(Printable p)
一个方法是主方法,在主方法中调用usePrintable方法
*/
public class PrintableDemo {
public static void main(String[] args) {
usePrintable((int i) -> {
System.out.println(i);
});
usePrintable(i -> System.out.println(i));
usePrintable(System.out::println);
}
private static void usePrintable(Printable p) {
p.printInt(12345);
}
}
1.3.添加建立session会话连接到会话管理及监听下线MapSessionWebSocketHandlerDecorator
package com.pig4cloud.plugin.websocket.holder;
import com.tbyf.common.security.service.TokenService;
import com.tbyf.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
import java.util.Objects;
/**
* 系统允许一个账号多地登录 *
* 为了适应同时登录都可以收到消息 重写添加sessionId *
*/
@Slf4j
public class MapSessionWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
@Autowired
private TokenService tokenService;
private final SessionKeyGenerator sessionKeyGenerator;
public MapSessionWebSocketHandlerDecorator(WebSocketHandler delegate, SessionKeyGenerator sessionKeyGenerator) {
super(delegate);
this.sessionKeyGenerator = sessionKeyGenerator;
}
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
Object sessionKey = this.sessionKeyGenerator.sessionKey(session);
WebSocketSessionHolder.addSession(sessionKey, session.getId(), session);
}
public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
Map queryParams = UriComponentsBuilder.fromUri(Objects.requireNonNull(session.getUri()))
.build()
.getQueryParams()
.toSingleValueMap();
LoginUser loginUser = tokenService.getLoginUser(queryParams);
log.warn("用户名:{}下线了, webSocket token is {}", loginUser.getUsername(), loginUser.getToken());
Object sessionKey = this.sessionKeyGenerator.sessionKey(session);
WebSocketSessionHolder.removeSession(sessionKey, session.getId());
}
}
1.4.添加断开session会话连接前请求处理UserAttributeHandshakeInterceptor
package com.pig4cloud.plugin.websocket.custom;
import com.tbyf.common.core.exception.ServiceException;
import com.tbyf.common.security.service.TokenService;
import com.tbyf.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Map;
@Slf4j
public class UserAttributeHandshakeInterceptor implements HandshakeInterceptor {
@Autowired
private TokenService tokenService;
public UserAttributeHandshakeInterceptor() {
}
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
// 获取请求的 URI 并解析查询参数
Map queryParams = UriComponentsBuilder.fromUri(request.getURI())
.build()
.getQueryParams()
.toSingleValueMap();
LoginUser loginUser = tokenService.getLoginUser(queryParams);
if(loginUser == null){
throw new ServiceException("登录状态已过期");
}
attributes.put("USER_KEY_ATTR_NAME", loginUser.getUsername());
log.warn("用户名:{}上线了, webSocket token is {}", loginUser.getUsername(), loginUser.getToken());
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
}
1.5.后端消息发送
package com.tbyf.system.controller;
import com.alibaba.fastjson2.JSONObject;
import com.pig4cloud.plugin.websocket.config.WebSocketMessageSender;
import com.pig4cloud.plugin.websocket.holder.WebSocketSessionHolder;
import com.tbyf.common.core.exception.ServiceException;
import com.tbyf.common.core.utils.StringUtils;
import com.tbyf.common.core.web.domain.AjaxResult;
import com.tbyf.common.security.annotation.InnerAuth;
import com.tbyf.system.api.model.LoginUser;
import com.tbyf.system.api.model.MessageModel;
import com.tbyf.system.service.ISysUserOnlineService;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
/**
*
* 发送消息
*
*
* @author Lisy
* @since 2020-05-29
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/msg")
public class MessageSendController {
@Autowired
private ISysUserOnlineService userOnlineService;
/**
* 通知消息
*/
@InnerAuth
@ApiOperation("通知消息")
@PostMapping("/sendMsg")
public AjaxResult sendMsg(@RequestBody MessageModel messageModel) {
if (StringUtils.isEmpty(messageModel.getUserName())) {
throw new ServiceException("用户名不能为空");
}
this.sendMsg(messageModel.getUserName(), JSONObject.toJSONString(messageModel));
return AjaxResult.success(Boolean.TRUE);
}
/**
* 通知消息
*/
@InnerAuth
@ApiOperation("向所有的session发送消息")
@PostMapping("/sendAllSessionMsg")
public AjaxResult sendAllSessionMsg() {
String msg = "这是测试消息";
Collection sessions = WebSocketSessionHolder.getSessions();
if (CollectionUtils.isEmpty(sessions)) {
log.error("当前没有用户连接websocket");
throw new ServiceException("当前没有用户连接websocket");
}
for (WebSocketSession session : sessions) {
Object userName = session.getAttributes().get("USER_KEY_ATTR_NAME");
if (session == null) {
log.error("[send] session 为 null");
} else if (!session.isOpen()) {
log.error("[send] session 已经关闭");
} else {
try {
session.sendMessage(new TextMessage(msg));
log.error("[send] user({}) 发送消息({})成功", new Object[]{userName, msg});
} catch (IOException var3) {
log.error("[send] user({}) 发送消息({}) 异常", new Object[]{userName, msg, var3});
}
}
}
return AjaxResult.success(Boolean.TRUE);
}
// (9.0使用的这个发送消息)
private void sendMsg(String userName, String message) {
boolean send = WebSocketMessageSender.send(userName, message);
if (send) {
log.error("发送消息成功!接收人:{}", userName);
} else {
log.error("发送消息失败!接收人:{}", userName);
}
}
/**
* 向所有符合科室的session发送消息(9.0使用的这个发送消息)
*/
@InnerAuth
@ApiOperation("向所有符合科室的session发送消息")
@PostMapping("/sendAllDeptMsg")
public AjaxResult sendAllDeptMsg(@RequestBody MessageModel messageModel) {
if (StringUtils.isEmpty(messageModel.getOrgCode())) {
throw new ServiceException("机构编码不能为空");
}
List userList = userOnlineService.onlineUser(messageModel.getOrgCode());
userList.stream().filter(user -> StringUtils.equals(user.getDeptCode(), messageModel.getDeptCode()))
.forEach(user -> {
String userName = user.getUsername();
sendMsg(userName, JSONObject.toJSONString(messageModel));
});
return AjaxResult.success(Boolean.TRUE);
}
}
// (9.0使用的这个发送消息)
private void sendMsg(String userName, String message) {boolean send = WebSocketMessageSender.send(userName, message);
if (send) {
log.error("发送消息成功!接收人:{}", userName);
} else {
log.error("发送消息失败!接收人:{}", userName);
}
}
/**
* 向所有符合科室的session发送消息(9.0使用的这个发送消息)
*/
@InnerAuth
@ApiOperation("向所有符合科室的session发送消息")
@PostMapping("/sendAllDeptMsg")
public AjaxResult sendAllDeptMsg(@RequestBody MessageModel messageModel) {
if (StringUtils.isEmpty(messageModel.getOrgCode())) {
throw new ServiceException("机构编码不能为空");
}
List<LoginUser> userList = userOnlineService.onlineUser(messageModel.getOrgCode());
userList.stream().filter(user -> StringUtils.equals(user.getDeptCode(), messageModel.getDeptCode()))
.forEach(user -> {
String userName = user.getUsername();
sendMsg(userName, JSONObject.toJSONString(messageModel));
});
return AjaxResult.success(Boolean.TRUE);
}
}
package com.pig4cloud.plugin.websocket.config;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pig4cloud.plugin.websocket.holder.WebSocketSessionHolder;
import com.pig4cloud.plugin.websocket.message.JsonWebSocketMessage;
import com.pig4cloud.plugin.websocket.util.SpringBeanContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class WebSocketMessageSender {
private static final Logger log = LoggerFactory.getLogger(WebSocketMessageSender.class);
public WebSocketMessageSender() {
}
public static void broadcast(String message) {
Collection sessions = WebSocketSessionHolder.getSessions();
Iterator var2 = sessions.iterator();
while (var2.hasNext()) {
WebSocketSession session = (WebSocketSession) var2.next();
send(session, message);
}
}
// 9.0 使用的这个推送消息
public static boolean send(Object sessionKey, String message) {
List sessions = WebSocketSessionHolder.getSession(sessionKey);
if (CollectionUtils.isEmpty(sessions)) {
log.info("[send] 当前 sessionKey:{} 对应 session 不在本服务中", sessionKey);
return false;
} else {
sessions.forEach(session -> send(session, message));
return Boolean.TRUE;
}
}
public static void send(WebSocketSession session, JsonWebSocketMessage message) {
ObjectMapper mapper = (ObjectMapper) SpringBeanContextHolder.getBean(ObjectMapper.class);
try {
send(session, mapper.writeValueAsString(message));
} catch (JsonProcessingException var4) {
throw new RuntimeException(var4);
}
}
public static boolean send(WebSocketSession session, String message) {
if (session == null) {
log.error("[send] session 为 null");
return false;
} else if (!session.isOpen()) {
log.error("[send] session 已经关闭");
return false;
} else {
try {
session.sendMessage(new TextMessage(message));
return true;
} catch (IOException var3) {
log.error("[send] session({}) 发送消息({}) 异常", new Object[]{session, message, var3});
return false;
}
}
}
}
9.0使用的这个方法推送消息
send(Object sessionKey, String message) {}
package com.tbyf.system.controller;
import com.pig4cloud.plugin.websocket.config.WebSocketMessageSender;
import com.pig4cloud.plugin.websocket.holder.WebSocketSessionHolder;
import com.tbyf.common.core.exception.ServiceException;
import com.tbyf.common.core.web.domain.AjaxResult;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Collection;
/**
*
* 发送任务消息
*
*
* @author Lisy
* @since 2020-05-29
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/msg")
public class MessageSendController {
/**
* 通知消息
*/
@ApiOperation("通知消息")
@RequestMapping("/sendMsg")
public AjaxResult sendMedicine() {
this.sendMsg("SYS", "hello world");
return AjaxResult.success();
}
/**
* 通知消息
*/
@ApiOperation("向所有的session发送消息")
@PostMapping("/sendAllSessionMsg")
public AjaxResult sendAllSessionMsg() {
String msg = "这是测试消息";
Collection sessions = WebSocketSessionHolder.getSessions();
if (CollectionUtils.isEmpty(sessions)) {
log.error("当前没有用户连接websocket");
throw new ServiceException("当前没有用户连接websocket");
}
for (WebSocketSession session : sessions) {
Object userName = session.getAttributes().get("USER_KEY_ATTR_NAME");
if (session == null) {
log.error("[send] session 为 null");
} else if (!session.isOpen()) {
log.error("[send] session 已经关闭");
} else {
try {
session.sendMessage(new TextMessage(msg));
log.error("[send] user({}) 发送消息({})成功", new Object[]{userName, msg});
} catch (IOException var3) {
log.error("[send] user({}) 发送消息({}) 异常", new Object[]{userName, msg, var3});
}
}
}
return AjaxResult.success();
}
private void sendMsg(String userName, String message) {
boolean send = WebSocketMessageSender.send(userName, message);
if (send) {
log.error("发送消息成功!接收人:{}", userName);
} else {
log.error("发送消息失败!接收人:{}", userName);
}
}
}
流程:
1.启动时会调用
MapSessionWebSocketHandlerDecorator的构造器
2.建立websocket连接时会调用
MapSessionWebSocketHandlerDecorator.afterConnectionEstablished方法

存方到自定义的USER_SESSION_MAP集合中
参数1:sessionKey 就是用户名,



参数2:sessionId就是SESSION的id用于区分同一用户账号多地登录它的sessionKey 是一样的(比如同一个SYS账号多台电脑登录),用于同一用户同一科室但是不同电脑登录

参数3:session信息

模拟同一用户不同电脑同一科室登录


1.6xxljob定时任务推送后展示的消息(同一用户同一科室不同电脑展示)


1.7引入XXLJOB流程
1.7.1引入xxl job依赖
com.xuxueli
xxl-job-core
2.3.0
1.7.2application.yml中添加
xxl:
job:
admin:
addresses: http://your ip:8080/xxl-job-admin/ #xxl job管理界面的ip
executor:
appname: his9-xxl-job #xxl job中执行器要配置的AppName,要与这里一致
address:
ip:
port: 8998
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30
accessToken: default_token
1.7.3xxl job配置说明



2.前端:
2.1在你想要建立连接的页面created中添加连接
created() {
//连接Websocket
var domain = window.location.host;
//domain = '192.168.60.45:19100';
var WSS_URL = `ws://${domain}/system/ws/info?access_token=${getToken()}`
console.log("WSS_URL",WSS_URL)
setURL(WSS_URL)
//websocket连接失败导致node崩溃解决方案:
//https://blog.csdn.net/qq_36577699/article/details/130559531
createSocket()
window.addEventListener('onmessageWS', this.handleMessage)
this.$bus.$on("onmessageWS", this.handleMessage)
}
2.2websocket.js处理连接及断开重连
import {getToken} from '@/utils/auth'
import eventBus from '@/utils/eventBus.js'
let Socket = ''
var timer = null
var WSS_URL = null
// 建立连接
export function setURL(url) {
WSS_URL = url
}
export function createSocket() {
var domain = window.location.host;
//console.log(WSS_URL)
if (!Socket) {
Socket = new WebSocket(WSS_URL)
// const WebSocketProxy = new Proxy(WebSocket, {
// construct: function(target, arg) {
// try {
// return new target(...arg)
// } catch (error) {
// return error
// }
// }
// })
// Socket = new WebSocketProxy(WSS_URL);
Socket.onopen = onopenWS
Socket.onmessage = onmessageWS
Socket.onerror = onerrorWS
Socket.onclose = oncloseWS
} else {
console.log('websocket已连接')
}
}
// 打开WS之后发送心跳
export function onopenWS() {
console.log('Websocket connect success')
timer = setInterval(() => {
try {
Socket.send("/test");
} catch (err) {
console.log("断开了:" + err);
createSocket();
}
}, 5 * 60 * 1000);
}
// 连接失败重连
export function onerrorWS(e) {
console.log(e)
Socket.close()
// createSocket() //重连
}
// WS数据接收统一处理
export function onmessageWS(message) {
eventBus.$emit('onmessageWS', message)
}
/**发送数据
1. @param eventType
*/
export function sendWSPush(eventTypeArr) {
const obj = {
appId: 'airShip',
cover: 0,
event: eventTypeArr
}
if (Socket !== null && Socket.readyState === 3) {
Socket.close()
createSocket() //重连
} else if (Socket.readyState === 1) {
Socket.send(JSON.stringify(obj))
} else if (Socket.readyState === 0) {
setTimeout(() => {
Socket.send(JSON.stringify(obj))
}, 3000)
}
}
export function oncloseWS(e) {
clearInterval(timer)
console.log('websocket已断开', e)
}
2.3接收到消息处理方法(这里不唯一根据自己业务逻辑来处理)
handleMessage(message) {
var deptCode = this.$store.state.user.deptCode
var orgCode = this.$store.state.user.orgCode
var userName = this.$store.state.user.userInfo.userName
try {
let info = JSON.parse(message.data)
let infoList = JSON.parse(info.msg)
//消息面向机构进行筛选
if (info.orgCode != orgCode) return
//消息面向科室进行筛选
if (info.type == 'dept') {
if (info.deptCode == deptCode) {
if (info.windowType == 'nurse') this.nurerDeptList = infoList
if (info.windowType == 'doctor') {
this.doctorDeptList = infoList.map(item => {
return {
...item,
TYPE: 'dept'
}
})
}
}
}
//消息面向个人进行筛选
else if (info.type == 'ry') {
if (info.userName == userName) {
if (info.windowType == 'nurse') this.nurseUserList = infoList
if (info.windowType == 'doctor') {
this.doctorUserList = infoList.map(item => {
return {
...item,
TYPE: 'ry'
}
})
}
}
}
if (info.windowType == 'nurse') {
// console.log(this.nurerDeptList)
this.nurseList = this.nurseUserList.concat(this.nurerDeptList)
// console.log(this.nurseList)
} else if (info.windowType == 'doctor') {
this.doctorList = this.doctorUserList.concat(this.doctorDeptList)
}
} catch (e) {
// console.log('error:', e)
// console.log(message)
}
// this.$nextTick(() => {
// document.querySelector('.black-red').style.animation = 'none';
// })
},
3.后续遇到的问题及解决办法
3.1由于定时任务是使用feign调用了其它服务模块执行的,并且使用了future.get(); 阻塞等待线程执行结束。那么有时future任务多了fegin调用因为调用了feture.get()就会超时。
报下面的错:
Caused by: java.net.SocketException: Socket closed
at java.net.SocketInputStream.read(SocketInputStream.java:204)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at okio.InputStreamSource.read(JvmOkio.kt:90)
at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:129)
... 36 common frames omitted
2025-12-05 14:35:34,980 [Thread-25] ERROR [c.t.c.f.sentinel.ext.TbyfSentinelInvocationHandler] TbyfSentinelInvocationHandler.java:127 - feign 服务间调用异常
feign.RetryableException: timeout executing GET http://tbyf-modules-job/mess/messTaskInDoctor?orgCode=66
at feign.FeignException.errorExecuting(FeignException.java:268)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:131)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:91)
at com.tbyf.common.feign.sentinel.ext.TbyfSentinelInvocationHandler.invoke(TbyfSentinelInvocationHandler.java:103)
at org.springframework.cloud.openfeign.FeignCachingInvocationHandlerFactory$1.proceed(FeignCachingInvocationHandlerFactory.java:66)
at org.springframework.cache.interceptor.CacheInterceptor.lambda$invoke$0(CacheInterceptor.java:54)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:351)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:64)
at org.springframework.cloud.openfeign.FeignCachingInvocationHandlerFactory.lambda$create$1(FeignCachingInvocationHandlerFactory.java:53)
at com.sun.proxy.$Proxy129.messTaskInDoctor(Unknown Source)
at com.tbyf.his.xxl.job.service.HisXxljobHandler.inDoctor(HisXxljobHandler.java:29)
at sun.reflect.GeneratedMethodAccessor38.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.xxl.job.core.handler.impl.MethodJobHandler.execute(MethodJobHandler.java:31)
at com.xxl.job.core.thread.JobThread.run(JobThread.java:163)
Caused by: java.net.SocketTimeoutException: timeout
at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:143)
at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:162)
at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:335)
at okio.RealBufferedSource.indexOf(RealBufferedSource.kt:427)
at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:320)
at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29)
at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178)
at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154)
at feign.okhttp.OkHttpClient.execute(OkHttpClient.java:180)
at org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(LoadBalancerUtils.java:57)
at org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(LoadBalancerUtils.java:95)
at org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient.execute(FeignBlockingLoadBalancerClient.java:114)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:121)
... 14 common frames omitted
Caused by: java.net.SocketException: Socket closed
at java.net.SocketInputStream.read(SocketInputStream.java:204)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at okio.InputStreamSource.read(JvmOkio.kt:90)
at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:129)
... 36 common frames omitted

解决办法:
在feign中由原来的10000毫秒增加超时等待时长
# feign 配置
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
default:
connectTimeout: 60000
readTimeout: 300000
#connectTimeout: 10000
#readTimeout: 10000
3.2多线程并发发送时,像同一用户session发送消息报错及解决方案
为什么会发生这一问题呢?
由于同一用户可以多地登录,那么保存会话时,sessionKey是一样的(登录用户名),sessionId是每台电脑的会话id不一样。如果同一用户不同电脑都登录了,那么里面就会存SYS:101和SYS:102类似这样的
public static void addSession(Object sessionKey, String sessionId, WebSocketSession session) {
USER_SESSION_MAP.put(sessionKey.toString() + ":" + sessionId, session);
}
而下面发送消息时,多个线程获取到登录token,根据token获取到用户信息有两台电脑都登录了SYS账号用户,那么多线程并发执行到下面方法时,正好这两个线程都执行到下面,那么他们的sessionKey都为SYS。
public static boolean send(Object sessionKey, String message) {
List sessions = WebSocketSessionHolder.getSession(sessionKey);
if (CollectionUtils.isEmpty(sessions)) {
log.info("[send] 当前 sessionKey:{} 对应 session 不在本服务中", sessionKey);
return false;
} else {
sessions.forEach(session -> send(session, message));
return Boolean.TRUE;
}
}
我们再来看看WebSocketSessionHolder.getSession(sessionKey)方法的逻辑,key.contains(keyword),keyword为 SYS: 会过滤出上面SYS:101和SYS:102
public static List getSession(Object sessionKey) {
return getMatchingKeys(USER_SESSION_MAP, sessionKey + ":");
}
/**
* 获取匹配的keys *
* 因当前系统允许多地同账号登录 所以需要兼容多地登录同账号都能接受到消息 *
*
* @param map USER_SESSION_MAP
* @param keyword addSession时是拼接的sessionKey + ":" + sessionId
* @return List
*/
public static List getMatchingKeys(Map map, String keyword) {
return map.keySet().stream()
.filter(key -> key.contains(keyword)) // 你可以使用 contains, startsWith, endsWith 等
.map(map::get).collect(Collectors.toList());
}
这时候,这两个线程如果同时像SYS:101和SYS:102这两个会话发送消息,那么就会报错:
文件tbyf-cloud/tbyf-modules/tbyf-modules-system/src/main/java/com/pig4cloud/plugin/websocket/config/WebSocketMessageSender.java 66行会报错:
2025-12-09 11:31:36.832 ERROR 44532 --- [io-19120-exec-3] c.t.c.f.s.h.GlobalBizExceptionHandler : 全局异常信息 ex=The remote endpoint was in state [TEXT_PARTIAL_WRITING] which is an invalid state for called method
java.lang.IllegalStateException: The remote endpoint was in state [TEXT_PARTIAL_WRITING] which is an invalid state for called method
at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.checkState(WsRemoteEndpointImplBase.java:1249) ~[tomcat-embed-websocket-9.0.75.jar:9.0.75]
at org.apache.tomcat.websocket.WsRemoteEndpointImplBase$StateMachine.textPartialStart(WsRemoteEndpointImplBase.java:1209) ~[tomcat-embed-websocket-9.0.75.jar:9.0.75]
at org.apache.tomcat.websocket.WsRemoteEndpointImplBase.sendPartialString(WsRemoteEndpointImplBase.java:221) ~[tomcat-embed-websocket-9.0.75.jar:9.0.75]
at org.apache.tomcat.websocket.WsRemoteEndpointBasic.sendText(WsRemoteEndpointBasic.java:48) ~[tomcat-embed-websocket-9.0.75.jar:9.0.75]
at org.springframework.web.socket.adapter.standard.StandardWebSocketSession.sendTextMessage(StandardWebSocketSession.java:215) ~[spring-websocket-5.3.27.jar:5.3.27]
at org.springframework.web.socket.adapter.AbstractWebSocketSession.sendMessage(AbstractWebSocketSession.java:108) ~[spring-websocket-5.3.27.jar:5.3.27]
at com.pig4cloud.plugin.websocket.config.WebSocketMessageSender.send(WebSocketMessageSender.java:66) ~[classes/:1.2.0]
at com.pig4cloud.plugin.websocket.config.WebSocketMessageSender.lambda$send$0(WebSocketMessageSender.java:42) ~[classes/:1.2.0]
at java.util.ArrayList.forEach(ArrayList.java:1249) ~[na:1.8.0_131]
at com.pig4cloud.plugin.websocket.config.WebSocketMessageSender.send(WebSocketMessageSender.java:42) ~[classes/:1.2.0]
at com.tbyf.system.controller.MessageSendController.sendMsg(MessageSendController.java:93) ~[classes/:na]
at com.tbyf.system.controller.MessageSendController.lambda$sendAllDeptMsg$1(MessageSendController.java:115) ~[classes/:na]
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:1.8.0_131]
at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175) ~[na:1.8.0_131]
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374) ~[na:1.8.0_131]
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) ~[na:1.8.0_131]
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) ~[na:1.8.0_131]
at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:1.8.0_131]
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:1.8.0_131]
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:1.8.0_131]
at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) ~[na:1.8.0_131]
at com.tbyf.system.controller.MessageSendController.sendAllDeptMsg(MessageSendController.java:113) ~[classes/:na]
at com.tbyf.system.controller.MessageSendController$$FastClassBySpringCGLIB$$62490a72.invoke() ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) ~[spring-aop-5.3.27.jar:5.3.27]
at com.tbyf.system.aop.HttpLoggingAspect.doAround(HttpLoggingAspect.java:79) ~[classes/:na]
at sun.reflect.GeneratedMethodAccessor132.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89) ~[spring-aop-5.3.27.jar:5.3.27]
at com.tbyf.common.security.aspect.InnerAuthAspect.innerAround(InnerAuthAspect.java:40) ~[classes/:na]
at sun.reflect.GeneratedMethodAccessor274.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:634) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:624) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:72) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.27.jar:5.3.27]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.27.jar:5.3.27]
at com.tbyf.system.controller.MessageSendController$$EnhancerBySpringCGLIB$$cfc02c17.sendAllDeptMsg() ~[classes/:na]
at sun.reflect.GeneratedMethodAccessor284.invoke(Unknown Source) ~[na:na]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.27.jar:5.3.27]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.27.jar:5.3.27]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.27.jar:5.3.27]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:517) [jakarta.servlet-api-4.0.4.jar:4.0.4]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.27.jar:5.3.27]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:584) [jakarta.servlet-api-4.0.4.jar:4.0.4]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) [tomcat-embed-websocket-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) [spring-web-5.3.27.jar:5.3.27]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.27.jar:5.3.27]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) [spring-web-5.3.27.jar:5.3.27]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.27.jar:5.3.27]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) [spring-boot-actuator-2.7.12.jar:2.7.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.27.jar:5.3.27]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) [spring-web-5.3.27.jar:5.3.27]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) [spring-web-5.3.27.jar:5.3.27]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at com.tbyf.common.datasource.config.ClearTtlDataSourceFilter.doFilter(ClearTtlDataSourceFilter.java:30) [classes/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) [tomcat-embed-core-9.0.75.jar:9.0.75]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.75.jar:9.0.75]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
text_partial_writing状态表示远程端点正在处理一个部分的文本消息写入操作。在这种状态下,某些操作(如再次写入或关闭连接)是不被允许的,因为它们可能会破坏消息的完整性或导致连接状态不一致。并发写入:多个线程或操作尝试同时写入同一个WebSocket连接导致报错
解决方案:
在 WebSocketMessageSender 中添加同步机制
- 使用 ConcurrentHashMap 为每个 session 维护一个锁对象
- 使用 synchronized 确保同一 session 的发送操作串行执行
- 捕获 IllegalStateException 并记录日志
- 在 session 关闭时清理锁对象,避免内存泄漏
/**
* 为每个 WebSocketSession 维护一个锁对象,确保同一 session 的发送操作是串行的
* 解决并发写入导致的 IllegalStateException: TEXT_PARTIAL_WRITING 问题
*/
private static final ConcurrentHashMap sessionLocks = new ConcurrentHashMap<>();
/**
* 发送消息到指定的 WebSocket 会话
* 使用同步机制确保同一 session 的发送操作是串行的,避免并发写入冲突
*
* @param session WebSocket 会话
* @param message 要发送的消息
* @return 发送是否成功
*/
public static boolean send(WebSocketSession session, String message) {
if (session == null) {
log.error("[send] session 为 null");
return false;
} else if (!session.isOpen()) {
log.error("[send] session 已经关闭");
return false;
} else {
// 获取或创建该 session 的锁对象
String sessionId = session.getId();
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
// 使用 synchronized 确保同一 session 的发送操作是串行的
synchronized (lock) {
try {
// 再次检查 session 是否仍然打开(可能在等待锁的过程中关闭了)
if (!session.isOpen()) {
log.warn("[send] session({}) 在等待锁期间已关闭", sessionId);
// 清理已关闭 session 的锁
sessionLocks.remove(sessionId);
return false;
}
session.sendMessage(new TextMessage(message));
return true;
} catch (IllegalStateException e) {
// 捕获并发写入异常,记录日志但不抛出
log.error("[send] session({}) 发送消息时发生状态冲突,可能由于并发写入: {}", sessionId, e.getMessage());
return false;
} catch (IOException var3) {
log.error("[send] session({}) 发送消息({}) 异常", new Object[]{sessionId, message, var3});
return false;
} finally {
// 如果 session 已关闭,清理锁对象(避免内存泄漏)
if (!session.isOpen()) {
sessionLocks.remove(sessionId);
}
}
}
}
}
/**
* 清理指定 session 的锁对象
* 当 WebSocket 连接关闭时调用,避免内存泄漏
*
* @param sessionId WebSocket 会话 ID
*/
public static void removeSessionLock(String sessionId) {
if (sessionId != null) {
sessionLocks.remove(sessionId);
}
}
public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
Map queryParams = UriComponentsBuilder.fromUri(Objects.requireNonNull(session.getUri()))
.build()
.getQueryParams()
.toSingleValueMap();
LoginUser loginUser = tokenService.getLoginUser(queryParams);
log.warn("用户名:{}下线了, webSocket token is {}", loginUser.getUsername(), loginUser.getToken());
Object sessionKey = this.sessionKeyGenerator.sessionKey(session);
WebSocketSessionHolder.removeSession(sessionKey, session.getId());
// 清理 WebSocket 消息发送器的锁对象,避免内存泄漏
WebSocketMessageSender.removeSessionLock(session.getId());
}
浙公网安备 33010602011771号