Server-Sent Events(SSE) 简单实现和避坑

服务器向浏览器推送信息,除了 WebSocket,还有一种方法:Server-Sent Events(以下简称 SSE)。

一、客户端API(EventSource 对象)

各浏览器支持情况: https://caniuse.com/eventsource

 

前端测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" type="text/css" href="static/css/home.css">
    <script>
        window.onload = ()=> {
            if (window.EventSource) {
                let source = new EventSource("/mySpringMVC/sse/subscribe");
                let s = '';
                // 
                source.addEventListener('message', function(e) {
                    document.querySelector("p").innerText = e.data;
                })
                source.addEventListener('open',function(e){
                    console.log("connect is open");
                },false);
                source.addEventListener('error',function(e){
                    if(e.readyState == EventSource.CLOSE){
                        console.log("connect is close");
                    }else{
                        console.log(e.readyState);
                    }
                },false);
            } else {
                alert("浏览器不支持EventSource");
            }
        }
    </script>
</head>
<body>
    <p></p>
</body>
</html>
View Code

对应后端

package MySpringMVC;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.WebAsyncTask;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;

@Controller
@RequestMapping(value = "/sse")
public class SSEController {
    // 新建一个容器,保存连接,用于输出返回
    private Map<String, PrintWriter> responseMap = new ConcurrentHashMap<>();


    // 发送数据给客户端
    private void writeData(String id, String msg, boolean over) throws IOException {
        PrintWriter pw = responseMap.get(id);
        if (pw == null) {
            return;
        }
        pw.println(msg);
        pw.println();
        pw.flush();
        if (over) {
            responseMap.remove(id);
        }
    }

    // 推送
    @RequestMapping(value = "subscribe")
    @ResponseBody
    public WebAsyncTask<Void> subscribe(@RequestParam(defaultValue = "id") String id, HttpServletResponse response) {
        response.setHeader("Content-Type", "text/event-stream");
        response.setCharacterEncoding("utf-8");
        Callable<Void> callable = () -> {
            responseMap.put(id, response.getWriter());
            writeData(id, "data:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()) , false);
            return null;
        };

        // 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称
        WebAsyncTask<Void> webAsyncTask = new WebAsyncTask<>(30000, callable);
        // 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的
        webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调"));

        // 这两个返回的内容,最终都会放进response里面去===========
        webAsyncTask.onTimeout(() -> {
            responseMap.remove(id);
            System.out.println("超时了!!!");
            return null;
        });
        // 备注:这个是Spring5新增的
        webAsyncTask.onError(() -> {
            System.out.println("出现异常!!!");
            return null;
        });
        return webAsyncTask;
    }

    @RequestMapping(value = "subscribe2")
    public void subscribe2(HttpServletResponse response) {
        response.setHeader("Content-Type", "text/event-stream");
        response.setCharacterEncoding("utf-8");
        try {
            PrintWriter pw = response.getWriter();
            pw.println("data:" + new SimpleDateFormat("YYYY-MM-dd hh:mm:ss").format(new Date()));
            pw.println();
            pw.flush();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
View Code

 

注意(避坑):

1、数据内容用data字段表示。

    上面代码中,事件对象的 "data:"属性就是服务器端传回的数据(文本格式),只有这种格式EventSource 的message事件才会触发。

: this is a test stream\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

2、不同事件的内容之间通过仅包含回车符和换行符的空行(“\n\n”)来分隔 (上文代码用了两个”pw.println()“)

data: first event
 
data: second event
id: 100
 
event: myevent
data: third event
id: 101
 
: this is a comment
data: fourth event
data: fourth event continue

 

效果(每隔三秒刷新当前时间)

 

 

详细文档请看文章:http://www.ruanyifeng.com/blog/2017/05/server-sent_events.html 

 

posted @ 2021-10-18 22:12  隐语者  阅读(2744)  评论(0编辑  收藏  举报