Commit 2d1ff0aff7b82ee8ad99af94d87f20d2588db6b3
1 parent
d86c36a1
commit 增加webSocket
Showing
8 changed files
with
435 additions
and
0 deletions
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> |