轻量化server-sent-events,java后端推送消息给客户端,并实现所有打开客户端,都收到此一条消息(类似消息中间件topic)...

 后端与后端消息推送,直接使用消息中间件即可,后端->多个客户端推送消息,怎么推送呢?

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相比,简单,轻,容易实现;但适合场景服务端推送给客户端;无法双向通信。

 

经常使用场景: 比如前端页面,消息统计,消息通知等等

Java实现server-sent events,可以使用Servlet 3.0规范中的异步支持。具体步骤如下: 1. 在Servlet中启用异步支持 ```java @WebServlet(urlPatterns = "/sse", asyncSupported = true) public class SseServlet extends HttpServlet { // ... } ``` 2. 获取AsyncContext对象 ```java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); // ... } ``` 3. 设置响应的Content-Type为text/event-stream ```java response.setContentType("text/event-stream"); ``` 4. 通过AsyncContext对象获取响应的输出流,实现数据的推送 ```java PrintWriter out = response.getWriter(); out.write("data: Hello\n\n"); out.flush(); ``` 5. 通过AsyncContext对象设置超时时间和完成回调函数 ```java asyncContext.setTimeout(0); // 禁用超时 asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent event) throws IOException { // 关闭输出流等资源 } // 其他回调函数 }); ``` 完整示例代码如下: ```java @WebServlet(urlPatterns = "/sse", asyncSupported = true) public class SseServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext asyncContext = request.startAsync(); response.setContentType("text/event-stream"); PrintWriter out = response.getWriter(); asyncContext.setTimeout(0); // 禁用超时 asyncContext.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent event) throws IOException { out.close(); } @Override public void onTimeout(AsyncEvent event) throws IOException { out.close(); } // 其他回调函数 }); while (true) { out.write("data: Hello\n\n"); out.flush(); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } } } ``` 上述代码中的while循环可以替换成其他的数据推送逻辑。需要注意的是,由于server-sent events协议是基于HTTP的长连接,因此需要在客户端实现相应的事件监听和重连机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值