オブジェクトの読み込み:この記事では、初心者やSpringBootの関心の子供用の靴SpringBoot読書のためです。
背景:企業におけるWEBアプリケーション開発、順序でより良いユーザー体験&には、多くの場合、時間がかかり、骨の折れる(など、Excelのインポートやエクスポート、複雑な計算、)の数を要求します、応答速度を向上させる行なわの非同期に処理します。これの重要な問題があることを、ユーザーのタスクのステータスを通知する方法を、一般的な方法は、大きく二つのカテゴリー4種類に分けられます。
HTTP Polling
クライアントプルHTTP Long-Polling
クライアントプルServer-Sent Events (SSE)
サーバープッシュWebSocket
サーバープッシュ
1.ポーリング短いポーリング
これは非常に単純な実装です。クライアントであることにより、タイミングタスク常に新しいメッセージを取得するように、リクエストサーバを繰り返す必要があり、メッセージサーバは、単一または複数のリクエストが時系列最後以来発生しています。
短いポーリングの利点は、それがシンプルで、非常に明白です。二方向のデータが非常に小さく、要求が非常に近接して配置されていない場合、この方法は非常に効果的であることができます。例えば、ニュースレビュー情報が一度半分分を更新することができる、それがユーザーのために可能です。
それは我々がデータのリアルタイム要件を持っていたら、メッセージのタイムリーな配送を確保するために、非常に高く、また非常に明らかな欠点だった、リクエスト間隔は、この場合には、短縮されなければならない、廃棄物のサーバリソースを悪化させ、サービスの可用性が低下します。別の欠点は、メッセージの数が少ない、多くのがあるだろうときということですrequest
ので、有用な作業を行う、としても、サーバーリソースの浪費につながります。
2.ロングポーリングロングポーリング
ロングポーリング公式の定義は次のとおりです。
サーバが提供するイベントがある場合にのみ応答し、各HTTPリクエストを(notimmediatelyに返信)「オープンホールド」しようと試みます。このように、彼らが発生すると、サーバは、それによってメッセージの配信に遅延を最小限に、イベントを配信するために応答することができるために保留中の要求が常に存在します。
場合はPolling
、比較のため、見つけるLong-Polling
の利点は、不要な要求を減らすことによって、オープンHTTPリクエストを保持します。
一般的な手順は次のとおりです。
- クライアントの要求とサーバへの応答を待っています。
- サーバーは、ブロッキングを要求し、新しいメッセージをチェックしていきます。この期間中に生成された新しいメッセージがある場合は、すぐに返します。それ以外の場合は、に待機していました
请求超时
。 - クライアント場合
获取到新消息
や请求超时
、メッセージ処理とは、次の要求を開始します。
Long-Polling
そのため、一つの欠点は、サーバリソースの無駄であるPolling
のと同じのすべての部分受動的買収は、あなたは常にサーバに要求する必要があります。高い同時実行の場合は、サーバーのパフォーマンスは、厳しいテストです。
注意:ので2を達成するために、上記の2つの方法の比較的単純なので、ここでは、コードは示していません。次は何に焦点を当てる
Server-Sent Events
とWebSocket
。
3. Demo概要
下面我们将通过一个下载文件的案例进行演示SSE
和WebSocket
的消息推送,在这之前,我们先简单说一下我们项目的结构,整个项目基于SpringBoot 构建。
首先我们定义一个供前端访问的APIDownloadController
@RestController
public class DownloadController {
private static final Logger log = getLogger(DownloadController.class);
@Autowired
private MockDownloadComponent downloadComponent;
@GetMapping("/api/download/{type}")
public String download(@PathVariable String type, HttpServletRequest request) { // (A)
HttpSession session = request.getSession();
String sessionid = session.getId();
log.info("sessionid=[{}]", sessionid);
downloadComponent.mockDownload(type, sessionid); // (B)
return "success"; // (C)
}
}
- (A)
type
参数用于区分使用哪种推送方式,这里为sse
,ws
,stomp
这三种类型。 - (B)
MockDownloadComponent
用于异步模拟下载文件的过程。 - (C) 因为下载过程为异步化,所以该方法不会被阻塞并立即向客户端返回
success
,用于表明下载开始。
在DownloadController
中我们调用MockDownloadComponent
的mockDownload()
的方法进行模拟真正的下载逻辑。
@Component
public class MockDownloadComponent {
private static final Logger log = LoggerFactory.getLogger(DownloadController.class);
@Async // (A)
public void mockDownload(String type, String sessionid) {
for (int i = 0; i < 100; i++) {
try {
TimeUnit.MILLISECONDS.sleep(100); // (B)
int percent = i + 1;
String content = String.format("{\"username\":\"%s\",\"percent\":%d}", sessionid, percent); // (C)
log.info("username={}'s file has been finished [{}]% ", sessionid, percent);
switch (type) { // (D)
case "sse":
SseNotificationController.usesSsePush(sessionid, content);
break;
case "ws":
WebSocketNotificationHandler.usesWSPush(sessionid, content);
break;
case "stomp":
this.usesStompPush(sessionid, content);
break;
default:
throw new UnsupportedOperationException("");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- (A) 我们使用
@Async
让使其异步化
。 - (B) 模拟下载耗时。
- (C) 消息的格式为
{"username":"abc","percent":1}
。 - (D) 根据不同的
type
选择消息推送方式。
4. Server-Sent Events
SSE
是W3C定义的一组API规范,这使服务器能够通过HTTP将数据推送到Web页面,它具有如下特点:
- 单向半双工:只能由server向client推送消息
- 基于http:数据被编码为“text/event-stream”内容并使用HTTP流机制进行传输
- 数据格式无限制:消息只是遵循规范定义的一组
key-value
格式&UTF-8
编码的文本数据流,我们可以在消息payload
中可以使用JSON
或者XML
或自定义数据格式。 - http 长连接: 消息的实际传递是通过一个长期存在的HTTP连接完成的,消耗资源更少
- 简单易用的API
浏览器支持情况:
Note:IE 浏览器可通过第三方JS库进行支持SSE
4.1 SpringBoot 中使用SSE
从Spring 4.2开始支持SSE规范,我们只需要在Controller
中返回SseEmitter
对象即可。
Note:Spring 5 中提供了Spring Webflux 可以更加方便的使用SSE,但是为更贴近我们的实际项目,所以文本仅演示使用Spring MVC SSE。
我们在服务器端定义一个SseNotificationController
用于和客户端处理和保存SSE
连接. 其endpoint
为/api/sse-notification
。
@RestController
public class SseNotificationController {
public static final Map<String, SseEmitter> SSE_HOLDER = new ConcurrentHashMap<>(); // (A)
@GetMapping("/api/sse-notification")
public SseEmitter files(HttpServletRequest request) {
long millis = TimeUnit.SECONDS.toMillis(60);
SseEmitter sseEmitter = new SseEmitter(millis); // (B)
HttpSession session = request.getSession();
String sessionid = session.getId();
SSE_HOLDER.put(sessionid, sseEmitter);
return sseEmitter;
}
/**
* 通过sessionId获取对应的客户端进行推送消息
*/
public static void usesSsePush(String sessionid, String content) { // (C)
SseEmitter emitter = SseNotificationController.SSE_HOLDER.get(sessionid);
if (Objects.nonNull(emitter)) {
try {
emitter.send(content);
} catch (IOException | IllegalStateException e) {
log.warn("sse send error", e);
SseNotificationController.SSE_HOLDER.remove(sessionid);
}
}
}
}
- (A)
SSE_HOLDER
保存了所有客户端的SseEmitter
,用于后续通知对应客户端。 - (B) 根据指定超时时间创建一个
SseEmitter
对象, 它是SpringMVC提供用于操作SSE的类。 - (C)
usesSsePush()
提供根据sessionId向对应客户端发送消息。发送只需要调用SseEmitter
的send()
方法即可。
至此服务端已经完成,我们使用Vue编写客户端Download.html
进行测试。核心代码如下:
usesSSENotification: function () {
var tt = this;
var url = "/api/sse-notification";
var sseClient = new EventSource(url); // (A)
sseClient.onopen = function () {...}; // (B)
sseClient.onmessage = function (msg) { // (C)
var jsonStr = msg.data;
console.log('message', jsonStr);
var obj = JSON.parse(jsonStr);
var percent = obj.percent;
tt.sseMsg += 'SSE 通知您:已下载完成' + percent + "%\r\n";
if (percent === 100) {
sseClient.close(); // (D)
}
};
sseClient.onerror = function () {
console.log("EventSource failed.");
};
}
- (A) 开启一个新的 SSE connection 并访问
/api/sse-notification
。 - (B) 当连接成功时的callback。
- (C) 当有新消息时的callback。
- (D) 当下载进度为100%时,关闭连接。
效果演示:
4. WebSocket
WebSocket
类似于标准的TCP连接,它是IETF(RFC 6455)定义的通过TCP进行实时全双工通信一种通信方式,这意味这它的功能更强大,常用于如股票报价器,聊天应用。
相比于SSE,它不仅可以双向通信,而且甚至还能处理音频/视频等二进制内容。
Note:使用
WebSocket
,在高并发情况下,服务器将拥有许多长连接。这对网络代理层组件及WebSocket
服务器都是一个不小的性能挑战,我们需要考虑其负载均衡方案。同时连接安全等问题也不容忽视。
4.1 Spring WebSocket (低级API)
Spring 4提供了一个新的Spring-WebSocket
模块,用于适应各种WebSocket引擎,它与Java WebSocket API标准(JSR-356)兼容,并且提供了额外的增强功能。
Note: 对于应用程序来说,直接使用WebSocket API会大大增加开发难度,所以Spring为我们提供了 STOMP over WebSocket 更高级别的API使用WebSocket。在本文中将会分别演示通过low level API及higher level API进行演示。
如果想在SpringBoot中使用WebSocket,首先需要引入spring-boot-starter-websocket
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
然后就可以配置相关信息,我们先通过low level API进行演示。
首先需要自定义一个WebSocketNotificationHandler
用于处理WebSocket 的连接及消息处理。我们只需要实现WebSocketHandler
或子类TextWebSocketHandler
BinaryWebSocketHandler
。
public class WebSocketNotificationHandler extends TextWebSocketHandler {
private static final Logger log = getLogger(WebSocketNotificationHandler.class);
public static final Map<String, WebSocketSession> WS_HOLDER= new ConcurrentHashMap<>(); // (A)
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception { // (B)
String httpSessionId = (String) session.getAttributes().get(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME);
WS_HOLDER.put(httpSessionId, session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
log.info("handleTextMessage={}", message.getPayload());
}
public static void usesWSPush(String sessionid, String content) { // (C)
WebSocketSession wssession = WebSocketNotificationHandler.WS_HOLDER.get(sessionid);
if (Objects.nonNull(wssession)) {
TextMessage textMessage = new TextMessage(content);
try {
wssession.sendMessage(textMessage);
} catch (IOException | IllegalStateException e) {
WebSocketNotificationHandler.SESSIONS.remove(sessionid);
}
}
}
}
- (A)
WS_HOLDER
用于保存客户端的WebSocket Session
- (B) 重写
afterConnectionEstablished()
方法,当连接建立之后,按sessionId
将WebSocket Session
保存至WS_HOLDER
,用于后续向client推送消息。 - (C) 根据
sessionId
获取对应WebSocket Session
,并调用WebSocket Session
的sendMessage(textMessage)
方法向client发送消息。
使用@EnableWebSocket
开启WebSocket,并实现WebSocketConfigurer
进行配置。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
WebSocketNotificationHandler notificationHandler = new WebSocketNotificationHandler();
registry.addHandler(notificationHandler, "/ws-notification") // (A)
.addInterceptors(new HttpSessionHandshakeInterceptor()) // (B)
.withSockJS(); // (C)
}
}
- (A) 将我们自定义的
WebSocketNotificationHandler
注册至WebSocketHandlerRegistry
. - (B)
HttpSessionHandshakeInterceptor
是一个内置的拦截器,用于传递HTTP会话属性到WebSocket会话。当然你也可以通过HandshakeInterceptor
接口实现自己的拦截器。 - (C) 开启SockJS的支持,SockJS的目标是让应用程序使用WebSocket API时,当发现浏览器不支持时,无需要更改任何代码,即可使用非WebSocket替代方案,尽可能的模拟WebSocket。关于SockJS的更多资料,可参考https://github.com/sockjs/sockjs-client
server端至此就基本大功告成,接下来我们来完善一下client端Download.html
,其核心方法如下:
usesWSNotification: function () {
var tt = this;
var url = "http://localhost:8080/ws-notification";
var sock = new SockJS(url); // (A)
sock.onopen = function () {
console.log('open');
sock.send('test');
};
sock.onmessage = function (msg) { // (B)
var jsonStr = msg.data;
console.log('message', jsonStr);
var obj = JSON.parse(jsonStr);
var percent = obj.percent;
tt.wsMsg += 'WS 通知您:已下载完成' + percent + "%\r\n";
if (percent === 100) {
sock.close();
}
};
sock.onclose = function () {
console.log('ws close');
};
}
- (A) 首先需要在项目中引入SockJS Client , 并根据指定URL创建一个SockJS对象。
- (B) 当有新消息时的
callback
,我们可以在该方法中处理我们的消息。
效果演示:
4.2 STOMP over WebSocket (高级API)
WebSocket虽然定义了两种类型的消息,文本和二进制,但是针对消息的内容没有定义,为了更方便的处理消息,我们希望Client和Server都需要就某种协议达成一致,以帮助处理消息。那么,有没有已经造好的轮子呢?答案肯定是有的。这就是STOMP。
STOMP是一种简单的面向文本的消息传递协议,它其实是消息队列的一种协议, 和AMQP,JMS是平级的。 只不过由于它的简单性恰巧可以用于定义WS的消息体格式。虽然STOMP是面向文本的协议,但消息的内容也可以是二进制数据。同时STOMP 可已使用任何可靠的双向流网络协议,如TCP和WebSocket,目前很多服务端消息队列都已经支持了STOMP, 比如RabbitMQ, ActiveMQ等。
它结构是一种基于帧的协议,一帧由一个命令,一组可选的Header和一个可选的Body组成。
COMMAND
header1:value1
header2:value2
Body^@
客户端可以使用SEND
或SUBSCRIBE
命令发送或订阅消息。 通过destination
标记述消息应由谁来接收处理,形成了类似于MQ的发布订阅机制。
STOMP的优势也非常明显,即:
- 不需要创建自定义消息格式
- 我们可以使用现有的stomp.js客户端
- 可以实现消息路由及广播
- 可以使用第三方成熟的消息代理中间件,如RabbitMQ, ActiveMQ等
最重要的是,Spring STOMP
为我们提供了能够像Spring MVC一样的编程模型,减少了我们的学习成本。
下面将我们的DEMO稍作调整,使用Spring STOMP
来实现消息推送,在本例中我们使用SimpleBroker
模式,我们的应用将会内置一个STOMP Broker
,将所有信息保存至内存中。
具体代码如下:
@Configuration
@EnableWebSocketMessageBroker // (A)
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp-notification")
.addInterceptors(httpSessionHandshakeInterceptor()) // (B)
.setHandshakeHandler(httpSessionHandshakeHandler()) // (C)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app") // (D)
.enableSimpleBroker("/topic", "/queue"); // (E)
}
@Bean
public HttpSessionHandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HttpSessionHandshakeInterceptor();
}
@Bean
public HttpSessionHandshakeHandler httpSessionHandshakeHandler() {
return new HttpSessionHandshakeHandler();
}
}
- (A) 使用
@EnableWebSocketMessageBroker
注解开启支持STOMP - (B) 创建一个拦截器,用于传递HTTP会话属性到WebSocket会话。
- (C) 配置一个自定义的
HttpSessionHandshakeHandler
,其主要作用是按sessionId标记识别连接。 - (D) 设置消息处理器路由前缀,当消息的
destination
已/app
开头时,将会把该消息路由到server端的对应的消息处理方法中。(在本例中无实际意义) - (E) 设置客户端订阅消息的路径前缀
HttpSessionHandshakeHandler
代码如下:
public class HttpSessionHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
String sessionId = (String) attributes.get(HttpSessionHandshakeInterceptor.HTTP_SESSION_ID_ATTR_NAME);
return new HttpSessionPrincipal(sessionId);
}
}
当我们需要向client发送消息时,只需要注入SimpMessagingTemplate
对象即可,是不是感觉非常熟悉?! 没错,这种Template
模式和我们日常使用的RestTemplate
JDBCTemplate
是一样的。
我们只需要调用SimpMessagingTemplate
的convertAndSendToUser()
方法即可向对应用户发送消息了。
private void usesStompPush(String sessionid, String content) {
String destination = "/queue/download-notification";
messagingTemplate.convertAndSendToUser(sessionid, destination, content);
}
在浏览器端,client可以使用stomp.js和sockjs-client进行如下连接:
usesStompNotification: function () {
var tt = this;
var url = "http://localhost:8080/ws-stomp-notification";
// 公共topic
// var notificationTopic = "/topic/download-notification";
// 点对点广播
var notificationTopic = "/user/queue/download-notification"; // (A)
var socket = new SockJS(url);
var stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log("STOMP connection successful");
stompClient.subscribe(notificationTopic, function (msg) { // (B)
var jsonStr = msg.body;
var obj = JSON.parse(jsonStr);
var percent = obj.percent;
tt.stompMsg += 'STOMP 通知您:已下载完成' + percent + "%\r\n";
if (percent === 100) {
stompClient.disconnect()
}
});
}, function (error) {
console.log("STOMP protocol error " + error)
})
}
- (A) 如果想针对特定用户接收消息,我们需要以
/user/
为前缀,Spring STOMP会把以/user/
为前缀的消息交给UserDestinationMessageHandler
进行处理并发给特定的用户,当然这个/user/
是可以通过WebSocketBrokerConfig
进行个性化配置的,为了简单起见,我们这里就使用默认配置,所以我们的topic url就是/user/queue/download-notification
。 - (B) 设置
stompClient
消息处理callback进行消息处理。
效果演示:
5 总结
在文中为大家简单讲解了几种常用的消息推送方案,并通过一个下载案例重点演示了SSE
及WebSocket
这两种server push模式的消息推送。当然还有很多细节并没有在文中说明,建议大家下载源码对照参考。
モデル、我々のニーズだけであればという小さなシリーズのこれらのタイプに比べてクライアントにメッセージをプッシュし、その後の使用SSE
コストが高いが、Long-Polling
続きます。使用WebSocket
ハンマーと鶏の感があり、そして私たちのシステムに、より複雑な、良いよりも害をもたらし、それが推奨されていません。そして、Polling
最も簡単かつ最強の互換性の実現が、その効率は低すぎるが、それは推奨されません。あなたが他のアイデアを持っている場合はもちろん、メッセージ交換を議論する歓迎。
サンプルソーステキスト:https://github.com/leven-space/SpringBootNotification.git
あなたはこの記事を有用見つけた場合は、あなたの小さなを残してください