Web服务器推送信息SSE/WebSocket
介绍
没有简单,通用的方法来以可接受的性能在Web应用程序中实现服务器到客户端的异步通信。
HTTP是客户端-服务器计算模型中的请求-响应协议。为了开始交换,客户端向服务器提交请求。为了完成交换,服务器将响应返回给客户端。在HTTP协议中,客户端是消息交换的发起者。
在某些情况下,服务器应该是发起者。实现此目的的方法之一是允许服务器将消息推送到发布/订阅计算模型中的客户端。
服务器发送事件(SSE)是一种用于为特定 Web应用程序实现异步服务器到客户端通信的简单技术。
总览
有几种技术可以使客户端从服务器接收有关异步更新的消息。它们可以分为两类:客户端请求 和 服务器请求。
客户拉
在客户端请求技术中,客户端会定期请求服务器进行更新。服务器可以响应更新,也可以响应尚未更新的特殊响应。客户端请求有两种类型:短轮询和长轮询。
短轮询
客户端定期向服务器发送请求。如果服务器有更新,它将向客户端发送响应并关闭连接。如果服务器没有更新,它将向客户端发送特殊响应,并关闭连接。
比如客户端使用 ajax 定时执行。
长时间轮询
客户端向服务器发送请求。如果服务器有更新,它将向客户端发送响应并关闭连接。如果服务器没有更新,它将保持连接,直到有可用的更新为止。当有可用更新时,服务器将响应发送到客户端并关闭连接。如果更新在某些超时时间内不可用,则服务器会向客户端发送特殊响应,并关闭连接。
服务器推送
在服务器推送技术中,服务器在客户端可用后立即主动向客户端发送消息。其中,服务器推送有两种类型:服务器发送事件(SSE) 和 WebSocket。
服务器发送的事件(SSE)
服务器发送的事件是一种仅从服务器向基于浏览器的Web应用程序中的客户端发送文本消息的技术。服务器发送的事件基于HTTP协议中的持久连接。服务器发送的事件具有W3C 标准化的网络协议和EventSource客户端接口,作为HTML5标准套件的一部分。
WebSocket
WebSocket是一种在Web应用程序中实现同时,双向,实时通信的技术。WebSocket基于除HTTP之外的协议,因此它可能需要对网络基础架构(代理服务器,NAT,防火墙等)进行额外的设置。但是,WebSocket可以提供使用基于HTTP的技术难以实现的性能。
SSE网络协议
要订阅服务器事件,客户端应GET
使用标头进行请求:
Accept: text/event-stream
指示标准要求的事件的媒体类型Cache-Control: no-cache
禁用所有事件缓存Connection: keep-alive
指示正在使用持久连接
GET /sse HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
服务器应使用以下标头的响应来确认订阅:
Content-Type: text/event-stream;charset=UTF-8
指示标准要求的媒体类型和事件编码Transfer-Encoding: chunked
表示服务器流式传输动态生成的内容,因此事先不知道内容大小
HTTP/1.1 200
Content-Type: text/event-stream;charset=UTF-8
Transfer-Encoding: chunked
订阅后,服务器将在消息可用后立即发送消息。事件是UTF-8
编码的文本消息。事件之间用两个换行符分隔\n\n
。每个事件都包含一个或多个name: value
字段,以单个换行符分隔\n
。
在该data
字段中,服务器可以发送事件数据。
data: The first event.data: The second event.
服务器可以data
通过一个换行符将字段分成几行\n
。
data: The third
data: event.
id
服务器可以在该字段中发送唯一的事件标识符。如果连接断开,客户端应自动重新连接并发送id
带有header 的最后一个接收到的事件Last-Event-ID
。
id: 1
data: The first event.id: 2
data: The second event.
event
服务器可以在该字段中发送事件类型。服务器可以在同一预订中发送不同类型的事件,也可以不发送任何类型的事件。
event: type1
data: An event of type1.event: type2
data: An event of type2.data: An event without any type.
retry
服务器可以在该字段中发送超时(以毫秒为单位),在此之后客户端应在连接断开时自动重新连接。如果未指定此字段,则标准值为3000毫秒。
retry: 1000
如果一行以冒号开头:
,则客户端应将其忽略。这可用于从服务器发送注释或防止某些代理服务器因超时而关闭连接。
: ping
SSE客户端:EventSource界面
要打开连接,应创建一个EventSource
对象。
var eventSource = new EventSource('/sse);
尽管服务器发送事件旨在将事件从服务器发送到客户端,但是仍然可以使用GET
查询参数将数据从客户端传递到服务器。
var eventSource = new EventSource('/sse?event=type1);
...
eventSource.close();
eventSource = new EventSource('/sse?event=type1&event=type2);
...
要关闭连接,应将其称为method close()
。
eventSource.close();
有readyState
一个表示连接状态的属性:
EventSource.CONNECTING = 0
-尚未建立连接,或者连接已关闭并且客户端正在重新连接EventSource.OPEN = 1
-客户端具有打开的连接,并在接收事件时处理事件EventSource.CLOSED = 2
-连接未打开,并且客户端没有尝试重新连接,或者出现致命错误或close()
调用了该方法
要处理连接的建立,应将其预订给onopen
事件处理程序。
eventSource.onopen = function () {console.log('connection is established');
};
要处理连接状态中的某些更改或致命错误,应将其预订给onerrror
事件处理程序。
eventSource.onerror = function (event) {console.log('connection state: ' + eventSource.readyState + ', error: ' + event);
};
要处理没有该event
字段的接收事件,应将其预订给onmessage
事件处理程序。
eventSource.onmessage = function (event) {console.log('id: ' + event.lastEventId + ', data: ' + event.data);
};
要使用该event
字段处理接收事件,应为该事件订阅事件处理程序。
eventSource.addEventListener('type1', function (event) {console.log('id: ' + event.lastEventId + ', data: ' + event.data);
}, false);
SSE Java服务器:Spring Web MVC
介绍
Spring Web MVC框架5.2.0基于Servlet 3.1 API,并使用线程池来实现异步Java Web应用程序。此类应用程序可以在Servlet 3.1+容器(例如Tomcat 8.5和Jetty 9.3)上运行。
总览
要使用Spring Web MVC框架实现发送事件:
- 创建一个控制器类,并用
@RestController
注释对其进行标记 - 创建一个创建客户端连接的方法,该方法返回SseEmitter,处理
GET
请求并产生text/event-stream
- 创建一个new
SseEmitter
,以保存它并从方法中返回它
- 创建一个new
- 在另一个线程中异步发送事件,获取已保存的内容,
SseEmitter
并SseEmitter.send
根据需要多次调用方法- 完成发送事件,请调用SseEmitter.complete()方法
- 要完成特殊的事件发送,请调用SseEmitter.completeWithError()方法
简化的控制器源:
@RestController
public class SseWebMvcControllerprivate SseEmitter emitter;@GetMapping(path="/sse", produces=MediaType.TEXT_EVENT_STREAM_VALUE)SseEmitter createConnection() {emitter = new SseEmitter();return emitter;}// in another threadvoid sendEvents() {try {emitter.send("Alpha");emitter.send("Omega");emitter.complete();} catch(Exception e) {emitter.completeWithError(e);}}
}
若要仅使用data
字段发送事件,应使用SseEmitter.send(Object object)方法。要发送的事件与领域data
,id
,event
,retry
和意见,应当使用 SseEmitter.send(SseEmitter.SseEventBuilder建设者)方法。
在下面的示例中,为了将相同的事件发送给许多客户端,实现了SseEmitters类。要创建客户端连接,有一种add(SseEmitter emitter)
方法将a保存SseEmitter
在线程安全的容器中。为了异步发送事件,有一种send(Object obj)
方法可以将相同的事件发送到所有连接的客户端。
简化的类源:
class SseEmitters {private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();SseEmitter add(SseEmitter emitter) {this.emitters.add(emitter);emitter.onCompletion(() -> {this.emitters.remove(emitter);});emitter.onTimeout(() -> {emitter.complete();this.emitters.remove(emitter);});return emitter;}void send(Object obj) {List<SseEmitter> failedEmitters = new ArrayList<>();this.emitters.forEach(emitter -> {try {emitter.send(obj);} catch (Exception e) {emitter.completeWithError(e);failedEmitters.add(emitter);}});this.emitters.removeAll(failedEmitters);}
}
处理持久性周期性事件流
在此示例中,服务器每秒发送一次持续时间很短的定期事件流-有限的单词流(快速的棕色狐狸跳过懒惰的狗 pangram),直到单词完成为止。
为了实现这一点,使用了提到的SseEmitters类。为了异步并定期发送事件,已创建了一个缓存的线程池。因为事件流是持久的,所以每个客户端连接都在controller方法内部将单独的任务提交给线程池。
简化的控制器源:
@Controller @RequestMapping("/sse/mvc") public class WordsController {private static final String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");private final ExecutorService cachedThreadPool = Executors.newCachedThreadPool();@GetMapping(path = "/words", produces = MediaType.TEXT_EVENT_STREAM_VALUE)SseEmitter getWords() {SseEmitter emitter = new SseEmitter();cachedThreadPool.execute(() -> {try {for (int i = 0; i < WORDS.length; i++) {emitter.send(WORDS[i]);TimeUnit.SECONDS.sleep(1);}emitter.complete();} catch (Exception e) {emitter.completeWithError(e);}});return emitter;} }
具有EventSource
JavaScript客户端的事件客户端源。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Server-Sent Events client example with EventSource</title>
</head>
<body>
<script>if (window.EventSource == null) {alert('The browser does not support Server-Sent Events');} else {var eventSource = new EventSource('/sse/mvc/words');eventSource.onopen = function () {console.log('connection is established');};eventSource.onerror = function (error) {console.log('connection state: ' + eventSource.readyState + ', error: ' + event);};eventSource.onmessage = function (event) {console.log('id: ' + event.lastEventId + ', data: ' + event.data);if (event.data.endsWith('.')) {eventSource.close();console.log('connection is closed');}};}
</script>
</body>
</html>
EventSource
浏览器中带有JavaScript客户端的事件客户端示例。在客户端使用了自动重新连接,在服务器端使用了已实现的重新连接。
处理持久的周期性事件
在此示例中,服务器发送持久的周期性事件流-服务器性能信息的每秒潜在无限流:
- 承诺的虚拟内存大小
- 交换空间总大小
- 可用交换空间大小
- 物理内存总大小
- 可用物理内存大小
- 系统CPU负载
- 处理CPU负载
为了实现此目的,实现了PerformanceService类,该类使用OperatingSystemMXBean类从操作系统读取性能信息。还使用了提到的SseEmitters类。为了异步并定期发送事件,已创建了计划的线程池。因为事件流是持久的,所以将单个任务提交到线程池以将事件同时发送到所有客户端。
简化的控制器示例:
@RestController
@RequestMapping("/sse/mvc")
public class PerformanceController {private final PerformanceService performanceService;PerformanceController(PerformanceService performanceService) {this.performanceService = performanceService;}private final AtomicInteger id = new AtomicInteger();private final ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);private final SseEmitters emitters = new SseEmitters();@PostConstructvoid init() {scheduledThreadPool.scheduleAtFixedRate(() -> {emitters.send(performanceService.getPerformance());}, 0, 1, TimeUnit.SECONDS);}@GetMapping(path = "/performance", produces = MediaType.TEXT_EVENT_STREAM_VALUE)SseEmitter getPerformance() {return emitters.add();}
}
处理非周期性事件
在此示例中,服务器发送有关正在监视的文件夹中的文件更改(创建,修改,删除)的非定期事件流。使用该文件夹后,System.getProperty("user.home")
属性将提供当前用户的主文件夹。
为了实现此目的,实现了FolderWatchService类,该类使用Java NIO文件监视功能。还使用了提到的SseEmitters类。为了异步和非周期性地发送事件,FolderWatchService类将生成Spring应用程序事件,这些事件由控制器(通过实现侦听器方法)消耗。
一个简化的服务器示例:
@RestController
@RequestMapping("/sse/mvc")
public class FolderWatchController implements ApplicationListener<FolderChangeEvent> {private final FolderWatchService folderWatchService;FolderWatchController(FolderWatchService folderWatchService) {this.folderWatchService = folderWatchService;}private final SseEmitters emitters = new SseEmitters();@PostConstructvoid init() {folderWatchService.start(System.getProperty("user.home"));}@GetMapping(path = "/folder-watch", produces = MediaType.TEXT_EVENT_STREAM_VALUE)SseEmitter getFolderWatch() {return emitters.add(new SseEmitter());}@Overridepublic void onApplicationEvent(FolderChangeEvent event) {emitters.send(event.getEvent());}
}
SSE Java服务器:Spring Web Flux
介绍
Spring Web Flux框架5.2.0基于Reactive Streams API,并使用事件循环计算模型来实现异步Java Web应用程序。这样的应用可以在非阻挡的Web服务器等的Netty 4.1和1.4暗流运行和上的Servlet容器3.1+如Tomcat 8.5和9.3码头。
总览
要使用Spring Web Flux框架实现发送事件:
- 创建一个控制器类,并用
@RestController
注释对其进行标记 - 创建一个创建客户端连接并发送事件的方法,该方法返回Flux,处理
GET
请求并产生text/event-stream
- 创建一个新的
Flux
并从方法中返回它
- 创建一个新的
简化的控制器源:
@RestController
public class ExampleController@GetMapping(path="/sse", produces=MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<String> createConnectionAndSendEvents() {return Flux.just("Alpha", "Omega");}
}
要仅使用data
字段发送事件,应使用该Flux<T>
类型。要发送的事件与领域data
,id
,event
,retry
和意见,应当使用的Flux<ServerSentEvent<T>>
类型。
处理持久性周期性事件流
在此示例中,服务器每秒发送一次持续时间很短的定期事件流-有限的单词流(快速的棕色狐狸跳过懒惰的狗 pangram),直到单词完成为止。
要实现这一点:
- 创建类型为a
Flux
的单词Flux.just(WORDS)
Flux<String>
- 创建一个 类型每秒
Flux
发出递增long
值Flux.interval(Duration.ofSeconds(1))
的Flux<Long>
- 通过
zip
方法将它们组合在一起键入Flux<Tuple2<String,Long>>
- 通过
map(Tuple2::getT1)
类型提取元组的第一个元素Flux<String>
简化的控制器源:
@RestController
@RequestMapping("/sse/flux")
public class WordsController {private static final String[] WORDS = "The quick brown fox jumps over the lazy dog.".split(" ");@GetMapping(path = "/words", produces = MediaType.TEXT_EVENT_STREAM_VALUE)Flux<String> getWords() {return Flux.zip(Flux.just(WORDS), Flux.interval(Duration.ofSeconds(1))).map(Tuple2::getT1);}
}
此示例的事件客户端与Web MVC示例中使用的事件客户端相同。
处理持久的周期性事件
在此示例中,服务器发送持久的周期性事件流-服务器性能信息的每秒潜在无限流。
要实现这一点:
- 创建一个类型每秒
Flux
发出递增long
值Flux.interval(Duration.ofSeconds(1))
的Flux<Long>
- 通过
map(sequence -> performanceService.getPerformance())
方法将其转换为类型Flux<Performance>
简化的控制器示例:
@RestController
@RequestMapping("/sse/flux")
public class PerformanceController {private final PerformanceService performanceService;PerformanceController(PerformanceService performanceService) {this.performanceService = performanceService;}@GetMapping(path = "/performance", produces = MediaType.TEXT_EVENT_STREAM_VALUE)Flux<Performance> getPerformance() {return Flux.interval(Duration.ofSeconds(1)).map(sequence -> performanceService.getPerformance());}
}
此示例的事件客户端与Web MVC示例中使用的事件客户端相同。
处理非周期性事件
在此示例中,服务器发送有关正在监视的文件夹中的文件更改(创建,修改,删除)的非定期事件流。使用该文件夹后,System.getProperty("user.home")
属性将提供当前用户的主文件夹。
为了实现此目的,实现了FolderWatchService类,该类使用Java NIO文件监视功能。为了异步和非周期性地发送事件,FolderWatchService类将生成Spring应用程序事件,这些事件由控制器(通过实现侦听器方法)消耗。控制器侦听器方法将事件发送到SubscribableChannel
,在控制器方法中订阅该Flux
事件以产生事件。
简化的控制器示例:
@RestController
@RequestMapping("/sse/flux")
public class FolderWatchController implements ApplicationListener<FolderChangeEvent> {private final FolderWatchService folderWatchService;FolderWatchController(FolderWatchService folderWatchService) {this.folderWatchService = folderWatchService;}private final SubscribableChannel subscribableChannel = MessageChannels.publishSubscribe().get();@PostConstructvoid init() {folderWatchService.start(System.getProperty("user.home"));}@GetMapping(path = "/folder-watch", produces = MediaType.TEXT_EVENT_STREAM_VALUE)Flux<FolderChangeEvent.Event> getFolderWatch() {return Flux.create(sink -> {MessageHandler handler = message -> sink.next(FolderChangeEvent.class.cast(message.getPayload()).getEvent());sink.onCancel(() -> subscribableChannel.unsubscribe(handler));subscribableChannel.subscribe(handler);}, FluxSink.OverflowStrategy.LATEST);}@Overridepublic void onApplicationEvent(FolderChangeEvent event) {subscribableChannel.send(new GenericMessage<>(event));}
}
此示例的事件客户端与Web MVC示例中使用的事件客户端相同。
SSE限制
SSE 在设计上有局限性:
- 从服务器到客户端只能在一个方向上发送消息
- 可以只发送短信;尽管可以使用
Base64
编码和gzip
压缩来发送二进制消息,但效率低下。
但是SSE 在实施方面也存在局限性 :
- Internet Explorer / Edge和许多移动浏览器不支持SSE。尽管可以使用polyfills,但效率低下
- 许多浏览器允许打开数量非常有限的SSE连接(对于Chrome,Firefox,每个浏览器最多支持6个连接)
Web服务器推送信息SSE/WebSocket相关推荐
- .net 服务器推送信息,.net websocket服务端开发,实现消息推送功能
WebSocket协议是一种双向通信协议,它建立在TCP之上,同http一样通过TCP来传输数据,但是它和http最大的不同有两点: WebSocket是一种双向通信协议,在建立连接后,WebSock ...
- java web 服务器推送技术--comet4j
1.背景 首先实现服务器推送技术一直一来是B/S应用开发的一块难题,因为是基于HTTP协议的,HTTP协议为无状态,单向性的协议,即,必须由客户端发起一个请求建立连接,服务器接收请求,把数据返回给客户 ...
- ajax轮询模拟websocket,Ajax轮询和SSE服务器推送数据与websocket模式的区别性学习
我们试想一下我们做个实时聊天的窗口有几种方法? 在我们不刷新页面并且可以试试更新页面内容的方法 你这时候是不是想到了ajax没错确实可以 Ajax轮询 什么是轮询?顾名思义就是我轮着问你,规定一个时间 ...
- Web消息推送之SSE
文章目录 一.消息推送简介 1.消息推送介绍 2.几种方式介绍 二.SSE原理介绍 1.SSE基础概念 2.SSE特点 3.SSE与WebSocket异同 三.SSE推送实现 1.概述 1.1 使用S ...
- 服务器推送信息到客户端,服务器如何发送消息到客户端
服务器如何发送消息到客户端 内容精选 换一换 使用SSH方式登录CentOS 6.5操作系统的弹性云服务器时,过一段时间就会自动断开连接.本节操作介绍如何保持SSH会话持续连接不断开该文档适用于Cen ...
- web服务器推送技术
传统模式的 Web 系统以客户端发出请求.服务器端响应的方式工作.不能满足很多现实应用的需求,譬如: 监控系统:后台硬件温度.电压发生变化: 即时通信系统:其它用户登录.发送信息: 即时报价系统:后台 ...
- html5服务器推送消息的各种解决办法,WEB服务器推送消息的各种解决办法
前言:在各种BS架构的应用程序中,往往都希望服务端能够主动地向客户端推送各种消息,以达到类似于邮件.消息.待办事项等通知.而BS架构本身存在的问题就是,服务器一直采用的是一问一答的机制.这就意味着如果 ...
- sse服务器推送性能,SSE 服务端向客户端推送
传统的ajax都是由客户端主动去请求,服务端才可以返回数据 而sse是建立一个通道,并且在断线后自动重连,由服务端去推送,不需要客户端去主动请求,只需要建立通道 websocket是双向通信 客户端可 ...
- Web即时通信技术 -- 服务器推送技术盘点
介绍 Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web即时通讯方案主要有4种: 客户端轮询:传统意义上的短轮询(Short Polling) 服务器端轮询:长轮询( ...
最新文章
- 学生课程表管理系统——stage 1
- 中台是个什么鬼 | 白话中台战略
- lua脚本简单编辑及常用指令
- 工业用微型计算机(21)-指令系统(17)
- leetcode 493. 翻转对(分治算法)
- activemq消息丢失_面试必问之消息中间件
- Qt深入:不能不知道的Type、Attribute和Flags
- 浅谈line-height和vertical-align
- CYQ.Data V4.5.5 版本发布[顺带开源Emit编写的快速反射转实体类FastToT类]
- 信息系统项目管理师计算题(进度管理总浮动时间、自由浮动时间、工期)
- web网页设计实例作业 我的家乡- 达州(4页) HTML+CSS+JavaScript dreamweaver作业静态HTML网页设计模板
- 飞凌小课堂-RK3399系列 linux双千兆网口解决方法-RTL8153
- 逻辑数据库设计 - 单纯的树(递归关系数据)
- 计算机学校教师培训方案,教师基本功培训方案范文
- Arduino的控制(一):Arduino步进电机六轴机械手(油管搬)
- 让发展中国家展示清洁能源领导力
- 整理关于Java进行word文档的数据动态数据填充
- 【深度学习】为什么深度学习需要大内存?
- 2021-2027中国工业物联网通信产品市场现状及未来发展趋势
- 简单使用hbuildx把vue-cli项目打包,并使用electron转换成可执行的exe文件