SSE推送技术
SSE全称Server-sent Events,是HTML 5 规范的一个组成部分,具体去MDN网站查看相关文档。该规范十分简单,
SSE推送技术是服务器端与浏览器端之间的通讯协议,通讯协议是基于纯文本的简单协议。
服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流,由不同的事件所组成。每个事件由类型和数据两部分组成,同时每个事件可以有一个可选的标识符。不同事件的内容之间通过仅包含回车符和换行符的空行(“rn”)来分隔。每个事件的数据可能由多行组成。
编辑
如上图所示,每个事件之间通过空行来分隔。每一行都是由键值对组成。如果键为空则表示该行为注释,会在处理时被忽略。例如第10行。第1行表示一个只包含数据的事件。会按照默认事件走(message事件)。第3-4代表一个附带eventID的事件。第6-8代表一个自定义事件。第10-14代表一个多行数据事件,多行数据由换行符链接
key定义有以下几种:
- data,表示该行包含的是数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。
- 类型为 event,表示该行用来声明事件的类型。浏览器在收到数据时,会产生对应类型的事件。默认提供三个标准事件(当然你可以自定义):
编辑
- id,表示该行用来声明事件的标识符。服务器端返回的数据中包含了事件的标识符,浏览器会记录最近一次接收到的事件的标识符。如果与服务器端的连接中断,当浏览器端再次进行连接时,会通过 HTTP 头“Last-Event-ID”来声明最后一次接收到的事件的标识符。服务器端可以通过浏览器端发送的事件标识符来确定从哪个事件开始来继续连接。
- retry,表示该行用来声明浏览器在连接断开之后进行再次连接之前的等待时间。
SSE只适用于高级浏览器,但是注意IE不直接支持。IE上的XMLHttpRequest对象不支持获取部分的响应内容,所以不支持。每次总有IE怪不得快被淘汰了。
SSE VS Websocket
- SSE 只能Server到Client单项,而Websocket是双向通信。
- SSE 比 Websocket 轻量。当然功能要简单的多。开发便利,不牵涉协议升级问题。
- SSE 天然支持断线重连
服务端代码示例
import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.hxtx.spacedata.common.domain.ResponseDTO; import com.hxtx.spacedata.domain.entity.task.TaskInfoEntity; import com.hxtx.spacedata.enums.task.TaskInfoStatusEnum; import com.hxtx.spacedata.mapper.task.TaskInfoDao; 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.HttpServletRequest; 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 tarzan Liu * @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<>(); /** * 如果没有客户端,则直接修改消息已发送 (2分钟执行一次) * @author sunboqiang * @date 2020/11/3 */ @Scheduled(cron = "0 0/2 * * * ?") public void finishSend() { if(ssePushUsers.size()==0){ 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); if(CollectionUtils.isNotEmpty(list)){ taskInfoDao.updateSendStatusByIds(list.stream().map(TaskInfoEntity::getId).collect(Collectors.toList()), 2); } } } /** * 剔除关闭的客户端 * @author sunboqiang * @date 2020/11/3 */ @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) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } 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){ //存在接收客户端 ResponseDTO result = new ResponseDTO(); result.setCode(1); result.setMsg("有新的编译"); result.setSuccess(true); result.setData("drawingIds="+drawingIds); data = "data:"+ JSONObject.toJSONString(result) +"\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; } }