Engineering
Ben Wilcock
April 06, 2020

Reading Time: about 6 minutes.
Practice Time: about 20 minutes.

If, like me, you’re still at the beginning of your RSocket journey, check out the motivations behind the RSocket protocol. This short but insightful document includes one message that resonates very strongly with me — ‘a mismatched abstraction increases the cost of developing a system.’

From a software design point of view, RSocket’s four interaction models offer a significant benefit. It means we can model our component-to-component communications using the correct interaction model for each use case. This more productive model could save you lots of time and energy when coding!

So far, in this series, we’ve already explored request-response, fire-and-forget, and request-stream messaging. Today you’ll add channels to your client and server code.
What Are Channels?

Channels are bi-directional pipes that allow streams of data to flow in either direction. With channels, a data stream from client-to-server can coexist alongside a data stream from server-to-client. Channels have many real-world uses. Channels could carry video conferencing streams, send and receive two-way chat messages, synchronize data using deltas and diffs, or provide a means to report, observe, and monitor your system.

Channels in RSocket are no more complicated than streams or request-response. That said, the scenario you’ll implement below is slightly more complicated than you’ve attempted previously, so it’s best to understand it before you begin.

In the exercise that follows, the server streams messages to the client. The client controls the frequency of the messages in the server’s stream. It does this using a stream of ‘delay’ settings. The settings in the client’s stream tell the server how long the pause should be between each message it sends. Think of it as a message frequency dial. With the frequency setting high, the pause is shorter, so you’ll see lots of server-sent messages. With the frequency setting low, the pause is longer, so you’ll see fewer server-sent messages. With that outcome in mind, let’s start coding.

The full code sample is available on GitHub. The RSocketController is in the rsocket-server folder in the io.pivotal.rsocketserver package. The RSocketShellClient is in the rsocket-client folder in the io.pivotal.rsocketclient package.

Step 1: Add The Channel Method To The RSocketController

In the server-side RSocketController class, add a method called channel() which accepts a Flux and returns a Flux. This method signature — flux-in, flux out — identifies this method as an RSocket channel method. Annotate the method with @MessageMapping() using the value “channel”. The code for this method is below.

@MessageMapping("channel")
Flux<Message> channel(final Flux<Duration> settings) {
    return settings
                .doOnNext(setting -> log.info("\nFrequency setting is {} second(s).\n", setting.getSeconds()))
                .switchMap(setting -> Flux.interval(setting)
                                               .map(index -> new Message(SERVER, CHANNEL, index)))
                                               .log();
}

In the code, the .doOnNext() is listening to the stream of settings coming from the client. Each time a new delay setting arrives, it writes a message to the log. The .switchMap() creates a new Flux for each new setting. This new flux emits a new Message object based on the .interval() delay contained in the delay setting. The server sends these new messages back to the client in the stream.
Step 2: Add The Channel Method To The RSocketShellClient

In the client-side RSocketShellClient class, add a new channel() method and annotate it with the @ShellMethod() annotation. Add a description of the method’s purpose as the annotation value, as shown in the example below.

@ShellMethod("Stream some settings to the server. Stream of responses will be printed.")
public void channel(){

// Code goes here

}

Next, in the method, add the code that creates the stream of settings. The code looks like this:

Mono setting1 = Mono.just(Duration.ofSeconds(1));
Mono setting2 = Mono.just(Duration.ofSeconds(3)).delayElement(Duration.ofSeconds(5));
Mono setting3 = Mono.just(Duration.ofSeconds(5)).delayElement(Duration.ofSeconds(15));

Flux settings = Flux.concat(setting1, setting2, setting3)
.doOnNext(d -> log.info("\nSending setting for {}-second interval.\n", d.getSeconds()));

Each Mono contains a single Duration setting. Each duration controls the pause between each message coming from the server. There are 3 monos in total. The first contains a short pause setting of 1 second. The second has a more relaxed pause setting of 3 seconds, but this mono is told to delay the production of this duration by 5 seconds using the .delayElement() method. The third mono contains a pause setting of 5 seconds, but won’t emit its duration until 15 seconds have passed. These 3 mono’s get concatenated into a single Flux using the .concat() method. A logging statement is added using .doOnNext() so you can see what’s happening when the code is running.

Note: There are many ways to build a Flux based stream, but for this tutorial, it’s just something simple.

Now you have the stream of settings inside the flux, add to the method the code required to communicate with the server:

disposable = this.rsocketRequester
.route(“channel”)
.data(settings)
.retrieveFlux(Message.class)
.subscribe(message -> log.info(“Received: {} \n(Type ‘s’ to stop.)”, message));

If you’ve been following the series, this code looks familiar by now. The rsocketRequester is a global variable you built in the constructor. It provides your RSocket communication link with the server. The .route() is set to “channel” to match the message mapping in the server-side code. The .data() is the stream of mono’s you created above. The .retrieveFlux() is specifying that you expect a stream of Message objects, and the .subscribe() begins your subscription and ensures that each message received is printed to the log so you can see what’s happening. The Disposable object created by the subscription is kept and used to control the channel.

You can see the complete code for the method here. That’s all the code we need. Let’s run it!
Step 3: Build And Run The RSocket Server

Open a terminal window and move to the rsocket-server directory and run the server as follows:

cd rsocket-server
./mvnw clean package spring-boot:run -DskipTests=true

The server starts up on localhost port 7000.
Step 4: Build And Run The RSocket Client

Open a second terminal window and move to the rsocket-client directory. From there, build and run the client as follows:

cd rsocket-client
./mvnw clean package spring-boot:run -DskipTests=true

Once booted, Spring Shell presents you with a new prompt:

shell:>

You request a channel from the server by typing channel at the prompt.

The client creates its stream of delay timer settings and begins to send them to the server. Each duration in the outbound steam gets printed by the client and the server. The server sends back a stream of messages, which the client prints out to the log. The terminal on the client-side looks something like this:

shell:>channel
i.p.rsocketclient.RSocketShellClient :

Sending setting for 1-second interval.

i.p.rsocketclient.RSocketShellClient : Received: Message(origin=Server, interaction=Channel, index=0, created=1585304561)
(Type ‘s’ to stop.)

removed log-lines

i.p.rsocketclient.RSocketShellClient :

Sending setting for 3-second interval.

i.p.rsocketclient.RSocketShellClient : Received: Message(origin=Server, interaction=Channel, index=0, created=1585304568)
(Type ‘s’ to stop.)

removed log-lines

i.p.rsocketclient.RSocketShellClient :

Sending setting for 5-second interval.

2020-03-27 10:23:00.243 INFO 5680 — [tor-tcp-epoll-1] i.p.rsocketclient.RSocketShellClient : Received: Message(origin=Server, interaction=Channel, index=4, created=1585304580)
(Type ‘s’ to stop.)

removed log-lines

To stop the channeling, type s and then tap Enter.
Step 5: Tidy Up

You can exit the rsocket-client by typing exit at the shell:> prompt like this.

shell:>exit

You can stop the rsocket-server process by pressing Ctrl-C in its terminal window.
How It Works

The client creates a sequence of 3 duration elements. The first duration setting gets emitted immediately, the second after 5 seconds have passed, and the third after 15 seconds have passed. Each time a new duration is emitted, it’s logged to the console. This stream of settings gets sent to the server and controls the server’s generated stream of messages.

On the server-side, every time a new duration setting gets extracted from the client’s stream, a new stream of messages is created and returned. The most recent setting sent from the client controls the time delay between each message in this server-sent stream.

The channeling stops when the subscription’s disposable object is disposed of by the client.
Final Thoughts

If you followed the whole series, you’ve now seen all four of RSocket’s interaction models in action: request-response, fire-and-forget, request-stream, and now channels too.

With these four communication styles at your disposal, you’re far less likely to encounter one of those pesky ‘mismatched abstraction’ scenarios we discussed at the beginning. With RSocket in your toolbox, you’ll have a flexible, low friction, high-performance messaging protocol you can use in your software — one that’s purpose-built for microservice architectures.
comments powered by Disqus

translate:
翻译:

阅读时间:约6分钟。
练习时间:约20分钟。

如果像我一样,您仍处于RSocket旅程的开始,请查看RSocket协议背后的动机。这份简短而有见地的文档包含一条与我非常共鸣的信息-“不匹配的抽象会增加开发系统的成本。”

从软件设计的角度来看,RSocket的四个交互模型具有显着的优势。这意味着我们可以为每个用例使用正确的交互模型来建模组件之间的通信。这种效率更高的模型在编码时可以节省大量时间和精力!

到目前为止,在本系列文章中,我们已经研究了请求-响应,即发即弃和请求流消息传递。今天,您将为客户端和服务器代码添加频道。
什么是频道?

通道是双向管道,允许数据流沿任一方向流动。通过通道,可以将客户端到服务器的数据流与服务器到客户端的数据流共存。频道有许多实际用途。频道可以承载视频会议流,发送和接收双向聊天消息,使用增量和差异来同步数据,或者提供一种报告,观察和监视系统的方式。

RSocket中的通道不比流或请求响应复杂。也就是说,您将在下面实现的场景比以前尝试的要复杂一些,因此最好在开始之前先了解一下。

在接下来的练习中,服务器将消息流传输到客户端。客户端控制服务器流中邮件的频率。它使用“延迟”设置流来执行此操作。客户端流中的设置告诉服务器,每发送一条消息,暂停应间隔多长时间。将其视为消息频率拨号。频率设置越高,暂停时间越短,因此您会看到许多服务器发送的消息。频率设置较低时,暂停时间会更长,因此您将看到更少的服务器发送消息。考虑到这一结果,让我们开始编码。

完整的代码示例可在GitHub上获得。 RSocketController在io.pivotal.rsocketserver软件包的rsocket-server文件夹中。 RSocketShellClient位于io.pivotal.rsocketclient软件包的rsocket-client文件夹中。

步骤1:将Channel方法添加到RSocketController

在服务器端RSocketController类中,添加一个名为channel()的方法,该方法接受Flux 并返回Flux 。该方法签名(流入,流出)将该方法标识为RSocket通道方法。使用@MessageMapping()使用值“通道”注释该方法。该方法的代码如下。

@MessageMapping(“ channel”)
通量<消息>通道(最终通量<持续时间>设置){
    返回设置
                .doOnNext(设置-> log.info(“ \ n频率设置为{}秒。\ n”,setting.getSeconds()))
                .switchMap(设置-> Flux.interval(设置)
                                               .map(index-> new Message(SERVER,CHANNEL,index)))
                                               .log();
}

在代码中,.doOnNext()正在侦听来自客户端的设置流。每当新的延迟设置到达时,它都会向日志中写入一条消息。 .switchMap()为每个新设置创建一个新的Flux。这个新的通量基于延迟设置中包含的.interval()延迟发出一个新的Message对象。服务器将这些新消息发送回流中的客户端。
步骤2:将Channel方法添加到RSocketShellClient

在客户端RSocketShellClient类中,添加一个新的channel()方法并使用@ShellMethod()批注对其进行批注。如下例所示,将方法用途的描述添加为注释值。

@ShellMethod(“将某些设置传输到服务器。将打印响应流。”)
公共无效频道(){

//代码在这里

}

接下来,在方法中,添加创建设置流的代码。代码如下:

Mono 设置1 = Mono.just(Duration.ofSeconds(1));
 Mono setting2 = Mono.just(Duration.ofSeconds(3))。delayElement(Duration.ofSeconds(5));
 Mono 设置3 = Mono.just(Duration.ofSeconds(5))。delayElement(Duration.ofSeconds(15));Flux 设置= Flux.concat(setting1,setting2,setting3)
 .doOnNext(d-> log.info(“ \ n以{}秒为间隔发送设置。\ n”,d.getSeconds()));

每个单声道都包含一个持续时间设置。每个持续时间控制着来自服务器的每条消息之间的暂停。总共有3个单声道。第一个包含1秒的短暂停设置。第二个具有更宽松的3秒暂停设置,但是使用.delayElement()方法告知此单声道将此持续时间的生成延迟5秒。