前言
在前台页面需要不停获取服务器端的数据时,无非有两种操作,一种是通过前台页面使用轮询的方式,定时向服务器后台发送请求,以获取最新的数据;另一种就是在前台页面和后台服务之间建立长连接,服务器端一有数据产生就向前端页面推送。
这里的SSE是服务器发送事件(Server-Sent Events) 的缩写,在WebFlux框架里,服务器端是如何向前端(或调用端)实现服务器发送事件的呢?在有前端页面的情况下,又是如何实现的呢?
带着上面的这些疑问,来了解WebFlux框架,WebFlux框架是一款响应式编程web框架,什么是响应式编程呢,根据wikipedia上的定义:
响应式编程是就是对于数据流和传播改变的一种声明式的编程规范。这意味着可以通过编程语言轻松地表达静态(例如数组)或动态(例如事件发射器)数据流,并且存在相关执行模型内的推断依赖性,这有助于自动传播数据流涉及的变化。
围绕着WebFlux框架的,有这么几个关键字,异步的、非阻塞的、响应式的,那么是不是能够实现数据一有变化,就通知到对应的调用端呢,这些还有待证实。
基于WebFlux框架的SSE应用
首先,在pom文件中,引入webflux框架;
org.springframework.boot
spring-boot-starter-webflux
org.springframework.boot
spring-boot-starter-thymeleaf
第二,html代码,共有四个页面;
sse.html页面代码:
服务器推送事件
sse2.html页面代码:
服务器推送
sse3.html页面代码:
服务器推送
sse4.html页面代码:
服务器推送
注意:在前端页面,接收服务器的推送请求,需要html5的SSE支持,除了IE外,其他的浏览器都支持;
第三,后台代码;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant;
/**
* 需要推送的实体类
* @author 程就人生
* @Date
*/
public class Quote {
private static final MathContext MATH_CONTEXT = new MathContext(2);
private String ticker;
private BigDecimal price;
private Instant instant;
public Quote() {
}
public Quote(String ticker, BigDecimal price) {
this.ticker = ticker;
this.price = price;
}
public Quote(String ticker, Double price) {
this(ticker, new BigDecimal(price, MATH_CONTEXT));
}
@Override
public String toString() {
return "Quote{" +
"ticker='" + ticker + '\'' +
", price=" + price +
", instant=" + instant +
'}';
}
public final String getTicker() {
return ticker;
}
public final void setTicker(String ticker) {
this.ticker = ticker;
}
public final BigDecimal getPrice() {
return price;
}
public final void setPrice(BigDecimal price) {
this.price = price;
}
public final Instant getInstant() {
return instant;
}
public final void setInstant(Instant instant) {
this.instant = instant;
}
}
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
import com.example.entity.Quote;
import reactor.core.publisher.Flux;
/**
* 推送数据,模拟生成
* @author 程就人生
* @Date
*/
@Component
public class QuoteGenerator {
private final MathContext mathContext = new MathContext(2);
private final Random random = new Random();
private final List prices = new ArrayList<>();
/**
* 生成行情数据
*/
public QuoteGenerator() {
this.prices.add(new Quote("CTXS", 82.26));
this.prices.add(new Quote("DELL", 63.74));
this.prices.add(new Quote("GOOG", 847.24));
this.prices.add(new Quote("MSFT", 65.11));
this.prices.add(new Quote("ORCL", 45.71));
this.prices.add(new Quote("RHT", 84.29));
this.prices.add(new Quote("VMW", 92.21));
}
public Flux fetchQuoteStream(Duration period) {
// 需要周期生成值并返回,使用 Flux.interval
return Flux.interval(period)
// In case of back-pressure, drop events
.onBackpressureDrop()
// For each tick, generate a list of quotes
.map(this::generateQuotes)
// "flatten" that List into a Flux
.flatMapIterable(quotes -> quotes)
.log("io.spring.workshop.stockquotes");//以日志的形式输出
}
/**
* Create quotes for all tickers at a single instant.
*/
private List generateQuotes(long interval) {
final Instant instant = Instant.now();
return prices.stream()
.map(baseQuote -> {
BigDecimal priceChange = baseQuote.getPrice()
.multiply(new BigDecimal(0.05 * this.random.nextDouble()), this.mathContext);
Quote result = new Quote(baseQuote.getTicker(), baseQuote.getPrice().add(priceChange));
result.setInstant(instant);
return result;
})
.collect(Collectors.toList());
}
}
import java.time.Duration;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.example.entity.Quote;
import com.example.generator.QuoteGenerator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 数据处理handler,相当于service层
* @author 程就人生
* @Date
*/
@Component
public class QuoteHandler {
private final Flux quoteStream;
public QuoteHandler(QuoteGenerator quoteGenerator) {
this.quoteStream = quoteGenerator.fetchQuoteStream(Duration.ofMillis(1000 * 10)).share();
}
public Mono hello(ServerRequest request) {
Long start = System.currentTimeMillis();
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromObject("Hello Spring!" + start));
}
public Mono echo(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
.body(request.bodyToMono(String.class), String.class);
}
public Mono streamQuotes(ServerRequest request) {
Long start = System.currentTimeMillis();
System.out.println("--------------" + start + "--------------");
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_STREAM_JSON) //返回多次
.body(this.quoteStream, Quote.class);
}
public Mono fetchQuotes(ServerRequest request) {
int size = Integer.parseInt(request.queryParam("size").orElse("10"));
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON) //返回一次
.body(this.quoteStream.take(size), Quote.class);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.example.handler.QuoteHandler;
/**
* 路由,相当于Controller层
* @author 程就人生
* @Date
*/
@Configuration
public class QuoteRouter {
@Bean
public RouterFunction route(QuoteHandler quoteHandler) {
return RouterFunctions
.route(RequestPredicates.GET("/hello1").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), quoteHandler::hello)
.andRoute(RequestPredicates.POST("/echo1").and(RequestPredicates.accept(MediaType.TEXT_PLAIN).and(RequestPredicates.contentType(MediaType.TEXT_PLAIN))), quoteHandler::echo)
//响应一次
.andRoute(RequestPredicates.POST("/quotes").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), quoteHandler::fetchQuotes)
//响应多次
.andRoute(RequestPredicates.GET("/quotes").and(RequestPredicates.accept(MediaType.APPLICATION_STREAM_JSON)), quoteHandler::streamQuotes);
}
}
import java.time.Duration;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuples;
/**
* 服务器发送事件SSE(Server-Sent Events)
* 页面渲染及请求
* @author 程就人生
* @Date
*/
@Controller
@RequestMapping("/sse")
public class SseController {
//三分钟倒计时
private int count_down_sec=3*60*60;
/**
* 推送页面1
* @return
*/
@GetMapping
public String sse(){
return "sse";
}
/**
* 推送页面2
* @return
*/
@GetMapping("/two")
public String sse2(){
return "sse2";
}
/**
* 推送页面3
* @return
*/
@GetMapping("/three")
public String sse3(){
return "sse3";
}
/**
* 推送页面4
* @return
*/
@GetMapping("/four")
public String sse4(){
return "sse4";
}
//报头设置为 "text/event-stream",以便于发送事件流
@GetMapping(value="/countDown",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseBody
public Flux> countDown() {
//每一秒钟推送一次
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> Tuples.of(seq, getCountDownSec()))
.map(data -> ServerSentEvent.
最后,测试运行结果;
总结
虽然参考了很多资料,对于响应式编程还是很陌生,写个demo后,依旧没有感受到它的精华,基于WebFlux框架实现SSE事件,不难看出来还是基于长连接的,在实际场景中,基于长连接的推送事件是否适用,还值得再思考。
参考资料:
https://docs.spring.io/spring-framework/docs/5.0.3.RELEASE/spring-framework-reference/web-reactive.html#webflux-dispatcher-handler
https://blog.csdn.net/wshl1234567/article/details/80320116
https://blog.csdn.net/Message_lx/article/details/81075766
https://www.cnblogs.com/Alandre/category/957422.html
https://segmentfault.com/a/1190000020686218?utm_source=tag-newest
https://my.oschina.net/bianxin/blog/3063713
html5服务器发送事件
https://www.runoob.com/html/html5-serversentevents.html
https://www.xttblog.com/spring-webflux.html