Commit 4c0275fbf422edde2f771f5a863c08d2e0620c9e
Committed by
Gitee
!348 增加webSocket连接测试以及文件优化
Merge pull request !348 from 咱哥丶/dev
Showing
11 changed files
with
433 additions
and
190 deletions
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> |