Commit 4c0275fbf422edde2f771f5a863c08d2e0620c9e

Authored by 芋道源码
Committed by Gitee
2 parents 0f722d0b 18622c5d

!348 增加webSocket连接测试以及文件优化

Merge pull request !348 from 咱哥丶/dev
sql/mysql/ruoyi-vue-pro.sql
@@ -1710,6 +1710,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i @@ -1710,6 +1710,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
1710 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0'); 1710 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
1711 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0'); 1711 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
1712 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'example', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0'); 1712 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'example', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
  1713 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', '#', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
  1714 +
1713 COMMIT; 1715 COMMIT;
1714 1716
1715 -- ---------------------------- 1717 -- ----------------------------
yudao-dependencies/pom.xml
@@ -595,6 +595,12 @@ @@ -595,6 +595,12 @@
595 <artifactId>xercesImpl</artifactId> 595 <artifactId>xercesImpl</artifactId>
596 <version>${xercesImpl.version}</version> 596 <version>${xercesImpl.version}</version>
597 </dependency> 597 </dependency>
  598 + <!-- SpringBoot Websocket -->
  599 + <dependency>
  600 + <groupId>org.springframework.boot</groupId>
  601 + <artifactId>spring-boot-starter-websocket</artifactId>
  602 + <version>${spring.boot.version}</version>
  603 + </dependency>
598 </dependencies> 604 </dependencies>
599 </dependencyManagement> 605 </dependencyManagement>
600 606
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/controller/admin/file/FileController.java deleted
1 -package cn.iocoder.yudao.module.infra.controller.admin.file;  
2 -  
3 -import cn.hutool.core.io.IoUtil;  
4 -import cn.hutool.core.util.StrUtil;  
5 -import cn.iocoder.yudao.framework.common.pojo.CommonResult;  
6 -import cn.iocoder.yudao.framework.common.pojo.PageResult;  
7 -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;  
8 -import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;  
9 -import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;  
10 -import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO;  
11 -import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileUploadReqVO;  
12 -import cn.iocoder.yudao.module.infra.convert.file.FileConvert;  
13 -import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;  
14 -import cn.iocoder.yudao.module.infra.service.file.FileService;  
15 -import io.swagger.annotations.Api;  
16 -import io.swagger.annotations.ApiImplicitParam;  
17 -import io.swagger.annotations.ApiOperation;  
18 -import lombok.extern.slf4j.Slf4j;  
19 -import org.springframework.http.HttpStatus;  
20 -import org.springframework.security.access.prepost.PreAuthorize;  
21 -import org.springframework.validation.annotation.Validated;  
22 -import org.springframework.web.bind.annotation.*;  
23 -import org.springframework.web.multipart.MultipartFile;  
24 -  
25 -import javax.annotation.Resource;  
26 -import javax.annotation.security.PermitAll;  
27 -import javax.servlet.http.HttpServletRequest;  
28 -import javax.servlet.http.HttpServletResponse;  
29 -import javax.validation.Valid;  
30 -  
31 -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;  
32 -  
33 -@Api(tags = "管理后台 - 文件存储")  
34 -@RestController  
35 -@RequestMapping("/infra/file")  
36 -@Validated  
37 -@Slf4j  
38 -public class FileController {  
39 -  
40 - @Resource  
41 - private FileService fileService;  
42 -  
43 - @PostMapping("/upload")  
44 - @ApiOperation("上传文件")  
45 - @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要  
46 - public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {  
47 - MultipartFile file = uploadReqVO.getFile();  
48 - String path = uploadReqVO.getPath();  
49 - return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));  
50 - }  
51 -  
52 - @DeleteMapping("/delete")  
53 - @ApiOperation("删除文件")  
54 - @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)  
55 - @PreAuthorize("@ss.hasPermission('infra:file:delete')")  
56 - public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) throws Exception {  
57 - fileService.deleteFile(id);  
58 - return success(true);  
59 - }  
60 -  
61 - @GetMapping("/{configId}/get/**")  
62 - @PermitAll  
63 - @ApiOperation("下载文件")  
64 - @ApiImplicitParam(name = "configId", value = "配置编号", required = true, dataTypeClass = Long.class)  
65 - public void getFileContent(HttpServletRequest request,  
66 - HttpServletResponse response,  
67 - @PathVariable("configId") Long configId) throws Exception {  
68 - // 获取请求的路径  
69 - String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);  
70 - if (StrUtil.isEmpty(path)) {  
71 - throw new IllegalArgumentException("结尾的 path 路径必须传递");  
72 - }  
73 -  
74 - // 读取内容  
75 - byte[] content = fileService.getFileContent(configId, path);  
76 - if (content == null) {  
77 - log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);  
78 - response.setStatus(HttpStatus.NOT_FOUND.value());  
79 - return;  
80 - }  
81 - ServletUtils.writeAttachment(response, path, content);  
82 - }  
83 -  
84 - @GetMapping("/page")  
85 - @ApiOperation("获得文件分页")  
86 - @PreAuthorize("@ss.hasPermission('infra:file:query')")  
87 - public CommonResult<PageResult<FileRespVO>> getFilePage(@Valid FilePageReqVO pageVO) {  
88 - PageResult<FileDO> pageResult = fileService.getFilePage(pageVO);  
89 - return success(FileConvert.INSTANCE.convertPage(pageResult));  
90 - }  
91 -  
92 -}  
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java deleted
1 -package cn.iocoder.yudao.module.infra.service.file;  
2 -  
3 -import cn.hutool.core.lang.Assert;  
4 -import cn.hutool.core.util.StrUtil;  
5 -import cn.iocoder.yudao.framework.common.pojo.PageResult;  
6 -import cn.iocoder.yudao.framework.common.util.io.FileUtils;  
7 -import cn.iocoder.yudao.framework.file.core.client.FileClient;  
8 -import cn.iocoder.yudao.framework.file.core.utils.FileTypeUtils;  
9 -import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;  
10 -import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;  
11 -import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;  
12 -import lombok.SneakyThrows;  
13 -import org.springframework.stereotype.Service;  
14 -  
15 -import javax.annotation.Resource;  
16 -  
17 -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;  
18 -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;  
19 -  
20 -/**  
21 - * 文件 Service 实现类  
22 - *  
23 - * @author 芋道源码  
24 - */  
25 -@Service  
26 -public class FileServiceImpl implements FileService {  
27 -  
28 - @Resource  
29 - private FileConfigService fileConfigService;  
30 -  
31 - @Resource  
32 - private FileMapper fileMapper;  
33 -  
34 - @Override  
35 - public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {  
36 - return fileMapper.selectPage(pageReqVO);  
37 - }  
38 -  
39 - @Override  
40 - @SneakyThrows  
41 - public String createFile(String name, String path, byte[] content) {  
42 - // 计算默认的 path 名  
43 - String type = FileTypeUtils.getMineType(content, name);  
44 - if (StrUtil.isEmpty(path)) {  
45 - path = FileUtils.generatePath(content, name);  
46 - }  
47 - // 如果 name 为空,则使用 path 填充  
48 - if (StrUtil.isEmpty(name)) {  
49 - name = path;  
50 - }  
51 -  
52 - // 上传到文件存储器  
53 - FileClient client = fileConfigService.getMasterFileClient();  
54 - Assert.notNull(client, "客户端(master) 不能为空");  
55 - String url = client.upload(content, path, type);  
56 -  
57 - // 保存到数据库  
58 - FileDO file = new FileDO();  
59 - file.setConfigId(client.getId());  
60 - file.setName(name);  
61 - file.setPath(path);  
62 - file.setUrl(url);  
63 - file.setType(type);  
64 - file.setSize(content.length);  
65 - fileMapper.insert(file);  
66 - return url;  
67 - }  
68 -  
69 - @Override  
70 - public void deleteFile(Long id) throws Exception {  
71 - // 校验存在  
72 - FileDO file = this.validateFileExists(id);  
73 -  
74 - // 从文件存储器中删除  
75 - FileClient client = fileConfigService.getFileClient(file.getConfigId());  
76 - Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());  
77 - client.delete(file.getPath());  
78 -  
79 - // 删除记录  
80 - fileMapper.deleteById(id);  
81 - }  
82 -  
83 - private FileDO validateFileExists(Long id) {  
84 - FileDO fileDO = fileMapper.selectById(id);  
85 - if (fileDO == null) {  
86 - throw exception(FILE_NOT_EXISTS);  
87 - }  
88 - return fileDO;  
89 - }  
90 -  
91 - @Override  
92 - public byte[] getFileContent(Long configId, String path) throws Exception {  
93 - FileClient client = fileConfigService.getFileClient(configId);  
94 - Assert.notNull(client, "客户端({}) 不能为空", configId);  
95 - return client.getContent(path);  
96 - }  
97 -  
98 -}  
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 +@Configuration
  11 +public class WebSocketConfig {
  12 + @Bean
  13 + public ServerEndpointExporter serverEndpointExporter() {
  14 + return new ServerEndpointExporter();
  15 + }
  16 +}
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>