服务端向浏览器推送内容几种方式
轮询:最简单、最通用,基于http协议,所有浏览器都可用。
SSE:服务端向客户端单向推送,基于http协议,部分浏览器支持(html5)
websocket:服务器与客户端双向通信,由http协议Upgrade到WebSocket协议,部分浏览器支持。
一、轮询
客户端的轮询方式一般为 短轮询
或 长轮询
短轮询:
一般是由客户端每隔一段时间(如每隔5s)向服务器发起一次普通 HTTP 请求。服务端查询当前接口是否有数据更新,若有数据更新则向客户端返回最新数据,若无则提示客户端无数据更新。
长轮询 :
一般是由客户端向服务端发出一个设置较长网络超时时间的 HTTP 请求,并在Http连接超时前,不主动断开连接,待客户端超时或有数据返回后,再次建立一个同样的Http请求,重复以上过程。
例如:
客户端 向 服务端 发起Http请求,并且设置了超时时间为30秒如果30秒内 服务端 有数据变化,则将数据传递给 客户端 ,并主动断开连接;如果没有数据更新,待 客户端 超时后会主动断开连接,此后客户端将重新建立一个新的Htp连接,并重复上述过程。
二、SSE
Server-Sent Events(SSE)是一种用于实现服务器向客户端实时推送数据的Web技术。与传统的轮询和长轮询相比,SSE提供了更高效和实时的数据推送机制。
SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。
SSE的主要特点包括:
- 简单易用:SSE使用基于文本的数据格式,如纯文本、JSON等,使得数据的发送和解析都相对简单。
- 单向通信:SSE支持服务器向客户端的单向通信,服务器可以主动推送数据给客户端,而客户端只能接收数据。
- 实时性:SSE建立长时间的连接,使得服务器可以实时地将数据推送给客户端,而无需客户端频繁地发起请求。
在Spring Boot中使用SSE的场景案例
假设我们正在开发一个实时股票价格监控应用,需要将股票价格实时推送给客户端。
首先,定义一个控制器来处理SSE请求和发送实时股票价格:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Random;
@RestController
public class StockController {
@GetMapping(value = "/stock-price", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamStockPrice() {
SseEmitter emitter = new SseEmitter();
// 模拟生成实时股票价格并推送给客户端
Random random = new Random();
new Thread(() -> {
try {
while (true) {
// 生成随机的股票价格
double price = 100 + random.nextDouble() * 10;
// 构造股票价格的消息
String message = String.format("%.2f", price);
// 发送消息给客户端
emitter.send(SseEmitter.event().data(message));
// 休眠1秒钟
Thread.sleep(1000);
}
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
}
接下来,创建一个简单的HTML页面来展示实时股票价格:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>实时股票价格监控</title>
</head>
<body>
<h1>实时股票价格</h1>
<div id="stock-price"></div>
<script>
const eventSource = new EventSource('/stock-price');
eventSource.onmessage = function (event) {
document.getElementById('stock-price').innerHTML = event.data;
};
</script>
</body>
</html>
1、在服务端方法内部,创建了一个SseEmitter
对象作为事件发射器,通过emitter.send()
方法发送的数据会被封装为SSE事件流的形式,客户端可以通过监听该事件流来实时接收股票价格。
2、在HTML页面中,通过new EventSource('/stock-price')
创建了一个EventSource
对象,它会与/stock-price路径建立SSE连接。然后通过eventSource.onmessage
定义了接收消息的回调函数,在收到新消息时更新页面上的股票价格。
三、websocket
WebSocket
用于在Web浏览器和服务器之间进行任意的双向数据传输的一种技术。WebSocket
协议基于TCP
协议实现,包含初始的握手过程,以及后续的多次数据帧双向传输过程。其目的是在WebSocket应用和WebSocket服务器进行频繁双向通信时,可以使服务器避免打开多个HTTP连接进行工作来节约资源,提高了工作效率和资源利用率。
WebSocket目前支持两种统一资源标志符ws
和wss
,类似于HTTP和HTTPS。
在Spring Boot中使用websocket的场景案例
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
import lombok.Data;
@Data
public class WebSocketVO {
private Integer userId;
private String opt;
}
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@Configuration
public class WebSocketConfig implements ServletContextInitializer {
/**
* 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
import com.alibaba.fastjson.JSONObject;
import com.htang.hire.admin.api.vo.websocket.WebSocketVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/websocket/{userId}")
@Component
@Slf4j
public class WebSocketSever {
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// session集合,存放对应的session
private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<>();
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
private static CopyOnWriteArraySet<WebSocketSever> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 建立WebSocket连接
*
* @param session
* @param userId 用户ID
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") Integer userId) {
log.info("WebSocket建立连接中,连接用户ID:{}", userId);
try {
Session historySession = sessionPool.get(userId);
// historySession不为空,说明已经有人登陆账号,应该删除登陆的WebSocket对象
if (historySession != null) {
webSocketSet.remove(historySession);
historySession.close();
}
} catch (IOException e) {
log.error("重复登录异常,错误信息:" + e.getMessage(), e);
}
// 建立连接
this.session = session;
webSocketSet.add(this);
sessionPool.put(userId, session);
log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
}
/**
* 发生错误
*
* @param throwable e
*/
@OnError
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
log.info("连接断开,当前在线人数为:{}", webSocketSet.size());
}
/**
* 接收客户端消息
*
* @param message 接收的消息
*/
@OnMessage
public void onMessage(String message) {
log.info("收到客户端发来的消息:{}", message);
}
/**
* 推送消息到指定用户
*
* @param userId 用户ID
* @param message 发送的消息
*/
public static void sendMessageByUser(Integer userId, String message) {
log.info("用户ID:" + userId + ",推送内容:" + message);
Session session = sessionPool.get(userId);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
}
}
/**
* 群发消息
*
* @param message 发送的消息
*/
public static void sendAllMessage(String message) {
log.info("发送消息:{}", message);
for (WebSocketSever webSocket : webSocketSet) {
try {
webSocket.session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("群发消息发生错误:" + e.getMessage(), e);
}
}
}
}
WebSocketVO webSocketVO = new WebSocketVO();
webSocketVO.setUserId(123456789);
webSocketVO.setOpt("xxooxx");
WebSocketSever.sendMessageByUser(123456789, JSONObject.toJSONString(webSocketVO));
<script>
var ws = new WebSocket('ws://localhost:8080/websocket/123456789');
ws.onopen = function () {
console.log('ws onopen');
ws.send('from client: hello');
};
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
</script>
如何建立连接
WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
1、客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET
方法。
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重点请求首部意义如下:
Connection: Upgrade
:表示要升级协议Upgrade: websocket
:表示要升级到websocket协议。Sec-WebSocket-Version: 13
:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Version
header,里面包含服务端支持的版本号。Sec-WebSocket-Key
:与后面服务端响应首部的Sec-WebSocket-Accept
是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
2、服务端:响应协议升级
服务端返回内容如下,状态代码101
表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
连接保持+心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。
但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。
- 发送方->接收方:ping
- 接收方->发送方:pong
举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws
模块)
ws.ping('', false, true);