全局消息是推送,实现app在线更新,WebSocket
- 1.在main.js中定义全局的WebSocket
- 2.java后端建立和发送WebSocket
- 3.通知所有用户更新
背景:
开发人员开发后app后打包成.apk文件,上传后通知厂区在线用户更新app。
那么没在线的怎么办?因为我们在上一篇博客中写了,在app打开的时候回去校验是否需要更新了,所以已经完成了闭环。
即时通讯首先想到的就是WebSocket
- 1.我们定义全局的WebSocket
- 2.在全局监听,当监听到指定消息的时候弹窗更新,下载逻辑也就是下载最新的apk,在上一篇博客写了,点击下方链接。
uniapp:实现手机端APP登录强制更新,从本地服务器下载新的apk更新,并使用WebSocket,实时强制在线用户更新
但是有一个问题,就是手持机少还可以,要是多的话,几百台连接还不是不太合适?,所以用登录的时候检测更新是最好的
使用post调用测试
@GetMapping("/sendAllUser")public void sendAllUser(){webSocketServer.sendAllMessage("updateApp");}
1.在main.js中定义全局的WebSocket
import App from './App'
import Vue from 'vue'
import uView from 'uni_modules/uview-ui'
import debounce from '@/utils/debounce'Vue.use(uView)
Vue.config.productionTip = false;
App.mpType = 'app'
Vue.prototype.$debounce = debounce;
import store from '@/store';const app = new Vue({store,...App
})// 创建全局的事件总线
Vue.prototype.$eventBus = new Vue();// 创建全局WebSocket连接
Vue.prototype.$socket = uni.connectSocket({url: 'ws://127.0.0.1:8080/webSocket',complete: (res) => {console.log('WebSocket connection completed:', res);},
});// 全局监听WebSocket消息
Vue.prototype.$socket.onMessage((res) => {console.log('Received WebSocket message:', res);if(res.data == 'updateApp'){uni.navigateTo({url: '/pages/index/upgrade'})}// 将消息传递给事件总线,以便在整个应用中进行处理// Vue.prototype.$eventBus.$emit('socketMessage', res);
});// 引入请求封装,将app参数传递到配置中
require('@/config/http.interceptor.js')(app)app.$mount()
当接受到updateApp的消息的时候,打开更新弹窗,详情见链接:
2.java后端建立和发送WebSocket
下面java后端建立和发送WebSocket 消息的地方,若依自带websocket连接代码,咕咕在其基础上做了一些修改,添加了部分方法
package com.qygx.framework.websocket;import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Semaphore;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;/*** websocket 消息处理* * @author ruoyi*/
//@ConditionalOnClass(value = WebSocketConfig.class)
@ServerEndpoint("/webSocket")
@Component
public class WebSocketServer
{/*** WebSocketServer 日志控制器*/private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);/*** 默认最多允许同时在线人数100*/public static int socketMaxOnlineCount = 100;private static Semaphore socketSemaphore = new Semaphore(socketMaxOnlineCount);private static CopyOnWriteArraySet<WebSocketServer> webSockets =new CopyOnWriteArraySet<>();//与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;/*** 用户ID*/private String userId;/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session) throws Exception{this.session = session;this.userId = userId;webSockets.add(this);boolean semaphoreFlag = false;// 尝试获取信号量semaphoreFlag = SemaphoreUtils.tryAcquire(socketSemaphore);if (!semaphoreFlag){// 未获取到信号量LOGGER.error("\n 当前在线人数超过限制数- {}", socketMaxOnlineCount);WebSocketUsers.sendMessageToUserByText(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);session.close();}else{// 添加用户WebSocketUsers.put(session.getId(), session);LOGGER.info("\n 建立连接 - {}", session);LOGGER.info("\n 当前人数 - {}", WebSocketUsers.getUsers().size());WebSocketUsers.sendMessageToUserByText(session, "连接成功");}}/*** 连接关闭时处理*/@OnClosepublic void onClose(Session session){webSockets.remove(this);LOGGER.info("\n 关闭连接 - {}", session);// 移除用户WebSocketUsers.remove(session.getId());// 获取到信号量则需释放SemaphoreUtils.release(socketSemaphore);}/*** 抛出异常时处理*/@OnErrorpublic void onError(Session session, Throwable exception) throws Exception{if (session.isOpen()){// 关闭连接session.close();}String sessionId = session.getId();LOGGER.info("\n 连接异常 - {}", sessionId);LOGGER.info("\n 异常信息 - {}", exception);// 移出用户WebSocketUsers.remove(sessionId);// 获取到信号量则需释放SemaphoreUtils.release(socketSemaphore);}/*** 服务器接收到客户端消息时调用的方法*/@OnMessagepublic void onMessage(String message, Session session){String msg = message.replace("你", "我").replace("吗", "");WebSocketUsers.sendMessageToUserByText(session, msg);}// 此为广播消息public void sendAllMessage(String message) {for(WebSocketServer webSocket : webSockets) {try {if(webSocket.session.isOpen()) {webSocket.session.getAsyncRemote().sendText(message);}} catch (Exception e) {e.printStackTrace();}}}
}
3.通知所有用户更新
咕咕这里是在用户上传完,提交表单成功后发送消息的,大家可自定义。
/*** 新增app版本*/@PreAuthorize("@ss.hasPermi('system:appversion:add')")@Log(title = "app版本", businessType = BusinessType.INSERT)@PostMappingpublic AjaxResult add(@RequestBody SysAppVersion sysAppVersion){SysAppVersion newestVersion = sysAppVersionService.getNewestVersion();sysAppVersionService.insertSysAppVersion(sysAppVersion);if(newestVersion != null && newestVersion.getAppVersionCode() < sysAppVersion.getAppVersionCode()){webSocketServer.sendAllMessage("updateApp");}return toAjax(sysAppVersionService.insertSysAppVersion(sysAppVersion));}
下面的是,文件上传的代码,大家可以随意参考,基于若依的一些修改,本来若依也是开源的,所以这个也就不做删减了。
package com.qygx.mes.app.controller;import com.qygx.common.config.RuoYiConfig;
import com.qygx.common.core.domain.AjaxResult;
import com.qygx.common.utils.DateUtils;
import com.qygx.common.utils.StringUtils;
import com.qygx.common.utils.file.FileUploadUtils;
import com.qygx.common.utils.file.FileUtils;
import com.qygx.common.utils.file.MimeTypeUtils;
import com.qygx.common.utils.uuid.Seq;
import com.qygx.framework.config.ServerConfig;
import com.qygx.framework.websocket.WebSocketServer;
import com.qygx.mes.csm.domain.SysAppVersion;
import com.qygx.mes.csm.service.ISysAppVersionService;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Objects;@RestController
@RequestMapping("/app")
public class AppController {@Autowiredprivate WebSocketServer webSocketServer;@Autowiredprivate ServerConfig serverConfig;@Autowiredprivate ISysAppVersionService sysAppVersionService;@GetMapping("/download")public void download(String path, HttpServletResponse response) {try {//拿到最新的版本//http://127.0.0.1:8080/profile/upload/2023/12/28/__UNI__E832695__20231227183906_20231228110314A001.apkSysAppVersion newestVersion = sysAppVersionService.getNewestVersion();File file = new File("d://app//"+newestVersion.getFileName());String filename = file.getName();String ext = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();FileInputStream fileInputStream = new FileInputStream(file);InputStream fis = new BufferedInputStream(fileInputStream);byte[] buffer = new byte[fis.available()];fis.read(buffer);fis.close();response.reset();// 设置response的Headerresponse.setCharacterEncoding("UTF-8");//Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存//attachment表示以附件方式下载 inline表示在线打开 "Content-Disposition: inline; filename=文件名.mp3"// filename表示文件的默认名称,因为网络传输只支持URL编码的相关支付,因此需要将文件名URL编码后进行传输,前端收到后需要反编码才能获取到真正的名称response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));// 告知浏览器文件的大小response.addHeader("Content-Length", "" + file.length());OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());response.setContentType("application/octet-stream");outputStream.write(buffer);outputStream.flush();} catch (IOException ex) {ex.printStackTrace();}}/*** 通用上传请求(单个)*/@PostMapping("/upload")public AjaxResult uploadFile(MultipartFile file) throws Exception{try{String newName = extractFilename(file);String absPath = "D:\\app\\" + newName;file.transferTo(Paths.get(absPath));// 上传文件路径// 上传并返回新文件名称AjaxResult ajax = AjaxResult.success();ajax.put("url", absPath);ajax.put("fileName", "fileName");ajax.put("newFileName", newName);ajax.put("originalFilename", file.getOriginalFilename());return ajax;}catch (Exception e){return AjaxResult.error(e.getMessage());}}private String extractFilename(MultipartFile file){return StringUtils.format("{}_{}.{}",FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));}/*** 获取文件名的后缀** @param file 表单文件* @return 后缀名*/public static final String getExtension(MultipartFile file){String extension = FilenameUtils.getExtension(file.getOriginalFilename());if (StringUtils.isEmpty(extension)){extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));}return extension;}@GetMapping("/getNewestVersion")public AjaxResult getNewestVersion(){SysAppVersion newestVersion = sysAppVersionService.getNewestVersion();HashMap<String, Object> map = new HashMap<>();if(newestVersion != null ){map.put("newVersionName", newestVersion.getAppVersion());map.put("newVersionCode", newestVersion.getAppVersionCode());}else {map.put("newVersionName", "v");map.put("newVersionCode", 0);}return AjaxResult.success(map);}@GetMapping("/sendAllUser")public void sendAllUser(){webSocketServer.sendAllMessage("updateApp");}}
先上传文件嘛,然后将路径什么的,返回给表单提交,简单的。