服務端推送技術 Server-sent Events springBoot程式碼示例

洛陽泰山發表於2020-12-03

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;
    }

}

 

相關文章