应用案例(审批消息主动通知-Mes服务集成)
1、依赖引入
<!-- websocket 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
2、websocket配置类
注册自定义处理器、拦截器
@Configuration
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketConfigurer {
@Resource
private WebsocketHandler websocketHandler;
@Resource
private WebsocketInterceptor websocketInterceptor;
/**
* 注册自定义处理器
*
* @param webSocketHandlerRegistry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
// 注册处理器
webSocketHandlerRegistry.addHandler(websocketHandler,"/websocket")
.setAllowedOrigins("*")
// 注册拦截器
.addInterceptors(websocketInterceptor);
}
}
3、websocket自定义拦截器
@Slf4j
@Component
public class WebsocketInterceptor extends HttpSessionHandshakeInterceptor {
/**
* 管理用户信息
*
* @param request
* @param response
* @param handler
* @param map
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request,
ServerHttpResponse response,
WebSocketHandler handler,
Map<String, Object> map) throws Exception {
String headerKey = "Sec-WebSocket-Protocol";
HttpHeaders headers = request.getHeaders();
String info = headers.getFirst("sec-websocket-protocol");
// 从头信息中获取的数据进行分割
String[] split = info.split(",");
log.info(info);
HttpHeaders responseHeaders = response.getHeaders();
// 获取到用户信息并set进实体类中
UserAuthDTO userAuthDTO = new UserAuthDTO();
userAuthDTO.setToken(split[0]);
userAuthDTO.setUserId(split[2]);
userAuthDTO.setCustId(split[1]);
if(ObjectUtil.isEmpty(userAuthDTO)){
System.out.println("socket连接失败 ---> token过期 --->"+info);
response.setStatusCode(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED);
return false;
}
map.put("userAuthDTO",userAuthDTO);
responseHeaders.add(headerKey,split[0]);
return super.beforeHandshake(request, response, handler, map);
}
}
4、websocket自定义处理器
@Slf4j
@Component
public class WebsocketHandler extends AbstractWebSocketHandler {
/**
* webSocket连接创建后调用
*
* @param session
*/
@Override
public void afterConnectionEstablished(WebSocketSession session){
Map<String, Object> attrMap = session.getAttributes();
// 获取用户信息
UserAuthDTO userAuthDTO = (UserAuthDTO) attrMap.get("userAuthDTO");
long time = System.currentTimeMillis();
// 为了避免key重复 需要添加时间戳保证key的唯一性
String key = userAuthDTO.getToken() + "-" + time;
// 存入集合中
WebSocketUser.addUserInfoS(userAuthDTO);
// 用户信息存入list集合
WebSocketUser.putUserInfo(key,session);
log.info("连接建立成功");
}
/**
* 接收到消息会调用
*
* @param session
* @param message
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message){
log.info("收到客户端消息[{}]", message);
}
/**
* 连接关闭会调用
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("客户端关闭连接....");
//Map<String, Object> attrMap = session.getAttributes();
// 删除离线session信息
WebSocketUser.remove(session);
// 关闭连接
session.close();
log.info(status.toString());
log.info("已关闭socket连接");
}
/**
* 连接出错会调用
*
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("socket连接出错...");
exception.printStackTrace();
// 关闭连接
session.close();
log.error("已关闭socket连接");
}
}
5、websocket消息处理
@Slf4j
@Component
public class WebSocketUser {
/**
* WebSocketSession集合
*/
static Map<String, WebSocketSession> USER_SOCKETS = new ConcurrentHashMap<>();
/**
* 用户数据信息
*/
static List<UserAuthDTO> USER_INFO = new ArrayList<>();
@Resource
private ExternalUserTokenClient externalUserTokenClient;
/**
* 存储session信息
*
* @param key 唯一键
* @param webSocketSession 用户信息
*/
public static void putUserInfo(String key, WebSocketSession webSocketSession){
USER_SOCKETS.put(key, webSocketSession);
}
/**
* 存储用户信息
*
* @param userAuthDTO 用户信息
*/
public static void addUserInfoS(UserAuthDTO userAuthDTO){
USER_INFO.add(userAuthDTO);
}
/**
* 获取在线用户session列表
*
* @return 返回用户集合
*/
public static Map<String, WebSocketSession> getUsers(){
return USER_SOCKETS;
}
/**
* 获取在线用户信息集合列表
*
* @return 返回用户集合
*/
public static List<UserAuthDTO> getUserInfo(){
return USER_INFO;
}
/**
* 移除用户
*
* @param webSocketSession 用户信息
*
* @return 移除结果
*/
public static boolean remove(WebSocketSession webSocketSession){
String key = null;
// 是否包含websocketsession
boolean flag = USER_SOCKETS.containsValue(webSocketSession);
// 如果还包含的话
if (flag){
Set<Map.Entry<String, WebSocketSession>> entries = USER_SOCKETS.entrySet();
for (Map.Entry<String, WebSocketSession> entry : entries)
{
WebSocketSession value = entry.getValue();
if (value.equals(webSocketSession))
{
key = entry.getKey();
break;
}
}
}else{
return true;
}
//移除用户信息
return remove(key);
}
/**
* 移出用户
*
* @param key 键
*/
public static boolean remove(String key){
log.info("\n 正在移出用户 - {}", key);
//
WebSocketSession remove = USER_SOCKETS.remove(key);
USER_INFO.remove(key);
if (remove != null){
boolean containsValue = USER_SOCKETS.containsValue(remove);
log.info("\n 移出结果 - {}", containsValue ? "失败" : "成功");
return containsValue;
}else{
return true;
}
}
/**
* 群发消息文本消息
*
* @param message 消息内容
*/
public static void sendMessageToUsersByText(TextMessage message)
{
Collection<WebSocketSession> values = USER_SOCKETS.values();
if(values.isEmpty()){
// throw new CommonRuntimeException(MesStatusEnum.MES_APPROVAL_57021);
log.info("客户端在线人数为0");
}else{
for (WebSocketSession value : values){
sendMessageToUserByText(value, message);
}
}
}
/**
* 发送文本消息
*
* @param session 自己的用户名
* @param message 消息内容
*/
public static void sendMessageToUserByText(WebSocketSession session, TextMessage message){
if (session != null){
try{
session.sendMessage(message);
}catch (IOException e){
log.error("\n[发送消息异常]", e);
}
}else{
log.info("\n[你已离线]");
}
}
/**
* 获取用户信息(废弃)
*
* @param webSocketSession
* @return
*/
public static UserAuthDTO getUserAuthDto(WebSocketSession webSocketSession){
Map<String, Object> attrMap = webSocketSession.getAttributes();
// 获取用户信息
UserAuthDTO userAuthDTO = (UserAuthDTO) attrMap.get("userAuthDTO");
return userAuthDTO;
}
/**
* 消息发送指定人
*
* @param message
* @param custId
* @param userList
*/
@Async
public void sendMessageTo(TextMessage message, String custId, List<MesMessageRelation> userList){
// WebSocketUser.getUsers()获取所有在线的用户信息
Set<Map.Entry<String, WebSocketSession>> entries = WebSocketUser.getUsers().entrySet();
List<UserAuthDTO> entriesUserInfo = WebSocketUser.getUserInfo();
for (MesMessageRelation mesMessageRelation : userList) {
if("02".equals(mesMessageRelation.getObjIdType())){
for (Map.Entry<String, WebSocketSession> entry : entries){
String key = entry.getKey();
String[] splitRes = key.split("-");
String token = splitRes[0];
// 判断token是否过期,如果过期就从map集合中删除,如果没有过期就说明该用户在线 需要比对发送消息
// 调用/external/uaa/v1/userToken/{token}接口
ResultDTO<ExternalUserTokenResponseDTO> resultDTO = externalUserTokenClient.selectByToken(token);
if (!ObjectUtil.equal(resultDTO.getStatus(), HttpStatusEnum.SUCCESS.getCode())) {
throw new CommonRuntimeException(resultDTO.getStatus(), resultDTO.getMessage());
}
// 如果token过期了则需要在集合中将其删除
ExternalUserTokenResponseDTO data = resultDTO.getData();
if(ObjectUtil.isEmpty(data)){
WebSocketUser.remove(token);
// 如果没有过期则需要发送消息
}else{
WebSocketSession session = entry.getValue();
if(CollUtil.isNotEmpty(entriesUserInfo)){
for (UserAuthDTO userAuthEntry : entriesUserInfo) {
if(userAuthEntry.getCustId().equals(custId) && userAuthEntry.getUserId().equals(mesMessageRelation.getObjId())){
//消息发送指定人
try {
session.sendMessage(message);
}catch (Exception e){
e.printStackTrace();
}
log.info("【发送消息】:向{}发送消息:{}",mesMessageRelation.getObjName(),message);
break;
}
}
}
}
}
}
}
}
}
6、用户信息实体类
@Data
public class UserAuthDTO {
/**
* 用户token
*/
private String token;
/**
* 用户id
*/
private String userId;
/**
* 客户id
*/
private String custId;
/**
* 用户姓名
*/
private String userName;
}
配置网关时遇到的问题及解决方案
问题1:路由转发问题
出现的原因:
Gateway未能配置路由导致经过网关的时候找不到转发路径。
解决的方法:
因为前端需要发送ws://请求,因此经过网关的时候需要配置转发路由,由于项目中网关路由是通过数据库读取到redis缓存中,因此需要在数据库中添加转发路由数据,之后重启网关服务使得路由加载到redis中,就可以解决此问题。
问题2:请求过滤问题
出现的原因:
网关过滤器中过滤掉ws和wss请求。
解决的方法:
修改代码中过滤器部分,对ws和wss请求添加判断解决此问题。
在线websockt测试工具
通过WebSocket King client及postman接口测试的形式