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 | 583 | <artifactId>xercesImpl</artifactId> |
| 584 | 584 | <version>${xercesImpl.version}</version> |
| 585 | 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 | 592 | </dependencies> |
| 587 | 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 | 129 | .antMatchers(buildAppApi("/**")).permitAll() |
| 130 | 130 | // 1.5 验证码captcha 允许匿名访问 |
| 131 | 131 | .antMatchers("/captcha/get", "/captcha/check").permitAll() |
| 132 | + // 1.6 webSocket 允许匿名访问 | |
| 133 | + .antMatchers("/websocket/message").permitAll() | |
| 132 | 134 | // ②:每个项目的自定义规则 |
| 133 | 135 | .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 |
| 134 | 136 | authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) | ... | ... |
yudao-module-infra/yudao-module-infra-biz/pom.xml
| ... | ... | @@ -111,6 +111,12 @@ |
| 111 | 111 | <groupId>cn.iocoder.boot</groupId> |
| 112 | 112 | <artifactId>yudao-spring-boot-starter-file</artifactId> |
| 113 | 113 | </dependency> |
| 114 | + | |
| 115 | + <!-- WebSocket --> | |
| 116 | + <dependency> | |
| 117 | + <groupId>org.springframework.boot</groupId> | |
| 118 | + <artifactId>spring-boot-starter-websocket</artifactId> | |
| 119 | + </dependency> | |
| 114 | 120 | </dependencies> |
| 115 | 121 | |
| 116 | 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> | ... | ... |