哈喽,大家好,最近在工作中需要用到后端实时跟前端实时推送数据,目前常用的有两种实现方式 websocket和sse 。 废话不多说 上干货!!!
1. SSE和 Websocket 的区别
SSE说明:
SSE是一种基于HTTP的单向通信机制,用于服务器向客户端推送数据。它的工作原理如下:
建立连接:客户端通过发送HTTP请求与服务器建立连接。在请求中,客户端指定了接收事件的终点(Endpoint)。
保持连接:服务器接收到连接请求后,保持连接打开,并定期发送事件数据给客户端。
事件流:服务器使用 “Content-Type: text/event-stream” 头部标识SSE连接,并使用特定格式的数据(事件流)发送给客户端。
客户端处理事件:客户端通过JavaScript的 EventSource 接口监听SSE连接,一旦接收到事件,就可以处理数据并更新页面。
SSE的特点和适用场景:
单向通信:SSE是从服务器到客户端的单向通信模型,只能由服务器推送数据给客户端。
实时更新:SSE适用于需要实时更新数据的应用场景,如股票行情、新闻推送等。
简单易用:使用SSE相对简单,无需额外的库或框架支持,可以直接使用浏览器的原生API进行开发。
Websocket 说明:
WebSocket是一种全双工的通信协议,它通过在客户端和服务器之间建立持久连接,实现双向通信。WebSocket的工作原理如下:
握手阶段:客户端向服务器发送WebSocket握手请求,服务器返回握手响应。在这个阶段,客户端和服务器协商选择协议和版本。
建立连接:握手成功后,客户端和服务器之间建立持久连接,可以进行双向数据传输。
双向通信:一旦连接建立,客户端和服务器都可以主动发送消息给对方。数据可以以文本或二进制格式进行传输。
断开连接:当任一方决定关闭连接时,可以发送关闭帧来终止连接。
WebSocket的特点和适用场景:
双向通信:WebSocket支持双向通信,客户端和服务器可以互相发送消息。
实时互动:WebSocket适用于实时互动的应用场景,如聊天应用、协作编辑等。
复杂性和灵活性:相对于SSE,WebSocket更为灵活,可以处理更复杂的通信需求。它允许自定义消息格式、心跳检测、连接状态管理等。
SSE与WebSocket的比较
现在我们来比较一下SSE和WebSocket的异同点:
通信模型:SSE是单向通信模型,只能由服务器向客户端推送数据,而WebSocket是双向通信模型,客户端和服务器可以互相发送消息。
连接性:SSE使用长轮询或HTTP流技术,而WebSocket使用持久连接。SSE需要频繁地发起HTTP请求来获取数据,而WebSocket只需在握手阶段建立一次连接,然后保持连接打开。
实时性:WebSocket提供了更低的延迟和更高的实时性,因为它支持双向通信,可以立即将数据推送给客户端。SSE虽然也可以实现实时性,但由于其单向通信模型,需要服务器定期发送数据。
浏览器支持:WebSocket在现代浏览器中得到广泛支持,包括Chrome、Firefox、Safari等。SSE在大多数现代浏览器中也有支持,但在某些旧版本浏览器中可能存在兼容性问题。
API复杂性:WebSocket提供了更灵活和复杂的API,可以处理更高级的通信需求。SSE相对简单,使用浏览器的原生 EventSource 接口即可。
2.下面是我搭建的一个简单的SSE整合SpringBoot的项目,来吧兄弟们 开整!!
后端Controller代码:
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/sse/test")
public class SseController {
private final SseService sseService;
/**
* 创建新连接
* @return
*/
@GetMapping("/start")
public Object createSession() throws IOException {
return sseService.createSession();
}
/**
* 断开连接
* @return
*/
@GetMapping("/close")
public void cloneSession(@RequestParam String clientId){
sseService.cloneSession(clientId);
}
}
后端service层代码
public interface SseService {
Object createSession() throws IOException;
void cloneSession(String clientId);
}
后端service的ipl层代码
import com.xjww.ss.service.SseService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class SseServiceImpl implements SseService {
private final static Map<String, SseEmitter> SSE_CACHE= new ConcurrentHashMap<>();
/**
* 创建连接
* @return
*/
public synchronized SseEmitter createSession() throws IOException{
// 过期时间设置为0,表示永不过期
SseEmitter sseEmitter = new SseEmitter(0L);
String id = UUID.randomUUID().toString().replace("-","");
if (!SSE_CACHE.containsKey(id)){
SSE_CACHE.put(id,sseEmitter);
log.info("客户端:{} 新建连接成功,当前客户端总数为【{}】",id,SSE_CACHE.size() );
}
return sseEmitter;
}
@Override
public void cloneSession(String clientId) {
if (SSE_CACHE.containsKey(clientId)){
SSE_CACHE.get(clientId).complete();
SSE_CACHE.remove(clientId);
log.info("客户端:【{}】 断开成功,当前剩余客户端总数为【{}】",clientId,SSE_CACHE.size());
}
}
/**
* 定时任务 用于测试后端推送的数据
*/
@Scheduled(fixedDelay = 3, initialDelay = 1,timeUnit = TimeUnit.SECONDS)
public void job(){
if (SSE_CACHE.size()>0){
String msg ="消息"+UUID.randomUUID().toString();
System.out.println("msg = " + msg);
for (Map.Entry<String, SseEmitter> entry : SSE_CACHE.entrySet()) {
SseEmitter sseEmitter =SSE_CACHE.get(entry.getKey());
try {
sseEmitter.send(SseEmitter.event().reconnectTime(1000).id(entry.getKey()).data(msg));
}catch (IOException e){
SSE_CACHE.remove(entry.getKey());
}
}
}
}
}
前端html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>菠萝吹雪 -SSE轻量级化服务器端消息推送方案</title>
</head>
<body>
<div>sse测试</div>
<div id="result"></div>
</body>
</html>
<script>
var clientId;
var source = new EventSource('/sse/test/start?id=qew');
source.onmessage =function (event) {
text =document.getElementById('result').innerText;
text=event.data;
document.getElementById('result').innerText=text;
console.log(event.data);
clientId =event.lastEventId
};
source.onerror = function (e){
if (e.readyState === EventSource.CLOSED){
console.log("连接关闭");
}
}
<!-- 与后端建立连接成功-->
source.onopen = function (event){
console.log(event);
};
/**
* 刷新页面
*/
window.addEventListener('beforeunload', function(){
closeSession()
});
function closeSession(){
source.close();
const httpRequest = new XMLHttpRequest();
httpRequest.open('GET','/sse/test/close?clentId'+clientId,true);
httpRequest.send();
console.log("close");
}
</script>
至此是测试demo的所有代码,后面我会着重的强调一下测试过程。
测试结果:
1.启动后端项目,如图所示
2.打开前端的demo,首次建立长连接,如图所示
3.观察后端这时候已经建立连接,并且已经接收到后端推送的数据。
总结 : 后端是在实时推送是一条一条的推送数据,所以前端也是一条条的数据在接受,当然这是轻量级的解决方案,后面如果大家需要,我也会出websocker的推送数据方案,希望能帮到大家,ok~