在讲Server-Sent Events (SSE) 之前,我们先来看看 HTTP 请求- 响应。一个标准的 HTTP 请求- 响应,需要客户端打开一个连接,将一个 HTTP 请求(如 HTTP GET 请求)发送到服务端,然后接收到 HTTP 回来的响应,如果该响应被完全发送或者接收,服务端就会把连接关闭。通常是由某个客户发起,客户端才会需要请求所有数据。
然而, Server-Sent Events (SSE) 与 HTTP 请求- 响应背道而驰,它是一种机制,客户端一旦建立起客户机-服务器的连接,就能让服务端将数据以异步的方式从服务器推到客户端。当连接由客户端建立完成,服务端就提供数据,并决定新数据“块"可用时将其发送到客户端。当一个新的数据事件发生在服务端时,这个事件被服务端发送到客户端。因此,名称被称为 Server-Sent Events(服务器推送事件)。下面是支持服务端到客户端交互的技术总览:
用比较笼统的一个说法,就是WebSocket能做的,SSE也能做,反之亦然,但是它们还是有差别的,特别是在完成某些任务方面。WebSocket 是一种更为复杂的服务端实现技术,但它是真正的双向传输技术,既能从服务端向客户端推送数据,也能从客户端向服务端推送数据。WebSocket 和 SSE 的浏览器支持率差不多,除了IE。IE是个例外,即便IE11都还不支持原生 SSE,IE10 添加了WebSocket 支持,可见上图。与 WebSocket 相比,SSE 有一些显著的优势。我认为它最大的优势就是便利:不需要添加任何新组件,用任何你习惯的后端语言和框架就能继续使用。你不用为新建虚拟机、弄一个新的IP或新的端口号而劳神,就像在现有网站中新增一个页面那样简单。我喜欢把这称为既存基础设施优势。
SSE 的第二个优势是服务端的简洁。我们将在下节中看到,服务端代码只需几行。相对而言,WebSocket 则很复杂,不借助辅助类库基本搞不定。因为 SSE 能在现有的 HTTP/HTTPS 协议上运作,所以它能直接运行于现有的代理服务器和认证技术。而对 WebSocket 而言,代理服务器需要做一些开发(或其他工作)才能支持,在写这本书时,很多服务器还没有(虽然这种状况会改善)。SSE还有一个优势:它是一种文本协议,脚本调试非常容易。事实上,在本书中,我们会在开发和测试时用 curl,甚至直接在命令行中运行后端脚本。不过,这就引出了 WebSocket 相较 SSE 的一个潜在优势:WebSocket 是二进制协议,而 SSE 是文本协议(通常使用UTF-8编码)。当然,我们可以通过SSE连接传输二进制数据:在 SSE 中,只有两个具有特殊意义的字符,它们是 CR 和LF,而对它们进行转码并不难。但用 SSE 传输二进制数据时数据会变大,如果需要从服务端到客户端传输大量的二进制数据,最好还是用 WebSocket。
WebSocket 相较 SSE 最大的优势在于它是双向交流的,这意味向服务端发送数据就像从服务端接收数据一样简单。用 SSE时,一般通过一个独立的 Ajax 请求从客户端向服务端传送数据。相对于 WebSocket,这样使用 Ajax 会增加开销,但也就多一点点而已。如此一来,问题就变成了“什么时候需要关心这个差异?”如果需要以1次/秒或者更快的频率向服务端传输数据,那应该用 WebSocket。0.2次/秒到1次/秒的频率是一个灰色地带,用 WebSocket 和用 SSE 差别不大;但如果你期望重负载,那就有必要确定基准点。频率低于0.2次/秒左右时,两者差别不大。
从服务端向客户端传输数据的性能如何?如果是文本数据而非二进制数据(如前文所提到的),SSE和WebSocket没什么区别。它们都用TCP/IP套接字,都是轻量级协议。延迟、带宽、服务器负载等都没有区别。在旧版本浏览器上的兼容,WebSocket 难兼容,SSE 易兼容。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 服务器端实时推送技术之 SseEmitter 的用法测试
* <p>
* 测试步骤:
* 1.请求http://localhost:8888/sse/start?clientId=111接口,浏览器会阻塞,等待服务器返回结果;
* 2.请求http://localhost:8888/sse/send?clientId=111接口,可以请求多次,并观察第1步的浏览器返回结果;
* 3.请求http://localhost:8888/sse/end?clientId=111接口结束某个请求,第1步的浏览器将结束阻塞;
* 其中clientId代表请求的唯一标志;
*
* @author syj
*/
@RestController
@RequestMapping("/sse")
public class SseEmitterController {
private static final Logger logger = LoggerFactory.getLogger(SseEmitterController.class);
// 用于保存每个请求对应的 SseEmitter
private Map<String, Result> sseEmitterMap = new ConcurrentHashMap<>();
/**
* 返回SseEmitter对象
*
* @param clientId
* @return
*/
@RequestMapping("/start")
public SseEmitter testSseEmitter(String clientId) {
// 默认30秒超时,设置为0L则永不超时
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitterMap.put(clientId, new Result(clientId, System.currentTimeMillis(), sseEmitter));
return sseEmitter;
}
/**
* 向SseEmitter对象发送数据
*
* @param clientId
* @return
*/
@RequestMapping("/send")
public String setSseEmitter(String clientId) {
try {
Result result = sseEmitterMap.get(clientId);
if (result != null && result.sseEmitter != null) {
long timestamp = System.currentTimeMillis();
result.sseEmitter.send(timestamp);
}
} catch (IOException e) {
logger.error("IOException!", e);
return "error";
}
return "Succeed!";
}
/**
* 将SseEmitter对象设置成完成
*
* @param clientId
* @return
*/
@RequestMapping("/end")
public String completeSseEmitter(String clientId) {
Result result = sseEmitterMap.get(clientId);
if (result != null) {
sseEmitterMap.remove(clientId);
result.sseEmitter.complete();
}
return "Succeed!";
}
private class Result {
public String clientId;
public long timestamp;
public SseEmitter sseEmitter;
public Result(String clientId, long timestamp, SseEmitter sseEmitter) {
this.clientId = clientId;
this.timestamp = timestamp;
this.sseEmitter = sseEmitter;
}
}
}
<script>
//记录加载次数
var time=1;
if (typeof (EventSource) !== "undefined") {
var source = new EventSource("/sse/start?clientId=111");
console.log(source);
source.addEventListener("事件名", function(e) {
document.getElementById("result").innerHTML = e.data;
}, false);//使用false表示在冒泡阶段处理事件,而不是捕获阶段。
} else {
document.getElementById("result").innerHTML = "抱歉,你的浏览器不支持 server-sent 事件...";
}
</script>