西方著名宗教史家米尔恰·伊利亚德说,如果今天我们不生活在未来,那么未来,我们会生活在过去。
随着移动互联网的普及,基于C/S模式的APP开始了它的又一轮爆发式增长,过去,我们总说要去C/S模式,拥抱B/S,而实际上,任何开发模式都不过是具体的需求应用场景下的产物罢了,并没有绝对的好与坏。今天我们从推送的历史开始说起,介绍一下目前常见的推送实现方式,并结合反应式的例子来继续实战反应式的编程模型的应用。
推送最早是诞生于 Email 中,是用于提醒新的消息,而移动互联网时代则更多的运用在了移动客户端程序(APP)。
推送服务通常是基于提前的信息约束而形成的。也就是采用的设计模式是 生产者/订阅者 模型(publish/subscribe)。wiki定义是,客户端通过订阅由服务器生产的各种信息的频道,不论何时都可以在其中一个频道得到新的内容,同样服务器通过推送把信息传递给相应的客户端。打个比方说,推送服务就像是建立了一个像水管一样的数据管道。
而要获想取服务器的数据,通常有两种方式:第一种是客户端 PULL(拉)方式,即每隔一段时间去服务器获取是否有数据;第二种是服务端 PUSH(推)方式,服务器在有数据的时候主动发给客户端。很显然,PULL 方案优点是简单但是实时性较差,反之PUSH 方案基于 TCP 长连接方式实现,消息实时性好,但是要保持客户端和服务端的长连接心跳,而目前主流的推送实现方式都是基于 PUSH 的方案。
具体的推送实现方式有以下三种——
1、轮询方式(PULL)
客户端和服务器定期的建立连接,通过消息队列等方式来查询是否有新的消息,需要控制连接和查询的频率,频率不能过慢或过快,过慢会导致部分消息更新不及时,过快会就会出现数据压力,严重时甚至会导致后台系统宕机或客服端卡顿等问题。
2、短信推送方式(SMS PUSH)
通过短信发送推送消息,并在客户端植入短信拦截模块(主要针对 Android 平台),可以实现对短信进行拦截并提取其中的内容转发给 App 应用处理,这个方案借助于运营商的短消息,能够保证最好的实时性和到达率,但此方案对于成本要求较高,开发者需要为每一条 SMS 支付费用。
3、长连接方式(PUSH)
基于 TCP 长连接的实现方式, 客户端在主动和服务器建立 TCP 长连接之后, 还会定期向服务器发送心跳包用于保持连接, 有消息的时候, 服务器直接通过这个已经建立好的 TCP 连接通知客户端。尽管长连接也会造成一定的开销,但目前随着基础设施的不断升级完善(比如,更低的流量资费,更大的带宽资源),反而成了最优的方式。不过,随着客户端数量和消息并发量的上升,对于消息服务器的性能和稳定性要求提出了非常大的考验。
下面我们用两种不同的PUSH技术来构建反应式API。
服务器推送事件(Server-Sent Events,SSE),允许服务器端不断地推送数据到客户端。作为 W3C 的推荐规范,SSE 在浏览器端的支持也比较广泛,除了 IE 之外的其他浏览器都提供了支持。在 IE 上也可以使用 polyfill 库来提供支持。
在 WebFlux 中创建 SSE 的服务器端是非常简单的。只需要返回的对象的类型是 Flux,就会被自动按照 SSE 规范要求的格式来发送响应。
创建一个API控制器 SseController,代码如下:
@Controller
@SpringBootApplication
public class SseController {
public static void main(String[] args) {
SpringApplication.run(SseController.class,args);
}
@GetMapping("/randomNumbers")
public Flux> randomNumbers() {
return Flux.interval(Duration.ofSeconds(1))
.map(seq -> Tuples.of(seq, ThreadLocalRandom.current().nextInt()))
.map(data -> ServerSentEvent.builder()
.event("random")
.id(Long.toString(data.getT1()))
.data(data.getT2())
.build());
}
}
代码中的 SseController 是一个使用 SSE 的控制器的示例。其中的方法 randomNumbers()表示的是每隔一秒产生一个随机数的 SSE 端点。我们可以使用类 ServerSentEvent.Builder 来创建 ServerSentEvent 对象。这里我们指定了事件名称为 random,以及每个事件的标识符和数据。事件的标识符是一个递增的整数,而数据则是产生的随机数。
测试类SSEClient代码:
public class SSEClient {
public static void main(final String[] args) {
final WebClient client = WebClient.create();
client.get()
.uri("http://localhost:8080/sse/randomNumbers")
.accept(MediaType.TEXT_EVENT_STREAM)
.exchange()
.flatMapMany(response -> response.body(BodyExtractors.toFlux(new ParameterizedTypeReference>() {
})))
.filter(sse -> Objects.nonNull(sse.data()))
.map(ServerSentEvent::data)
.buffer(11)
.doOnNext(System.out::println)
.blockFirst();
}
}
测试代码中,我们使用 WebClient 访问 SSE 在发送请求部分与访问 REST API 是相同的,不同的地方在于对 HTTP 响应的处理。由于 SSE 服务的响应是一个消息流,我们需要使用 flatMapMany
把 Mono转换成一个 Flux对象,通过方法BodyExtractors.toFlux
来完成,其中的参数 new ParameterizedTypeReference
WebSocket 支持客户端与服务器端的双向通讯。当客户端与服务器端之间的交互方式比较复杂时,可以使用 WebSocket。WebSocket 在主流的浏览器上都得到了支持。
WebFlux 也对创建 WebSocket 服务器端提供了支持。在服务器端,我们需要实现接口 org.springframework.web.reactive.socket.WebSocketHandler
来处理 WebSocket 通讯。接口 WebSocketHandler 的方法 handle 的参数是接口 WebSocketSession 的对象,可以用来获取客户端信息、接送消息和发送消息。
同样创建一个API控制器WebsocketController,代码如下:
@SpringBootApplication
public class WebsocketController {
public static void main(String[] args) {
SpringApplication.run(WebsocketController.class,args);
}
@Bean
public HandlerMapping webSocketMapping(final EchoHandler echoHandler) {
final Map map = new HashMap<>(1);
map.put("/echo", echoHandler);
final SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
mapping.setUrlMap(map);
return mapping;
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
代码中的 EchoHandler 对于每个接收的消息,会发送一个添加了”ECHO -> “前缀的响应消息。WebSocketSession 的 receive 方法的返回值是一个 Flux对象,表示的是接收到的消息流。而 send 方法的参数是一个 Publisher对象,表示要发送的消息流。在 handle 方法,使用 map 操作对 receive 方法得到的 Flux中包含的消息继续处理,然后直接由 send 方法来发送。
测试类WSClient代码:
public class WSClient {
public static void main(final String[] args) {
final WebSocketClient client = new ReactorNettyWebSocketClient();
client.execute(URI.create("ws://localhost:8080/echo"), session ->
session.send(Flux.just(session.textMessage("Hello")))
.thenMany(session.receive().take(1).map(WebSocketMessage::getPayloadAsText))
.doOnNext(System.out::println)
.then())
.block(Duration.ofMillis(5000));
}
}
注意,访问 WebSocket 不能使用 WebClient,而应该使用专门的 WebSocketClient 客户端。Spring Boot 的 WebFlux 模板中默认使用的是 Reactor Netty 库。Reactor Netty 库提供了 WebSocketClient 的实现。WebSocketClient 的 execute 方法与 WebSocket 服务器建立连接,并执行给定的 WebSocketHandler 对象。在 WebSocketHandler 的实现中,首先通过 WebSocketSession 的 send 方法来发送字符串 Hello 到服务器端,然后通过 receive 方法来等待服务器端的响应并输出。方法 take(1)的作用是表明客户端只获取服务器端发送的第一条消息。
通过对推送技术的常用实现方式的介绍,我们了解到推送的内在特征,并通过两个简单的例子进一步的学习了反应式编程的应用场景。我们更能明白,任何一种技术都是在特定的场景下产生和应用的,没有绝对的好与坏,只是看我们如何更好地驾驭它。
示例代码:boot-flux
1、Spring Boot 官方文档
2、WebFlux 参考指南
3、Reactor Netty
我的其它穿越门——持续践行,我们一路同行。
头条号:「说言风语」
简书ID:「mickjoust」
公号:「说言风语」