目录

1.后端:

1.1引入依赖

1.2.添加session会话管理WebSocketSessionHolder 

1.3.添加建立session会话连接到会话管理及监听下线MapSessionWebSocketHandlerDecorator

1.4.添加断开session会话连接前请求处理UserAttributeHandshakeInterceptor

1.5.后端消息发送

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

1.7引入XXLJOB流程

1.7.1引入xxl job依赖

1.7.2application.yml中添加

1.7.3xxl job配置说明

2.前端:

2.1在你想要建立连接的页面created中添加连接

2.2websocket.js处理连接及断开重连

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:101SYS: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());
    }