Commit 2d1ff0aff7b82ee8ad99af94d87f20d2588db6b3

Authored by 咱哥丶
1 parent d86c36a1

commit 增加webSocket

yudao-dependencies/pom.xml
@@ -583,6 +583,12 @@ @@ -583,6 +583,12 @@
583 <artifactId>xercesImpl</artifactId> 583 <artifactId>xercesImpl</artifactId>
584 <version>${xercesImpl.version}</version> 584 <version>${xercesImpl.version}</version>
585 </dependency> 585 </dependency>
  586 + <!-- SpringBoot Websocket -->
  587 + <dependency>
  588 + <groupId>org.springframework.boot</groupId>
  589 + <artifactId>spring-boot-starter-websocket</artifactId>
  590 + <version>${spring.boot.version}</version>
  591 + </dependency>
586 </dependencies> 592 </dependencies>
587 </dependencyManagement> 593 </dependencyManagement>
588 594
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
@@ -129,6 +129,8 @@ public class YudaoWebSecurityConfigurerAdapter { @@ -129,6 +129,8 @@ public class YudaoWebSecurityConfigurerAdapter {
129 .antMatchers(buildAppApi("/**")).permitAll() 129 .antMatchers(buildAppApi("/**")).permitAll()
130 // 1.5 验证码captcha 允许匿名访问 130 // 1.5 验证码captcha 允许匿名访问
131 .antMatchers("/captcha/get", "/captcha/check").permitAll() 131 .antMatchers("/captcha/get", "/captcha/check").permitAll()
  132 + // 1.6 webSocket 允许匿名访问
  133 + .antMatchers("/websocket/message").permitAll()
132 // ②:每个项目的自定义规则 134 // ②:每个项目的自定义规则
133 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 135 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
134 authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) 136 authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
yudao-module-infra/yudao-module-infra-biz/pom.xml
@@ -111,6 +111,12 @@ @@ -111,6 +111,12 @@
111 <groupId>cn.iocoder.boot</groupId> 111 <groupId>cn.iocoder.boot</groupId>
112 <artifactId>yudao-spring-boot-starter-file</artifactId> 112 <artifactId>yudao-spring-boot-starter-file</artifactId>
113 </dependency> 113 </dependency>
  114 +
  115 + <!-- WebSocket -->
  116 + <dependency>
  117 + <groupId>org.springframework.boot</groupId>
  118 + <artifactId>spring-boot-starter-websocket</artifactId>
  119 + </dependency>
114 </dependencies> 120 </dependencies>
115 121
116 </project> 122 </project>
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +
  5 +import java.util.concurrent.Semaphore;
  6 +
  7 +/**
  8 + * 信号量相关处理
  9 + *
  10 + */
  11 +@Slf4j
  12 +public class SemaphoreUtils {
  13 +
  14 + /**
  15 + * 获取信号量
  16 + *
  17 + * @param semaphore
  18 + * @return
  19 + */
  20 + public static boolean tryAcquire(Semaphore semaphore) {
  21 + boolean flag = false;
  22 +
  23 + try {
  24 + flag = semaphore.tryAcquire();
  25 + } catch (Exception e) {
  26 + log.error("获取信号量异常", e);
  27 + }
  28 +
  29 + return flag;
  30 + }
  31 +
  32 + /**
  33 + * 释放信号量
  34 + *
  35 + * @param semaphore
  36 + */
  37 + public static void release(Semaphore semaphore) {
  38 +
  39 + try {
  40 + semaphore.release();
  41 + } catch (Exception e) {
  42 + log.error("释放信号量异常", e);
  43 + }
  44 + }
  45 +}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket;
  2 +
  3 +import org.springframework.context.annotation.Bean;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  6 +
  7 +/**
  8 + * websocket 配置
  9 + *
  10 + * @author ruoyi
  11 + */
  12 +@Configuration
  13 +public class WebSocketConfig
  14 +{
  15 + @Bean
  16 + public ServerEndpointExporter serverEndpointExporter()
  17 + {
  18 + return new ServerEndpointExporter();
  19 + }
  20 +}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket;
  2 +
  3 +import lombok.extern.slf4j.Slf4j;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +import javax.websocket.*;
  7 +import javax.websocket.server.ServerEndpoint;
  8 +import java.util.concurrent.Semaphore;
  9 +
  10 +/**
  11 + * websocket 消息处理
  12 + */
  13 +@Component
  14 +@ServerEndpoint("/websocket/message")
  15 +@Slf4j
  16 +public class WebSocketServer {
  17 +
  18 + /**
  19 + * 默认最多允许同时在线用户数100
  20 + */
  21 + public static int socketMaxOnlineCount = 100;
  22 +
  23 + private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount);
  24 +
  25 + /**
  26 + * 连接建立成功调用的方法
  27 + */
  28 + @OnOpen
  29 + public void onOpen(Session session) throws Exception {
  30 + // 尝试获取信号量
  31 + boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE);
  32 + if (!semaphoreFlag) {
  33 + // 未获取到信号量
  34 + log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount);
  35 + WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
  36 + session.close();
  37 + } else {
  38 + String userId = WebSocketUsers.getParam("userId", session);
  39 + if (userId != null) {
  40 + // 添加用户
  41 + WebSocketUsers.addSession(userId, session);
  42 + log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size());
  43 + WebSocketUsers.sendMessage(session, "接收内容:连接成功");
  44 + } else {
  45 + WebSocketUsers.sendMessage(session, "接收内容:连接失败");
  46 + }
  47 + }
  48 + }
  49 +
  50 + /**
  51 + * 连接关闭时处理
  52 + */
  53 + @OnClose
  54 + public void onClose(Session session) {
  55 + log.info("用户【sessionId={}】关闭连接!", session.getId());
  56 + // 移除用户
  57 + WebSocketUsers.removeSession(session);
  58 + // 获取到信号量则需释放
  59 + SemaphoreUtils.release(SOCKET_SEMAPHORE);
  60 + }
  61 +
  62 + /**
  63 + * 抛出异常时处理
  64 + */
  65 + @OnError
  66 + public void onError(Session session, Throwable exception) throws Exception {
  67 + if (session.isOpen()) {
  68 + // 关闭连接
  69 + session.close();
  70 + }
  71 + String sessionId = session.getId();
  72 + log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception);
  73 + // 移出用户
  74 + WebSocketUsers.removeSession(session);
  75 + // 获取到信号量则需释放
  76 + SemaphoreUtils.release(SOCKET_SEMAPHORE);
  77 + }
  78 +
  79 + /**
  80 + * 收到客户端消息时调用的方法
  81 + */
  82 + @OnMessage
  83 + public void onMessage(Session session, String message) {
  84 + WebSocketUsers.sendMessage(session, "接收内容:" + message);
  85 + }
  86 +}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.bouncycastle.util.Strings;
  7 +
  8 +import javax.validation.constraints.NotNull;
  9 +import javax.websocket.Session;
  10 +import java.io.IOException;
  11 +import java.util.List;
  12 +import java.util.Map;
  13 +import java.util.Set;
  14 +import java.util.concurrent.ConcurrentHashMap;
  15 +
  16 +/**
  17 + * websocket 客户端用户
  18 + */
  19 +@Slf4j
  20 +public class WebSocketUsers {
  21 +
  22 + /**
  23 + * 用户集
  24 + * TODO 需要登录用户的session?
  25 + */
  26 + private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
  27 +
  28 + /**
  29 + * 存储用户
  30 + *
  31 + * @param userId 唯一键
  32 + * @param session 用户信息
  33 + */
  34 + public static void addSession(String userId, Session session) {
  35 + SESSION_MAP.put(userId, session);
  36 + }
  37 +
  38 + /**
  39 + * 移除用户
  40 + *
  41 + * @param session 用户信息
  42 + * @return 移除结果
  43 + */
  44 + public static boolean removeSession(Session session) {
  45 + String key = null;
  46 + boolean flag = SESSION_MAP.containsValue(session);
  47 + if (flag) {
  48 + Set<Map.Entry<String, Session>> entries = SESSION_MAP.entrySet();
  49 + for (Map.Entry<String, Session> entry : entries) {
  50 + Session value = entry.getValue();
  51 + if (value.equals(session)) {
  52 + key = entry.getKey();
  53 + break;
  54 + }
  55 + }
  56 + } else {
  57 + return true;
  58 + }
  59 + return removeSession(key);
  60 + }
  61 +
  62 + /**
  63 + * 移出用户
  64 + *
  65 + * @param userId 用户id
  66 + */
  67 + public static boolean removeSession(String userId) {
  68 + log.info("用户【userId={}】退出", userId);
  69 + Session remove = SESSION_MAP.remove(userId);
  70 + if (remove != null) {
  71 + boolean containsValue = SESSION_MAP.containsValue(remove);
  72 + log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size());
  73 + return containsValue;
  74 + } else {
  75 + return true;
  76 + }
  77 + }
  78 +
  79 + /**
  80 + * 获取在线用户列表
  81 + *
  82 + * @return 返回用户集合
  83 + */
  84 + public static Map<String, Session> getUsers() {
  85 + return SESSION_MAP;
  86 + }
  87 +
  88 + /**
  89 + * 向所有在线人发送消息
  90 + *
  91 + * @param message 消息内容
  92 + */
  93 + public static void sendMessageToAll(String message) {
  94 + SESSION_MAP.forEach((userId, session) -> {
  95 + if (session.isOpen()) {
  96 + sendMessage(session, message);
  97 + }
  98 + });
  99 + }
  100 +
  101 + /**
  102 + * 异步发送文本消息
  103 + *
  104 + * @param session 用户session
  105 + * @param message 消息内容
  106 + */
  107 + public static void sendMessageAsync(Session session, String message) {
  108 + if (session.isOpen()) {
  109 + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
  110 + session.getAsyncRemote().sendText(message);
  111 + } else {
  112 + log.warn("用户【session={}】不在线", session.getId());
  113 + }
  114 + }
  115 +
  116 + /**
  117 + * 同步发送文本消息
  118 + *
  119 + * @param session 用户session
  120 + * @param message 消息内容
  121 + */
  122 + public static void sendMessage(Session session, String message) {
  123 + try {
  124 + if (session.isOpen()) {
  125 + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
  126 + session.getBasicRemote().sendText(message);
  127 + } else {
  128 + log.warn("用户【session={}】不在线", session.getId());
  129 + }
  130 + } catch (IOException e) {
  131 + log.error("发送消息异常", e);
  132 + }
  133 +
  134 + }
  135 +
  136 + /**
  137 + * 根据用户id发送消息
  138 + *
  139 + * @param userId 用户id
  140 + * @param message 消息内容
  141 + */
  142 + public static void sendMessage(String userId, String message) {
  143 + Session session = SESSION_MAP.get(userId);
  144 + //判断是否存在该用户的session,并且是否在线
  145 + if (session == null || !session.isOpen()) {
  146 + return;
  147 + }
  148 + sendMessage(session, message);
  149 + }
  150 +
  151 +
  152 + /**
  153 + * 获取session中的指定参数值
  154 + *
  155 + * @param key 参数key
  156 + * @param session 用户session
  157 + */
  158 + public static String getParam(@NotNull String key, Session session) {
  159 + //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数?
  160 + String value = null;
  161 + Map<String, List<String>> parameters = session.getRequestParameterMap();
  162 + if (MapUtil.isNotEmpty(parameters)) {
  163 + value = parameters.get(key).get(0);
  164 + } else {
  165 + String queryString = session.getQueryString();
  166 + if (!StrUtil.isEmpty(queryString)) {
  167 + String[] params = Strings.split(queryString, '&');
  168 + for (String paramPair : params) {
  169 + String[] nameValues = Strings.split(paramPair, '=');
  170 + if (key.equals(nameValues[0])) {
  171 + value = nameValues[1];
  172 + }
  173 + }
  174 + }
  175 + }
  176 + return value;
  177 + }
  178 +}
yudao-ui-admin/src/views/infra/webSocket/index.vue 0 → 100644
  1 +<template>
  2 + <div class="app-container">
  3 + <el-form label-width="120px">
  4 + <el-row type="flex" :gutter="0">
  5 + <el-col :sm="12">
  6 + <el-form-item label="WebSocket地址" size="small">
  7 + <el-input v-model="url" type="text"/>
  8 + </el-form-item>
  9 + </el-col>
  10 + <el-col :offset="1">
  11 + <el-form-item label="" label-width="0px" size="small">
  12 + <el-button @click="connect" type="primary" :disabled="ws&&ws.readyState===1">
  13 + {{ ws && ws.readyState === 1 ? "已连接" : "连接" }}
  14 + </el-button>
  15 + <el-button @click="exit" type="danger">断开</el-button>
  16 + </el-form-item>
  17 + </el-col>
  18 + </el-row>
  19 + <el-form-item label="发送内容" size="small">
  20 + <el-input type="textarea" v-model="message" :rows="5"/>
  21 + </el-form-item>
  22 + <el-form-item label="" size="small">
  23 + <el-button type="success" @click="send">发送消息</el-button>
  24 + </el-form-item>
  25 + <el-form-item label="接收内容" size="small">
  26 + <el-input type="textarea" v-model="content" :rows="12" disabled/>
  27 + </el-form-item>
  28 + <el-form-item label="" size="small">
  29 + <el-button type="info" @click="content=''">清空消息</el-button>
  30 + </el-form-item>
  31 + </el-form>
  32 + </div>
  33 +</template>
  34 +
  35 +<script>
  36 +import store from "@/store";
  37 +import {getNowDateTime} from "@/utils/ruoyi";
  38 +
  39 +export default {
  40 + data() {
  41 + return {
  42 + url: process.env.VUE_APP_BASE_API + "/websocket/message",
  43 + message: "",
  44 + content: "",
  45 + ws: null,
  46 + };
  47 + },
  48 + created() {
  49 + this.url = this.url.replace("http", "ws")
  50 + },
  51 + methods: {
  52 + connect() {
  53 + if (!'WebSocket' in window) {
  54 + this.$modal.msgError("您的浏览器不支持WebSocket");
  55 + return;
  56 + }
  57 + const userId = store.getters.userId;
  58 + this.ws = new WebSocket(this.url + "?userId=" + userId);
  59 + const self = this;
  60 + this.ws.onopen = function (event) {
  61 + self.content = self.content + "\n**********************连接开始**********************\n";
  62 + };
  63 + this.ws.onmessage = function (event) {
  64 + self.content = self.content + "接收时间:" + getNowDateTime() + "\n" + event.data + "\n";
  65 + };
  66 + this.ws.onclose = function (event) {
  67 + self.content = self.content + "**********************连接关闭**********************\n";
  68 + };
  69 + this.ws.error = function (event) {
  70 + self.content = self.content + "**********************连接异常**********************\n";
  71 + };
  72 + },
  73 + exit() {
  74 + if (this.ws) {
  75 + this.ws.close();
  76 + this.ws = null;
  77 + }
  78 + },
  79 + send() {
  80 + if (!this.ws || this.ws.readyState !== 1) {
  81 + this.$modal.msgError("未连接到服务器");
  82 + return;
  83 + }
  84 + if (!this.message) {
  85 + this.$modal.msgError("请输入发送内容");
  86 + return;
  87 + }
  88 + this.ws.send(this.message);
  89 + }
  90 + },
  91 +};
  92 +</script>