页面实时获取数据 在当今数字化交互日益频繁的时代,前端开发面临着诸多挑战,其中任务执行耗时久以及如何实时获取数据便是极为常见且棘手的问题。想象一下,当用户在网页上触发某个操作,比如提交一份复杂的表单申请、查询海量数据的检索结果,又或是等待一个长时间运行的后台任务反馈,他们往往被迫陷入漫长的等待之中,盯着屏幕发呆,期望着那个迟迟未到的回应。而这背后,正是前端实时获取数据的困境在作祟。
如何使得后端数据可以实时推送到前端呢?下面介绍几种方案
方案一:轮询 这是最容易理解的一种方式。前端实现一个定时器,让客户端每隔较短固定时间就向服务端发起请求,服务器判断任务还没跑完,就回复一个未跑完,定时器一直到服务器回复任务完成并把数据返回回来。
短轮询的优劣势一目了然。优势在于,容易理解、实现过程简便,同时兼容性又很好,在几乎所有支持 HTTP 协议
的浏览器及服务器环境都能很好的运行短轮询。不过,缺点也非常显而易见。当按照很短的固定时间间隔去频繁请求数据,如果此时的数据并未更新,这些请求就成了无效请求,但是每一个无效的请求都得完成 HTTP 建立连接的一系列流程,像三次握手
、 四次挥手
,这无疑造成了不必要的资源浪费。同时也是由于按固定间隔请求,数据更新也可能会存在延迟的现象,在要求实时性的场景下就不满足了。
方案二:WebSocket
Web Socket 是一种基于单个 TCP 连接的协议,拥有实现 全双工 通信的能力。它让客户端和服务端可以双向、实时传输数据,摆脱了传统 HTTP 请求那种请求 - 响应模式的限制。有了 Web Socket,服务器能随时主动给客户端推送消息,客户端也能马上向服务器发数据,就如同构建起了一条实时双向通道。
前端示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > WebSocket实时推送示例</title > </head > <body > <h1 > WebSocket实时推送消息展示</h1 > <div id ="message-container" > </div > <script > const socket = new WebSocket('ws://localhost:8080/echo' ); socket.addEventListener('open' , function (event ) { console .log('WebSocket连接已建立' ); }); socket.addEventListener('message' , function (event ) { const messageContainer = document .getElementById('message-container' ); const messageElement = document .createElement('p' ); messageElement.textContent = event.data; messageContainer.appendChild(messageElement); console .log('收到消息: ' , event.data); }); socket.addEventListener('close' , function (event ) { console .log('WebSocket连接已关闭' ); }); socket.addEventListener('error' , function (event ) { console .log('WebSocket连接出错: ' , event); }); </script > </body > </html >
后端示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import javax.websocket.OnClose;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.Timer;import java.util.TimerTask;@ServerEndpoint("/echo") public class WebSocketServerEndpoint { private Timer timer; @OnOpen public void onOpen (Session session) { System.out.println("有新的WebSocket连接建立" ); timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run () { try { String message = "这是服务器推送的实时消息,当前时间:" + System.currentTimeMillis(); session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } }, 0 , 5000 ); } @OnClose public void onClose (Session session) { System.out.println("WebSocket连接已关闭" ); if (timer!= null ) { timer.cancel(); } } @OnMessage public void onMessage (String message, Session session) { System.out.println("收到客户端消息: " + message); } }
相较于传统的 HTTP 请求,Web Socket 在实时性方面更好,它打破了以往被动等待响应的模式,服务器得以实时且主动地将数据推送至客户端。在面对高实时性的场景下,大大的提升了用户体验。并且只需建立一次 TCP 连接,后续数据传输高效便捷,既减少了网络带宽的无谓占用,又减轻了服务器频繁处理连接请求的负担。
方案三:SSE方案 上述WebSocket已经可以解决大多数的场景了,但是假设客户端仅仅是获取服务端推送的消息,自身并无向服务端发送信息的需求,这是我们就要考虑下 SSE(Server-Sent Events)了。
SSE(Server-Sent Events) 具备了服务端主动向客户端推送数据的能力,而无需客户端不断地发起请求。与Web Socket不同的是,SSE 是基于 HTTP 协议的,使用的是单向通信, 而Web Socket是基于TCP协议的,使用的话双向通信。
SSE通信原理
客户端发起请求 客户端需要发送一个特定的请求告知服务器准备接收事件。在传统的 HTTP 请求 - 响应模式中,一次请求完成后连接通常会关闭,但 SSE 通过在服务器端和客户端设置特定的头部信息,让连接持续开启。例如:设置了Content - Type头部为text/event - stream,并设置Cache - Control为no - cache以及Connection为keep - alive,这样告知客户端,这是一个 SSE 连接,数据会持续推送,并且不需要缓存数据。客户端只需向服务器发送一个普通的 HTTP 请求,指向服务器端提供 SSE 服务的特定端点就行,例如/events,就可以顺利开启SSE连接。
服务器响应请求 服务器收到客户端请求后,会维持一个长连接,并且定期向客户端发送事件数据。每个事件都是通过特定的格式(例如 data:\n\n)发送给客户端的。
客户端接收数据 客户端通过 JavaScript 的 EventSource 对象接收从服务器推送过来的数据。这些数据可以是普通文本,也可以是 JSON 格式的对象,取决于服务器如何发送。
事件流的关闭 一旦不再需要推送数据,比如客户端主动关闭连接,又或是中途出现错误,导致无法继续推送时,服务器便会果断关闭连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > SSE 实时推送示例</title > </head > <body > <h1 > SSE 实时推送消息展示</h1 > <div id ="message-container" > </div > <script > const eventSource = new EventSource('http://localhost:8080/events' ); eventSource.addEventListener('open' , function (e ) { console .log('已连接到 SSE 服务' ); }); eventSource.addEventListener('message' , function (e ) { const messageContainer = document .getElementById('message-container' ); const messageElement = document .createElement('p' ); messageElement.textContent = e.data; messageContainer.appendChild(messageElement); console .log('收到消息: ' , e.data); }); eventSource.addEventListener('error' , function (e ) { console .log('SSE 连接出错: ' , e); if (e.readyState === EventSource.CLOSED) { console .log('连接已关闭' ); } }); </script > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.util.Date;@WebServlet("/events") public class SSEServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/event - stream" ); response.setCharacterEncoding("UTF - 8" ); final PrintWriter writer = response.getWriter(); while (true ) { String data = "data: " + new Date() + "\n\n" ; writer.write(data); writer.flush(); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }
基本格式要求 在Server - Sent Events(SSE)规范中,data:
是最常用的消息前缀。当发送一个简单的文本消息时,消息格式通常是data:消息内容\n\n
。其中\n\n
(两个连续的换行符)是消息的结束标志,这是必须的,用于告知客户端消息已经结束。 例如,发送一个简单的文本消息“Hello, SSE”,正确的格式应该是data:Hello, SSE\n\n
。
事件类型和其他格式扩展 除了data:
前缀,还可以使用event:
前缀来指定事件类型。这在一个SSE连接中可能发送多种类型的事件时非常有用。例如,event:custom - event\ndata:This is a custom - event message\n\n
,客户端可以根据event
类型来分别处理不同的消息。 另外,还可以使用id:
前缀来为消息设置一个标识符,通常用于消息的重连和恢复等场景。比如id:123\ndata:Message with ID 123\n\n
。
特殊情况和兼容性考虑 如果没有data:
前缀,严格来说不符合SSE的标准格式,大多数标准的SSE客户端将无法正确解析消息。不过,一些自定义的或者兼容性较好的客户端可能会尝试根据自己的规则来解析这种不符合标准的消息,但这是不推荐的做法,因为这可能会导致在不同客户端之间出现兼容性问题。 在一些测试或者非标准的实现场景中,可能会省略data:
前缀,但这仅限于特定的、受控制的环境,并且这种做法可能会使代码难以维护和与其他SSE系统集成。