SSE的JFinal实现

最近接触到了SSE,先说一下概念:

SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。    

不同于WebSocket,SSE 是单向通道,只能服务器向客户端发送消息。但对于一般应用来说,客户端向服务器发消息可以直接GET/POST,因此并不是很迫切;而服务器下发才是有一定应用场景的,可以避免客户端频繁的轮询服务器。

SSE 最大的优势是简单,对服务器端和客户端依赖都很少:

  • 服务端:无额外的引用,只需要实现一个Servlet就可以(Servlet 3.0),10几行代码

  • 客户端:只要浏览器支持EventSource,4、5行代码就可以实现。目前主流浏览器基本都支持:

1158910-20180618205141152-972356492.png

首先实现服务器端,一个Handler,URL为/sse/demo,其中关键的代码是

  1. request.startAsync(),启动一个异步请求,获得了AsyncContext异步请求的上下文

  2. 使用AsyncContext.getResponse()获得输出流向客户端发送数据

  3. 数据格式是用回车换行符(\r\n)分隔的多行字符串:

        id:消息ID\r\n

        event:消息类型\r\n

        data:数据\r\n

public class SSEHandler extends Handler {

  private final static int DEFAULT_TIME_OUT = 30 * 1000;

  @Override
  public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {

    if (target.endsWith("/sse/demo")) {
      response.setContentType("text/event-stream");
      response.setCharacterEncoding("UTF-8");
      request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);

      AsyncContext actx = request.startAsync(request, response);
      actx.setTimeout(DEFAULT_TIME_OUT);
      actx.addListener(new AsyncListener() {
        @Override
        public void onComplete(AsyncEvent arg0) throws IOException {
          // TODO Auto-generated method stub
          System.out.println("[echo]event complete:" + ((HttpServletRequest)arg0.getSuppliedRequest()).getSession().getId());
        }

        @Override
        public void onError(AsyncEvent arg0) throws IOException {
          // TODO Auto-generated method stub
          System.out.println("[echo]event has error");
        }

        @Override
        public void onStartAsync(AsyncEvent arg0) throws IOException {
          // TODO Auto-generated method stub
          System.out.println("[echo]event start:" + arg0.getSuppliedRequest().getRemoteAddr());
        }

        @Override
        public void onTimeout(AsyncEvent arg0) throws IOException {
          // TODO Auto-generated method stub
          System.out.println("[echo]event time lost");
        }
      });
      new Thread(new AsyncWebService(actx)).start();
      isHandled[0] = true;
    }
    else {
      if (next != null) {
        next.handle(target, request, response, isHandled);
      }
    }
  }
}

class AsyncWebService implements Runnable {
  AsyncContext ctx;

  public AsyncWebService(AsyncContext ctx) {
    this.ctx = ctx;
  }

  public void run() {
    try {
      PrintWriter out = ctx.getResponse().getWriter();
      out.println("event:demo\r\nid:101\r\ndata:中文" + new Date() + "\r\n"); //js页面EventSource接收数据格式:id:[消息Id]\r\nevent:[事件类型]\r\ndata:[数据]\r\n"

      out.flush();
      ctx.complete();
      //等待十秒钟,以模拟业务方法的执行
      Thread.sleep(10000);
    }
    catch (Exception e) {
      e.printStackTrace();
    }

  }

}

客户端代码:

  1. new EventSource('/sse/demo'),连接到服务器对应的URL

  2. source.addEventListener('demo', function(e) {...}, false),添加监听器

<html>
<head>
<meta charset="utf-8" />
<script type="text/javascript">
	//jsp页面js脚本
  if (!!window.EventSource) { //EventSource是SSE的客户端.此时说明浏览器支持EventSource对象
	  var source = new EventSource('/sse/demo');//发送消息
	  s = '';

	  source.addEventListener('demo', function(e) {
			console.info(e);
		  s += e.data + "<br/>";
		  document.querySelector("#msgFromPush").innerHTML = (s);
	  }, false);//添加客户端的监听

	  source.addEventListener('open', function(e) {
		  console.log("连接打开");
	  }, false);

	  source.addEventListener('error', function(e) {
		  if (e.currentTarget.readyState == EventSource.CLOSED) {
			  console.log("连接关闭");
		  } else {
			  console.log(e.currentTarget.readyState);
		  }
	  });
  } else {
	  console.log("您的浏览器不支持SSE");
  }
</script>
</head>
<body>
	<div id="msgFromPush" />
</body>
</html>

以上,完成。

看看效果:

客户端:

傲游截图20191222104133.png

服务器端,现在打印的是SessionId,每次调用都不同是因为客户端未登录,如果客户端登录了,SessionId就固定了:

傲游截图20191222104310.png

是不是够简单?以后可以做为备选方案了。

参考资料:Java SSE 服务器推送WEB页面接收数据

评论区

马小酱

2019-12-23 09:56

很有用啊,准备应用到系统中啦,谢谢

flash866

2019-12-24 10:11

能讲下网络原理吗?我的理解是,只要是websocket,不都是建立的长连接吗?如果已经建立了长连接,怎么还会有来自客户端的轮询呢?服务端直接通过建立的连接推送数据不就行了吗?

苏伟伟

2019-12-24 11:45

undertow好像不行
UT010026: Async is not supported for this request, as not all filters or Servlets were marked as supporting async

kingyl007

2019-12-25 18:08

@flash866 websocket不是轮询,与SSE的区别在于websocket是双向的,SSE是服务器向客户端发送单向的。SSE比起websocket的优势是简单,不用增加依赖

kingyl007

2019-12-25 18:16

@苏伟伟 在web.xml里给filter增加async-supported参数试试
<filter>
<filter-name>jfinal</filter-name>
<filter-class>com.jfinal.core.JFinalFilter</filter-class>
<async-supported>true</async-supported>
</filter>
回复不能直接贴代码好麻烦 @JFinal

航程序员

2021-02-22 15:26

AsyncContext 作用是什么

热门分享

扫码入社