1 HTTP
传统的请求响应模式,客户端向服务端发请求,服务端才会返回响应,并且做完这一对操作,连接断开,需要频繁建立连接
优势:实现简单、兼容性好
缺点:服务端无法主动向客户端推送数据,部分场景需要客户端不断地轮训请求数据
2 WebSocket
全双工通信协议,允许建立一个持久连接,客户端和服务端都可以随意发送数据
优点:实时性强
缺点:需要处理一些握手、链接维护等问题,每个连接建立的资源较多
3 SSE
又叫Server Send Events,客户端需要先和服务端建立一个连接,一旦这个连接建立起来,服务端即可不断地向客户端推送数据
当然只适合于频繁单向通信的情况,在某些情况下,浏览器可能对SSE连接数有限制,这可能会影响大规模应用的性能
3.1 SSE基本使用
1 定义SSESever
在这里我们需要定义一些方法:例如连接建立、发送消息等等,每个用户的连接都会创建一个单独的sse对象,我们可以缓存起来;同时SseEmitter提供了很多编程型方法实现来处理注册调的结果
public class SSESever {
private static final Map<String, SseEmitter> sceClinents = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId){
SseEmitter sseEmitter = new SseEmitter(0L);
//注册回调
sseEmitter.onTimeout(timeOutCallback(userId));
sseEmitter.onCompletion(completeCallback(userId));
sseEmitter.onError(errorCallback(userId));
sceClinents.put(userId, sseEmitter);
log.info("用户{}建立SSE连接", userId);
return sseEmitter;
}
public static Runnable timeOutCallback(String userId){
return () -> {
sceClinents.remove(userId);
log.info("用户{}移除SSE连接", userId);
};
}
public static Runnable completeCallback(String userId){
return () -> {
sceClinents.remove(userId);
log.info("用户{}移除SSE连接", userId);
};
}
public static Consumer<Throwable> errorCallback(String userId){
return throwable -> {
sceClinents.remove(userId);
log.info("用户{}移除SSE连接", userId);
};
}
public static void sendMessage(String userId, String message, SSEMsgType msgType){
if (CollectionUtils.isEmpty(sceClinents)){
return;
}
SseEmitter sseEmitter = sceClinents.get(userId);
if(sseEmitter != null){
sendEmitterMessage(sseEmitter, userId, message, msgType);
}
}
private static void sendEmitterMessage(SseEmitter sseEmitter, String userId, String message, SSEMsgType msgType){
try {
SseEmitter.SseEventBuilder event = SseEmitter.event()
.id(userId)
.data(message)
.name(msgType.type);
sseEmitter.send(event);
} catch (Exception e){
log.error("用户{}发送SSE消息失败", userId);
sceClinents.remove(userId);
}
}
2 前后端交互 后端实现controller(注意:Accept需要传入text/event-stream)
@GetMapping(path = "connect",produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter connect(@RequestParam String userId) {
return SSESever.connect(userId);
}
@GetMapping(path = "sendMessage")
public Object connect(@RequestParam String userId,@RequestParam String msg) {
SSESever.sendMessage(userId,msg, SSEMsgType.MESSAGE);
return "ok";
}
前端使用EventSource即可完成消息接受和连接建立
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 客户端示例</title>
</head>
<body>
<h1>SSE 客户端示例1</h1>
<div id="messages"></div>
</body>
</html>
<script>
let source = null;
let userId = Math.random().toString(36).substr(2, 9); // 生成一个随机字符串作为 userId
if (window.EventSource) {
// 创建一个EventSource对象,建立SSE连接
source = new EventSource('http://10.1.74.152:9999/sse/connect?userId=' + userId);
// http://127.0.0.1:5501/
// 建立连接成功,则会触发open事件
source.addEventListener('open', function(e) {
console.log('建立连接成功...');
var text = document.getElementById('messages').innerHTML;
text += '<br> 建立连接成功...';
document.getElementById('messages').innerHTML = text;
}, false);
// 客户端接收到服务器发来的消息时,会触发message事件
source.addEventListener('message', function(e) {
var msg = e.data;
var text = document.getElementById('messages').innerHTML;
text += '<br>' + msg;
document.getElementById('messages').innerHTML = text;
}, false);
// add 事件,stream 流式推送
source.addEventListener('add', function(e) {
var msg = e.data;
var text = document.getElementById('messages').innerHTML;
text += '<br>' + msg;
document.getElementById('messages').innerHTML = text;
}, false);
// 用户自定义事件
source.addEventListener('custom_event', function(e) {
var msg = e.data;
var text = document.getElementById('messages').innerHTML;
text += '<br>' + "自定义事件:" + msg;
document.getElementById('messages').innerHTML = text;
}, false);
// SSE完成时调用的事件
source.addEventListener('finish', function(e) {
console.log('当前SSE事件推送完毕...');
}, false);
// 连接发生错误时,会触发error事件
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log('连接已关闭...');
var text = document.getElementById('messages').innerHTML;
text += '<br> 连接已关闭...';
document.getElementById('messages').innerHTML = text;
} else {
console.log('发生错误...');
}
})
} else {
console.log('您的浏览器不支持SSE');
// closeSSE();
}
function closeSSE() {
source.close();
}
</script>