轻量化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 ssePushUsers = new ConcurrentHashMap<>();

    @Scheduled(cron = "0/2 * * * * ?") // 2S执行一次
    public void clear() {
        //2秒执行一次,时间差>5S 说明客户端关闭了,直接剔除
        long now = System.currentTimeMillis();
        for (Iterator> it = ssePushUsers.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry 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 queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(TaskInfoEntity::getStatus, TaskInfoStatusEnum.SUCCESS.getStatus());
        queryWrapper.lambda().eq(TaskInfoEntity::getSendStatus,0);
        List list = taskInfoDao.selectList(queryWrapper);
        String data = "";
        if(CollectionUtils.isEmpty(list)){
            //还没有消息,收集等待推送的客户端
            ssePushUsers.put(version,System.currentTimeMillis());
            data = "data:没有编译消息,当前打开客户端数量:"+ ssePushUsers.size()+"个;" +"\n\n";
        } else {
            List 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.定时任务,剔除长时间没注册的客户端。(没有注册,说明客户端关闭了)

 

对应前端调用实现代码:




	
    sse 测试




 

个人任务使用SSE推送消息优点:

    1.虽然类似轮询,但是前端与后端只保留了一个请求;而轮询,则是前端一直在请求,性能浪费太大;

    2.与websocket相比,简单,轻,容易实现;但适合场景服务端推送给客户端;无法双向通信。

 

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

你可能感兴趣的:(数据库,java,websocket,ajax,session)