实时通信技术是基于Web开发的一项重要技术。网站需要前端和后端通信,所以数据刷新时间就是获取信息的时间。为了准确、快速地获取信息,需要尽可能提高信息刷新效率。 。
常见的实时通信技术:
通讯方式 | 阿贾克斯 | 彗星 | WebSockets | 上证所 |
---|---|---|---|---|
描述 | 短轮询是浏览器提交的表单查询 | 长轮询是指服务器收到请求后,如果有数据,则立即响应请求;如果没有数据,则等待一段时间,直到有数据,立即响应请求;如果时间到了还没有数据,就会响应http请求(定时刷新) | WebSocket实现了连接后双方进行通信的功能。首先,客户端发出 WebSocket 请求,服务器通过 TCP 三向握手进行响应。一旦建立了该连接,它就保留在客户端和服务器之间,并且可以在两者之间直接传输数据。 | sse场景下,客户端发起请求,并保持连接。当服务器有数据时,可以将数据返回给客户端。此返回可以是多个间隔。 sse是单通道,只有服务器端可以向客户端发送消息。 |
协议 | http | http | 网络套接字 | http |
Ajax实现方案
Ajax实时通信实现起来比较方便。您可以通过浏览器DOM元素设置页面刷新时间来动态获取后端数据。
- 添加 setinterval() 函数
<script language="javascript">
setInterval(function(){
window.location.reload();
},3000); //每隔3000毫秒刷新一次
</script>
- Mata 添加内容元素并定期刷新它们
<meta http-equiv="refresh" content="20">
- 预定页面跳转
<mata http-equiv="refresh" content"3,url=#">
//定时跳转也会刷新数据
通过js的动态刷新技术,在刷新的同时添加Ajax通信技术即可实现动态刷新。
基于Ajax的长轮询方法
/* setInterval(() => { mychart2.clear(); axios({ method:'get', url:'http://localhost:8100/json', }).then(function (response) { console.log(response) temp=response.data }) i=i+1; option.xAxis.data.push(i); option.xAxis.data.shift(); option.series[0].data.push(temp.data); option.series[0].data.shift(); mychart2.setOption(option); }, 3000); */
上函数通过在定时刷新里面添加了axios技术在间隔时间内查询数据并添加到列表中以实现动态刷新。如下图所示:
这种Ajax长轮询技术(Comet)的缺点是无法满足即时通讯等富交互应用的实时数据更新需求。
基于iframe的长轮询方法
此方法返回一个新的 HTML,其中标记了 src 属性的更改。服务器将返回的数据作为回调函数的参数,浏览器收到数据后执行脚本。
WebSocket实现方案
Websocket通过握手方式连接,形成半双工全通道通信,服务器端和客户端都可以主动相互通信。 WebSocket允许服务器直接向客户端发送信息,而不用等待客户端发起请求,服务器才返回信息。 (与轮询相比,可以更好地节省服务器资源和带宽,并且可以更实时地进行通信。
后端编写:
package com.example;
import java.io.IOException;
import java.util.logging.Logger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
/** * @Class: Test * @Description: 简单websocket demo */
@ServerEndpoint(value="/websocketTest/{userId}")
public class WsTest {
private Logger logger = Logger.getLogger("WebSocket");
private static String userId;
int i=0;
//连接时执行
@OnOpen
public void onOpen(@PathParam("userId") String userId,Session session) throws IOException{
this.userId = userId;
logger.info("有新的链接!");
System.out.println("新连接:"+userId);
}
//关闭时执行
@OnClose
public void onClose(){
logger.info("有链接关闭!");
System.out.println("连接:"+this.userId);
}
//收到消息时执行
@OnMessage
public void onMessage(String message, Session session) throws IOException {
System.out.println("收到用户"+this.userId+"的消息"+message);
//session.getBasicRemote().sendText("服务器 "+this.userId+" 发送来一条消息的消息 "); //回复用户
i++;
session.getBasicRemote().sendText(String.valueOf(i)); //返回数据
}
//连接错误时执行
@OnError
public void onError(Session session, Throwable error){
System.out.println("用户id为:"+this.userId+"的连接发送错误");
error.printStackTrace();
}
}
前端编写:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
websocket Demo---- user000 <br />
<input id="text" type="text" />
<button onclick="send()"> Send </button>
<button onclick="closeWebSocket()"> Close </button>
<div id="message"> </div>
<script type="text/javascript"> //判断当前浏览器是否支持WebSocket if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8080/demo/websocketTest/user000"); console.log("link success") }else{
alert('Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function(){
setMessageInnerHTML("error"); }; //连接成功建立的回调方法 websocket.onopen = function(event){
setMessageInnerHTML("open"); } console.log("-----") //接收到消息的回调方法 websocket.onmessage = function(event){
setMessageInnerHTML(event.data); } //连接关闭的回调方法 websocket.onclose = function(){
setMessageInnerHTML("close"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function(){
websocket.close(); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>'; } //关闭连接 function closeWebSocket(){
websocket.close(); } //发送消息 function send(){
var message = document.getElementById('text').value; websocket.send(message); } </script>
</body>
</html>
启动tomcat服务器,访问websocket前端接口返回数据:
SSE实现方案
SseEmitter
的用法是使用 HTTP 做服务端数据推送应用的技术。SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。
SSE 和 WebSocket 做同样的事情。当您需要使用新数据部分更新 Web 应用程序时,SSE 可以做到这一点,而无需任何用户操作。 SSE是一种单向通道,服务器只能向客户端发送消息。如果客户端需要向服务器发送消息,则需要一个新的HTTP请求。这将比 WebSocket 的双工通道有更大的开销。
这是SpringMVC提供的一项技术,可以将数据从服务器实时推送到客户端。使用方法非常简单。只需要在Controller中提供一个接口,创建并返回SseEmitter对象,在另一个接口上调用其send方法发送数据即可。数据。因此sse需要在spring mvc或者集成度更高的框架中使用,不能在servlet中使用。
SpringMVC内置的SseEmitter类有几个内置方法可以让我非常方便的使用服务器推送事件(Server Sent Event)。
使用sse之前有几个关于sse的前端知识需要了解。:
-
sse机制不同于传统的“请求-响应”模型,在前端必须使用新建的
EventSource
对象请求一个sse,
然后监听此对象的message
事件以接收后端推送的值。 -
当前端请求SSE时,服务器不会响应请求,达到“连接”效果,直到后端主动关闭且事件不超时。一旦后端响应请求(后端主动或“连接”自动超时)就代表“连接”结束;
-
一旦前端收到响应,EventSource对象会立即自动重新连接,以保证连接的有效性。
const source = new EventSource('http://localhost/sse');
source.addEventListener('message', (message: any) => {
console.log(message.data);
});
SseEmitter的使用步骤:
- 只需直接创建 SseEmitter 对象即可。创建时可以设置超时时间(默认为30000毫秒)。当到达这个时间时,请求将立即得到响应(连接将失败)。
SseEmitter sse() throws IOException {
SseEmitter event = new SseEmitter(10000L);
// 添加一些额外配置
event.send(
SseEmitter.event()
.reconnectTime(1000L)
.id("123")
);
concurrentHashMap.put(1, event);
return event;
}
-
发送事件可以调用创建好的对象的
send
方法,发送的数据即为在前端的接收到的message
事件中的data
属性值 -
发送的数据默认被识别为字符串。如果发送Map,则需要在前端用JSON对象解析,得到json数据。
event.send(
SseEmitter.event()
.data("值")
// 更改原来的“message”事件名称
.name("event")
);
- 对于服务器端返回的响应,浏览器端需要在 JavaScript 中使用
EventSource
对象来进行处理。EventSource 使用的是标准的事件监听器方式,只需要在对象上添加相应的事件处理方法即可。EventSource 提供了三个标准事件。
var es = new EventSource('events');
es.onmessage = function(e) {
console.log(e.data);
};
es.addEventListener('myevent', function(e) {
console.log(e.data);
});
在指定 URL 创建 EventSource 对象后,可以通过 onmessage 和 addEventListener 方法添加事件处理方法。当服务器端有新的事件发生时,就会调用相应的事件处理方法。 EventSource对象的onmessage属性的作用类似于addEventListener(‘message’),但是onmessage属性只支持一种事件处理方法。
- SSE对象是服务端向客户端发送信息,因此需要周期性执行,需要借助线程功能。
ScheduledExecutorService
是基于ExecutorService的功能实现的延迟和周期执行任务的功能。每个任务以及每个任务的每个周期都会提交到线程池中由线程去执行,所以任务在不同周期内执行它的线程可能是不同的。ScheduledExecutorService接口的默认实现类是ScheduledThreadPoolExecutor。在周期执行的任务中,如果任务执行时间大于周期时间,则会以任务时间优先,等任务执行完毕后才会进入下一次周期。如下面所示:
/* 数据实时推送sse数据推送 */
//ScheduledExecutorService```是基于ExecutorService的功能实现的延迟和周期执行任务的功能
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
@GetMapping(value = "/timeclick")
public SseEmitter subscribeCC(){
//获取数据库点击量数据
//List<ClickTimes> clickTimes = null;
SseEmitter sseEmitter = new SseEmitter(0L);
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
sseEmitter.send(opt1Mapper.select_all_times());
} catch (IOException e) {
e.printStackTrace();
}
}
}, 10,5, TimeUnit.SECONDS); //每个任务10执行时间,每个5秒执行一次
return sseEmitter;
}
借助
ScheduledExecutorService
的scheduleWithFixedDelay
方法周期性执行任务。
上交所案例
//接口是在spring boot的controller下实现的
/* 数据实时推送sse数据推送 */
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
@GetMapping(value = "/timeclick")
public SseEmitter subscribeCC(){
//获取数据库点击量数据
//List<ClickTimes> clickTimes = null;
SseEmitter sseEmitter = new SseEmitter(0L);
executorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
sseEmitter.send(opt1Mapper.select_all_times());
} catch (IOException e) {
e.printStackTrace();
}
}
}, 10,5, TimeUnit.SECONDS); //每个任务10执行时间,每个5秒执行一次
return sseEmitter;
}
function initEventSource() {
//请求地址,静态
const url = "/timeclick"
//接受实时推送数据的监听EventSource对象接收
const dataSource = new EventSource(url);
dataSource.onmessage= function (event){
console.log("SSE--->收到数据")
const vo = JSON.parse(event.data)
//调用更新实时数据方法
updateClickChart(vo)
}
}
在timeclick接口下,后台借助
ScheduledExecutorService
周期性返回数据,前端借助EventSource
对象接收,再调用其他方法将接受的数据更新。所以sse技术适用用可视化方案。
上面提到的自行车点击项目是sse项目,放置在资源中。