Skip to content

Commit 9707666

Browse files
committed
docs: add GUIDE.md
1 parent 64195bd commit 9707666

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

GUIDE.md

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Building a Chat application with Angular and Spring RSocket
2+
3+
4+
In this post, we will use RSocket protocol to reimplement the chat application.
5+
6+
If you have missed the former posts about implementing the chat application, there is a checklist.
7+
8+
* [Building a chat app with Angular and Spring reactive WebSocket](https://medium.com/@hantsy/building-a-chat-application-with-angular-and-spring-reactive-websocket-400e0769f4ec) and [part 2](https://medium.com/@hantsy/building-a-chat-application-with-angular-and-spring-reactive-websocket-part-2-ad140125cbd2)
9+
* [Building a chat app with Angular and Spring reactive Server Sent Events](https://medium.com/@hantsy/building-a-chat-application-with-angular-and-spring-reactive-sse-c0fdddcd7d70)
10+
11+
[RSocket](https://www.rsocket.io) is a binary protocol for use on byte stream transports, such as TCP, WebSocket, RCP etc.
12+
13+
RSocket embraces ReactiveStreams semantics, and Spring provides excellent RSocket support through the existing messaging infrastructure. I have introduced RSocket in my former posts, check here.
14+
15+
* [Using RSocket with Spring](https://medium.com/@hantsy/using-rsocket-with-spring-boot-cfc67924d06a)
16+
* [Building a CRUD application with RSocket and Spring](https://medium.com/@hantsy/building-a-crud-application-with-rsocket-and-spring-936570c72467)
17+
18+
In this post, we will use WebSocket as transport protocol which is good for web application. RSocket defines 4 interaction modes, we will use *fire-and-forget* to send a message to the server side, and *request/streams* to retrieve messages as an infinite stream from the server.
19+
20+
Firstly let's create the server application. Generate a project skeleton using [Spring Initializr](https://start.spring.io).
21+
22+
* Project type: Gradle
23+
* Language: Kotlin
24+
* Spring Boot version :2.4.0M1
25+
* Project Metadata/Java: 14
26+
* Dependencies: Reactive Web, RSocket
27+
28+
Hit the **Generate** button to download the generated archive, and extract it into your local disk.
29+
30+
Make sure you have installed the latest JDK 14 ([AdoptOpenJDK]( https://adoptopenjdk.net/) is highly recommended), then import the source codes in your IDEs. eg. Intellij IDEA, and start to implement the server side.
31+
32+
> We also skip the discussion of Reactor's Sink implementation here.
33+
34+
Create a `Message` document definition and a `Repository` for it.
35+
36+
```kotlin
37+
interface MessageRepository : ReactiveMongoRepository<Message, String> {
38+
@Tailable
39+
fun getMessagesBy(): Flux<Message>
40+
}
41+
42+
@Document(collection = "messages")
43+
data class Message(@Id var id: String? = null, var body: String, var sentAt: Instant = Instant.now())
44+
```
45+
46+
Create a `@Controller` to handle messages.
47+
48+
```kotlin
49+
@Controller
50+
class MessageController(private val messages: MessageRepository) {
51+
@MessageMapping("send")
52+
fun hello(p: String) = this.messages.save(Message(body = p, sentAt = Instant.now())).log().then()
53+
54+
@MessageMapping("messages")
55+
fun messageStream(): Flux<Message> = this.messages.getMessagesBy().log()
56+
}
57+
```
58+
59+
The **send** route accepts a String based message payload and return a `Mono<Void>`, which will handle messages of the *fire-and-forget* mode from clients. The **messages** route accepts a null payload and return a `Flux<Message>`, which will act as the handler of *request-stream* mode.
60+
61+
> If you are new to the Spring RSocket , you may be confused how `@Controller` and `MessageMapping` are mapped to the interaction modes which the original RSocket message handler used. Spring hides the complexity of the RSocket protocol itself , and reuse the existing messaging infrastructure to handle RSocket messages. Remember, compare the incoming payload and outgoing message type with 4 interaction mode definitions in the official [RSocket](rsocket.io) website, you can determine which interaction mode it is mapped to.
62+
63+
Configure RSocket to use websocket transport in the *application.properties* file.
64+
65+
```properties
66+
# a mapping path is defined
67+
spring.rsocket.server.mapping-path=/rsocket
68+
# websocket is chosen as a transport
69+
spring.rsocket.server.transport=websocket
70+
```
71+
72+
Start a MongoDB service as follows.
73+
74+
```bash
75+
docker-compose up mongodb
76+
```
77+
78+
> As described in the former posts, you have to prepare a **capped** messages collection, check [this post ](https://medium.com/@hantsy/building-a-chat-application-with-angular-and-spring-reactive-websocket-part-2-ad140125cbd2) for more details.
79+
80+
Run the following command to start the server side application.
81+
82+
```bash
83+
./gradlew bootRun
84+
```
85+
86+
I have written a small integration test to verify if it works.
87+
88+
```kotlin
89+
@SpringBootTest
90+
class RSocketServerApplicationTests {
91+
92+
@Autowired
93+
lateinit var rSocketRequester: RSocketRequester;
94+
95+
@Test
96+
fun contextLoads() {
97+
98+
val verifier= rSocketRequester.route("messages")
99+
.retrieveFlux(Message::class.java)
100+
.log()
101+
.`as` { StepVerifier.create(it) }
102+
.consumeNextWith { it -> assertThat(it.body).isEqualTo("test message") }
103+
.consumeNextWith { it -> assertThat(it.body).isEqualTo("test message2") }
104+
.thenCancel()
105+
.verifyLater()
106+
rSocketRequester.route("send").data("test message").send().then().block()
107+
rSocketRequester.route("send").data("test message2").send().then().block()
108+
109+
verifier.verify(Duration.ofSeconds(5))
110+
}
111+
112+
@TestConfiguration
113+
class TestConfig {
114+
115+
@Bean
116+
fun rSocketRequester(builder: RSocketRequester.Builder) = builder.dataMimeType(MimeTypeUtils.APPLICATION_JSON)
117+
.connectWebSocket(URI.create("ws://localhost:8080/rsocket")).block()
118+
}
119+
120+
}
121+
```
122+
123+
In the above codes, use a test specific `@TestConfiguration` to define a `RSocketRequester` bean, which is a helper to communicate with the server side.
124+
125+
Let's move to the frontend application.
126+
127+
Create a new Angular project, and add two dependencies: `roscket-core`, `rsocket-websocket-client`.
128+
129+
```bash
130+
npm install roscket-core rsocket-websocket-client
131+
```
132+
Fill the following codes in the `app.component.ts` file. I've spent some time on making this work with my backend, the article [ RSocket With Spring Boot + JS: Zero to Hero](https://dzone.com/articles/rsocket-with-spring-boot-amp-js-zero-to-hero) from [Domenico Sibilio ](https://dzone.com/users/3880926/domenicosibilio.html) is very helpful. The [rsocket-js ](https://github.com/rsocket/rsocket-js) project also includes excellent examples.
133+
134+
```typescript
135+
export class AppComponent implements OnInit, OnDestroy {
136+
137+
title = 'client';
138+
message = '';
139+
messages: any[];
140+
client: RSocketClient;
141+
sub = new Subject();
142+
143+
ngOnInit(): void {
144+
this.messages = [];
145+
146+
// Create an instance of a client
147+
this.client = new RSocketClient({
148+
serializers: {
149+
data: JsonSerializer,
150+
metadata: IdentitySerializer
151+
},
152+
setup: {
153+
// ms btw sending keepalive to server
154+
keepAlive: 60000,
155+
// ms timeout if no keepalive response
156+
lifetime: 180000,
157+
// format of `data`
158+
dataMimeType: 'application/json',
159+
// format of `metadata`
160+
metadataMimeType: 'message/x.rsocket.routing.v0',
161+
},
162+
transport: new RSocketWebSocketClient({
163+
url: 'ws://localhost:8080/rsocket'
164+
}),
165+
});
166+
167+
// Open the connection
168+
this.client.connect().subscribe({
169+
onComplete: (socket: RSocket) => {
170+
171+
// socket provides the rsocket interactions fire/forget, request/response,
172+
// request/stream, etc as well as methods to close the socket.
173+
socket
174+
.requestStream({
175+
data: null, // null is a must if it does not include a message payload, else the Spring server side will not be matched.
176+
metadata: String.fromCharCode('messages'.length) + 'messages'
177+
})
178+
.subscribe({
179+
onComplete: () => console.log('complete'),
180+
onError: error => {
181+
console.log("Connection has been closed due to:: " + error);
182+
},
183+
onNext: payload => {
184+
console.log(payload);
185+
this.addMessage(payload.data);
186+
},
187+
onSubscribe: subscription => {
188+
subscription.request(1000000);
189+
},
190+
});
191+
192+
this.sub.subscribe({
193+
next: (data) => {
194+
socket.fireAndForget({
195+
data: data,
196+
metadata: String.fromCharCode('send'.length) + 'send',
197+
});
198+
}
199+
})
200+
},
201+
onError: error => {
202+
console.log("Connection has been refused due to:: " + error);
203+
},
204+
onSubscribe: cancel => {
205+
/* call cancel() to abort */
206+
}
207+
});
208+
}
209+
210+
addMessage(newMessage: any) {
211+
console.log("add message:" + JSON.stringify(newMessage))
212+
this.messages = [...this.messages, newMessage];
213+
}
214+
215+
ngOnDestroy(): void {
216+
this.sub.unsubscribe();
217+
if (this.client) {
218+
this.client.close();
219+
}
220+
}
221+
222+
sendMessage() {
223+
console.log("sending message:" + this.message);
224+
this.sub.next(this.message);
225+
this.message = '';
226+
}
227+
}
228+
229+
```
230+
231+
Reuse the template file we've used in the former posts.
232+
233+
```html
234+
<div fxFlex>
235+
<p *ngFor="let m of messages">
236+
{{m|json}}
237+
</p>
238+
</div>
239+
<div>
240+
<form fxLayout="row baseline" #messageForm="ngForm" (ngSubmit)="sendMessage()">
241+
<mat-form-field fxFlex>
242+
<input name="message" fxFill matInput #messageCtrl="ngModel" [(ngModel)]="message" required />
243+
<mat-error fxLayoutAlign="start" *ngIf="messageCtrl.hasError('required')">
244+
Message body can not be empty.
245+
</mat-error>
246+
</mat-form-field>
247+
<div>
248+
<button mat-button mat-icon-button type="submit" [disabled]="messageForm.invalid || messageForm.pending">
249+
<mat-icon>send</mat-icon>
250+
</button>
251+
</div>
252+
</form>
253+
</div>
254+
```
255+
256+
Next run the client application.
257+
258+
```bash
259+
npm run start
260+
```
261+
262+
Open two browser windows(or two different browsers), type some messages in each window and experience it.
263+
264+
![run](./run.png)
265+
266+
> I found a weird issue may be caused by the JSON Serializer encode/decode from the roscket-js project, I described it in [rsocket-js issues #93](https://github.com/rsocket/rsocket-js/issues/93), if you have some idea to overcome this, please comment on this issue.
267+
268+
Get [the complete codes](https://github.com/hantsy/angular-spring-rsocket-sample) from my github.

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ This sample is to demonstrate a chat application using the following cutting-edg
66
* Spring WebFlux based RSocket server which uses WebSocket as transport protocol
77
* Spring Data MongoDB based `@Tailable` query result as an infinite stream
88

9+
## Prerequisites
10+
11+
* NodeJS 14
12+
* OpenJDK 14
13+
* Docker for Windows/MacOS
14+
915
## Build
1016

1117
Before running the application, you should build and run client and server side respectively.
@@ -50,3 +56,4 @@ Open a browser and navigate to http://localhost:4200.
5056
## Reference
5157

5258
* [rsocket-js samples](https://github.com/rsocket/rsocket-js/blob/master/packages/rsocket-examples)
59+
* [RSocket With Spring Boot + JS: Zero to Hero ](https://dzone.com/articles/rsocket-with-spring-boot-amp-js-zero-to-hero)

run.png

65.1 KB
Loading

server/src/main/kotlin/com/example/demo/RSocketServerApplication.kt

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class RSocketServerApplication {
2020
@Bean
2121
fun runner(template: ReactiveMongoTemplate) = CommandLineRunner {
2222
println("running CommandLineRunner...")
23+
template.insert(Message(body="test")).then().block()
2324
template.executeCommand("{\"convertToCapped\": \"messages\", size: 100000}").log().subscribe(::println)
2425
}
2526
}

0 commit comments

Comments
 (0)