全局消息是推送,实现app在线更新,WebSocket
背景 :
开发人员开发后app后打包成.apk文件,上传后通知厂区在线用户更新app。
那么没在线的怎么办?因为我们在上一篇博客中写了,在app打开的时候回去校验是否需要更新了,所以已经完成了闭环。
即时通讯首先想到的就是WebSocket
- 1.我们定义全局的WebSocket
- 2.在全局监听,当监听到指定消息的时候弹窗更新,下载逻辑也就是下载最新的apk,在上一篇博客写了,点击下方链接。
uniapp:实现手机端APP登录强制更新,从本地服务器下载新的apk更新,并使用WebSocket,实时强制在线用户更新
但是有一个问题,就是手持机少还可以,要是多的话,几百台连接还不是不太合适?,所以用登录的时候检测更新是最好的
使用post调用测试
java
@GetMapping("/sendAllUser")
public void sendAllUser(){
webSocketServer.sendAllMessage("updateApp");
}
1.在main.js中定义全局的WebSocket
js
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连接代码,咕咕在其基础上做了一些修改,添加了部分方法
java
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;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public 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, "连接成功");
}
}
/**
* 连接关闭时处理
*/
@OnClose
public void onClose(Session session)
{
webSockets.remove(this);
LOGGER.info("\n 关闭连接 - {}", session);
// 移除用户
WebSocketUsers.remove(session.getId());
// 获取到信号量则需释放
SemaphoreUtils.release(socketSemaphore);
}
/**
* 抛出异常时处理
*/
@OnError
public 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);
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public 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.通知所有用户更新
咕咕这里是在用户上传完,提交表单成功后发送消息的,大家可自定义。
java
/**
* 新增app版本
*/
@PreAuthorize("@ss.hasPermi('system:appversion:add')")
@Log(title = "app版本", businessType = BusinessType.INSERT)
@PostMapping
public 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));
}
下面的是,文件上传的代码,大家可以随意参考,基于若依的一些修改,本来若依也是开源的,所以这个也就不做删减了。
java
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 {
@Autowired
private WebSocketServer webSocketServer;
@Autowired
private ServerConfig serverConfig;
@Autowired
private 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.apk
SysAppVersion 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的Header
response.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");
}
}
先上传文件嘛,然后将路径什么的,返回给表单提交,简单的。