后端与后端消息推送,直接使用消息中间件即可,后端->多个客户端推送消息,怎么推送呢?
1.消息来源。(由于没有安装redis等数据库,就直接用mysql来记录消息了)
场景:后台处理完一项事务后,需要给所有客户端主动推送消息;如:服务器线程处理完一个任务,然后需要通知当前所有打开客户端;
做法:处理完任务后,把消息存到一个地方。(数据库,redis,本地缓存等等)
2.使用server-sent-events推送。
这个网上有很多实现做法,很简单,就不做阐述了。但是sse 严格来讲,推送了消息,只会有一个客户端收到,不符合场景。
因此,需要在 消息上做处理,使其比如:20个客户端打开了,那么任务处理完后,会把同样信息,推送给20个客户端,且只推送一次。
3.实现。(sse 没有会话id,每次都是新的,所以需要模拟会话,并且要销毁会话;)
业务场景:一个用户,一台电脑,多个浏览器,同时打开了web端,那么需要所有web端接收到消息;一个用户,多台电脑登录,所有电脑web端,都需要接收到消息;多个用户,多台电脑,都打开登录了web端,所有都要接收到消息;一句话:只要web端打开了,就当成一个客户终端。(如果前端可以直接使用消息中间件多好啊)
java后端简单实现:
1.先注册会话;(打开一个客户端,注册一个会话)
package com.hxtx.spacedata.controller.map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.hxtx.spacedata.domain.entity.task.TaskInfoEntity;
import com.hxtx.spacedata.enums.task.TaskInfoStatusEnum;
import com.hxtx.spacedata.mapper.task.TaskInfoDao;
import com.hxtx.spacedata.util.SmartDateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 服务端推送技术 server-sent events
* @description
* @author sbq
* @version 1.0.0
* @date 2020/10/27
*/
@RestController
@Slf4j
public class SSEController {
@Autowired
private TaskInfoDao taskInfoDao;
private static ConcurrentHashMap<String,Long> ssePushUsers = new ConcurrentHashMap<>();
@Scheduled(cron = "0/2 * * * * ?") // 2S执行一次
public void clear() {
//2秒执行一次,时间差>5S 说明客户端关闭了,直接剔除
long now = System.currentTimeMillis();
for (Iterator<Map.Entry<String, Long>> it = ssePushUsers.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, Long> item = it.next();
long time = item.getValue();
log.info(item.getKey()+"注册时间差:"+(now - time)/1000);
if(now - time > 5000){
//5 秒
it.remove();
log.info("剔除客户端:"+item.getKey());
}
}
}
@GetMapping(value="/sse/push/version/get")
public String getVersion(HttpServletRequest request){
HttpSession session = request.getSession();
if(null != session){
return session.getId();
}
return null;
}
/**
* 推送C++ json文件编译情况信息
* @author sunboqiang
* @date 2020/10/29
*/
@GetMapping(value="/sse/push/{version}",produces="text/event-stream;charset=utf-8")
public String push(@PathVariable("version") String version) {
QueryWrapper<TaskInfoEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(TaskInfoEntity::getStatus, TaskInfoStatusEnum.SUCCESS.getStatus());
queryWrapper.lambda().eq(TaskInfoEntity::getSendStatus,0);
List<TaskInfoEntity> list = taskInfoDao.selectList(queryWrapper);
String data = "";
if(CollectionUtils.isEmpty(list)){
//还没有消息,收集等待推送的客户端
ssePushUsers.put(version,System.currentTimeMillis());
data = "data:没有编译消息,当前打开客户端数量:"+ ssePushUsers.size()+"个;" +"\n\n";
} else {
List<Long> drawingIds = list.stream().map(TaskInfoEntity::getDrawingId).distinct().collect(Collectors.toList());
//编译成功,推送消息
if(ssePushUsers.size()>0){
//存在接收客户端
data = "data:有编译成功,drawingIds="+ drawingIds +"\n\n";
ssePushUsers.remove(version);
if(ssePushUsers.size() == 0){
//最后一个客户端推送完成
taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 1);
}
} else {
//没有客户端,直接推送成功
taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 1);
}
}
return data;
}
}
简单解释:
1.先调用接口,获取会话id
sse/push/version/get
2.推送接口,每次都需要前端传递这个会话id,然后存到本地hashmap。
/sse/push/{version}
获取之前存下的消息信息,如果还没推送,则推送给所有hashmap 存的客户端(具体业务逻辑,根据自己具体业务场景)
3.定时任务,剔除长时间没注册的客户端。(没有注册,说明客户端关闭了)
对应前端调用实现代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>sse 测试</title>
</head>
<body>
<div id="msg_from_server"></div>
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
var version = '';
var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
httpRequest.open('GET', 'http://172.16.10.116:8888/sse/push/version/get', true);//第二步:打开连接 将请求参数写在url中 ps:"./Ptest.php?name=test&nameone=testone"
httpRequest.send();//第三步:发送请求 将请求参数写在URL中
/**
* 获取数据后的处理程序
*/
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState == 4 && httpRequest.status == 200) {
var json = httpRequest.responseText;//获取到json字符串,还需解析
version = json;
console.log(json);
console.log(version);
if (!!window.EventSource) {
var source = new EventSource(`http://172.16.10.116:8888/sse/push/${json}`);
s = '';
source.addEventListener('message', function (e) {
s += e.data + "<br/>"
$("#msg_from_server").html(s);
});
source.addEventListener('open', function (e) {
console.log("连接打开.");
}, false);
source.addEventListener('error', function (e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log(e.readyState);
}
}, false);
} else {
console.log("没有sse");
}
}
};
</script>
</body>
</html>
个人任务使用SSE推送消息优点:
1.虽然类似轮询,但是前端与后端只保留了一个请求;而轮询,则是前端一直在请求,性能浪费太大;
2.与websocket相比,简单,轻,容易实现;但适合场景服务端推送给客户端;无法双向通信。
经常使用场景: 比如前端页面,消息统计,消息通知等等