Spring Boot 实践折腾记(13):使用WebFlux构建响应式「推送API 」

西方著名宗教史家米尔恰·伊利亚德说,如果今天我们不生活在未来,那么未来,我们会生活在过去。

随着移动互联网的普及,基于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。

场景1:服务器推送事件

服务器推送事件(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,以及每个事件的标识符和数据。事件的标识符是一个递增的整数,而数据则是产生的随机数。

测试1场景

测试类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

场景2:WebSocket

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 方法来发送。

测试2场景

测试类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」
公号:「说言风语」

你可能感兴趣的:(springboot)