RSocket over TCP¶
TCP is a reliable, connection-oriented transport. RSocket uses TCP to carry its messaging protocol between clients and servers.
Getting started¶
The fastest way to begin is to generate a project from Spring Initializr, as described in the Getting Started chapter. Select Spring WebFlux, RSocket, and Lombok — the resulting dependency is the RSocket starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-rsocket</artifactId>
</dependency>
Configure the server to accept RSocket connections over TCP on port 7000 in application.properties:
Defining the message payload¶
The client and server exchange a very small payload object:
Serving routes with @MessageMapping¶
@MessageMapping handles incoming RSocket routes the same way @GetMapping handles HTTP paths. The return type tells Spring which interaction model to use:
@Controller
@Slf4j
class GreetingServerController {
@MessageMapping("hello")
public Mono<Void> hello(Greeting p) {
log.info("received: {} at {}", p, Instant.now());
return Mono.empty();
}
@MessageMapping("greet.{name}")
public Mono<String> greet(@DestinationVariable String name, @Payload Greeting p) {
log.info("received: {}, {} at {}", name, p, Instant.now());
return Mono.just("Hello " + name + ", " + p.getMessage() + " at " + Instant.now());
}
@MessageMapping("greet-stream")
public Flux<String> greetStream(@Payload Greeting p) {
log.info("received: {} at {}", p, Instant.now());
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "greet-stream#(Hello #" + i + "," + p.getMessage() + ") at " + Instant.now());
}
@MessageMapping("greet-channel")
public Flux<String> greetChannel(@Payload Flux<Greeting> p) {
log.info("received: {} at {}", p, Instant.now());
return p.delayElements(Duration.ofSeconds(1))
.map(m -> "greet-channel#(" + m + ") at " + Instant.now());
}
}
Reading the handlers:
hello— a fire-and-forget route: theMono<Void>completes when the payload is consumed, and nothing is sent back.greet.{name}— a request/response route:@DestinationVariablecaptures the.{name}segment, and theMono<String>reply is sent back to the caller.greet-stream— a request/stream route: one incomingGreetingyields an endlessFlux<String>of periodic greetings.greet-channel— a channel route: the handler receives aFlux<Greeting>from the client and returns a transformedFlux<String>, creating a bidirectional exchange.
Start the server:
Calling the routes with RSocketRequester¶
The client side uses an RSocketRequester, built from the auto-configured Builder and a Netty TCP transport:
@Bean
public RSocketRequester rSocketRequester(RSocketRequester.Builder b) {
return b.dataMimeType(MimeTypeUtils.APPLICATION_JSON)
.connect(TcpClientTransport.create("localhost", 7000)).block();
}
Then call the routes from a REST controller — each interaction model exposed behind a tiny HTTP endpoint:
@Slf4j
@RestController()
@RequiredArgsConstructor
class GreetingController {
private final RSocketRequester requester;
@GetMapping("hello")
Mono<Void> hello() {
return this.requester.route("hello").data(new Greeting("Welcome to RSocket")).send();
}
@GetMapping("name/{name}")
Mono<String> greet(@PathVariable String name) {
return this.requester.route("greet." + name)
.data(new Greeting("Welcome to RSocket"))
.retrieveMono(String.class)
.doOnNext(msg -> log.info("received message::" + msg));
}
@GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> greetStream() {
return this.requester.route("greet-stream").data(new Greeting("Welcome to RSocket"))
.retrieveFlux(String.class)
.doOnNext(msg -> log.info("received messages::" + msg));
}
@GetMapping(value = "channel", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> greetChannel() {
return this.requester.route("greet-channel")
.data(Flux.range(0, 10).map(i -> new Greeting("Welcome to RSocket #" + i)))
.retrieveFlux(String.class)
.doOnNext(msg -> log.info("received messages::" + msg));
}
}
The client-side verbs are straightforward: .send() for fire-and-forget calls, .retrieveMono() for a single reply, .retrieveFlux() for streams and channels, and .data(Flux...) when the upstream is a channel.
Testing¶
In the ported module, the client code becomes tests. We connect with the builder and verify each interaction model with StepVerifier:
@SpringBootTest
public class IntegrationTests {
static RSocketRequester requester;
@BeforeAll
public static void setup(@Autowired RSocketRequester.Builder builder) {
requester = builder
.dataMimeType(MimeTypeUtils.APPLICATION_JSON)
.connect(TcpClientTransport.create("localhost", 7000)).block();
}
@Test
public void testRequestResponse() {
requester.route("greet.spring")
.data(new Greeting("Welcome to RSocket"))
.retrieveMono(String.class)
.as(StepVerifier::create)
.consumeNextWith(msg -> assertTrue(msg.contains("Hello spring")))
.verifyComplete();
}
@Test
public void testRequestChannel() {
Flux<Greeting> payloads = Flux.range(0, 10)
.map(i -> new Greeting("Welcome to RSocket #" + i));
requester.route("greet-channel")
.data(payloads)
.retrieveFlux(String.class)
.take(10)
.as(StepVerifier::create)
.expectNextCount(10)
.verifyComplete();
}
}
The @LocalRSocketServerPort annotation from earlier Boot versions no longer exists in Boot 4, so the tests connect to the fixed port configured in application.properties.
See the complete example and tests in boot-rsocket-tcp.