with spring boot rsocket capture the cancel frame type - spring-boot

I have a spring boot rsocket implementation where if a client cancels or closes their rsocket request then I want to cancel other subscription registrations on the server.
In the logs on the spring boot server I can see that a cancel message is sent or received:
WARN i.r.t.n.s.WebsocketServerTransport$1 [reactor-http-nio-3] received WebSocket Close Frame - connection is closing
INFO r.u.Loggers$Slf4JLogger [reactor-http-nio-3] cancel()
How do I capture and handle this cancel signal?
I tried cancel endpoints but these don't capture the signal:
#MessageMapping("cancel")
Flux<Object> onCancel() {
log.info("Captured cancel signal");
}
or
#ConnectMapping("cancel")
Flux<Object> onCancel2() {
log.info("Captured cancel2 signal");
}
This question on cancel subscriptions is possibly related, and this question on detecting websocket disconnection

To capture the cancel signal you can use subscribe to onClose() event.
In your controller
#Controller
class RSocketConnectionController {
#ConnectMapping("client-id")
fun onConnect(rSocketRequester: RSocketRequester, clientId: String) {
// rSocketRequester.rsocket().dispose() //to reject connection
rSocketRequester
.rsocket()
.onClose()
.subscribe(null, null, {
log.info("{} just disconnected", clientId)
//TODO here whatever you want
})
}
}
Your client needs to send the SETUP frame properly to invoke this #ConnectMapping. If you use rsocket-js you need to add a payload like this:
const client = new RSocketClient({
// send/receive JSON objects instead of strings/buffers
serializers: {
data: JsonSerializer,
metadata: IdentitySerializer
},
setup: {
//for connection mapping on server
payload: {
data: 'unique-client-id', //TODO you can receive this data on server side
metadata: String.fromCharCode("client-id".length) + "client-id"
},
// ms btw sending keepalive to server
keepAlive: 60000,
.....
}
});

It was not a well set out question. The answer is that
INFO r.u.Loggers$Slf4JLogger [reactor-http-nio-3] cancel()
is seen by a FluxSink that was setup from the original #MessageMapping endpoint.
For example:
#MessageMapping("hello")
Flux<Object> hello(#Payload String message) {
return myService.generateWorld(message);
}
In myService class
public Flux<Object> generateWorld(String hello) {
EmitterProcessor<Object> emitter = EmitterProcessor.create();
FluxSink<Object> sink = emitter.sink(FluxSink.OverflowStrategy.LATEST);
// doing stuff with sink here
sink.next(stuff());
// This part will handle a cancel from the client
sink.onCancel(() -> {log.info("********** SINK.onCancel ***********");});
return Flux.from(emitter));
}
The sink.onCancel() will handle a cancel of the flux to the hello endpoint, from the client.

Related

Webflux RSocket Server using RSocketRequestor sending message to Rsocket-js client .Responder not receiving

My requirement is that as and when some event happens on server, it should push updates to client. Want to use RSocket and not SSE.
Will responder configured on the rsocket-js client respond to server request?
If yes then how should the rsocket-js responder be configured to accept message on a particular route? That part please clarify.
Not sure if my spring service is correct.
My Webflux Spring Boot RSocket server code-
#Service
#RequiredArgsConstructor
public class RsocketService {
private final RSocketRequester rSocketRequester;
public void serverToClientRequest(){
Mono.just("Your request is completed at "+ LocalDateTime.now())
.delayElement(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(5, 10)))
.flatMap(m -> rSocketRequester.route("request.stream").data(m).send())
.subscribe();
}
}
I have a Controller -
#MessageMapping("request.stream")
public Flux<String> requestStream(#Payload String requestData) {
}
Client side I am using RSocketWebSocketClient from 'rsocket-websocket-client';
const client = new RSocketClient({
responder: new EchoResponder(),
transport: new RSocketWebSocketClient(
{
url: 'ws://localhost:7000/rsocket',
wsCreator: (url: string) => new WebSocket(url),
debug: true,
}
),
setup: {
dataMimeType: "text/plain",
metadataMimeType: 'message/x.rsocket.routing.v0',
keepAlive: 600000,
lifetime: 180000,
}
});
My reactjs component-
async componentDidMount() {
const rsocket= await client.connect();
console.log('rsocket client connected');
rsocket
.requestStream({
data: "client message",
metadata: String.fromCharCode('request.stream'.length) + 'request.stream'
})
.subscribe({
onComplete: () => {
console.log("request stream completed");
},
onNext: value => {
console.log("on next-->got data from sever");
console.log(value.data);
},
onError: (error: any) => {
console.log("got error with requestResponse");
console.error(error);
},
onSubscribe: sub => {
console.log("subscribe request Stream!");
sub.request(2147483647);
}
});
}
EchoResponder is taken from https://github.com/rsocket/rsocket-js/blob/master/packages/rsocket-examples/src/LeaseClientExample.js. But the responder does not get any message. Any help is appreciated please.

rsocket-js routing fireAndForget to Spring Boot #MessageMapping

As I understand RSocket-JS supports routing messages using encodeCompositeMetadata and encodeRoute, however, I cannot get the server to accept a fireAndForget message. The server constantly logs the following message:
o.s.m.r.a.support.RSocketMessageHandler : No handler for fireAndForget to ''
This is the server mapping I am trying to trigger:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}
This is the TypeScript code that's trying to make the connection:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: { someData: "Hello world!" },
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
I've also tried adding the route in other ways (metadata: String.fromCharCode('route'.length)+'route') I found on the internet, but none seem to work.
What do I need to do to format the route in a way that the Spring Boot server recognizes it and can route the message correctly?
Binary only communication when using CompositeMetadata
Please make sure that you have configured your ClientTransport with binary codecs as follows:
new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>'
},
BufferEncoders,
),
Having Binary encoders you will be able to properly send your routes using composite metadata.
Also, please make sure that you have configured metadataMimeType as:
...
const metadataMimeType = MESSAGE_RSOCKET_COMPOSITE_METADATA.string; // message/x.rsocket.composite-metadata.v0
new RSocketClient<Buffer, Buffer>({
setup: {
...
metadataMimeType,
},
transport: new RSocketWebSocketClient(
{
url: 'ws://<host>:<port>',
},
BufferEncoders,
),
});
Note, once you enabled BufferEncoders your JSONSeriallizer will not work and you would need to encode your JSON to binary yours selves ( I suggest doing that since in the future versions we will remove support of Serializers concept completely). Therefore, your request has to be adjusted as it is in the following example:
client.connect().subscribe({
onComplete: socket => {
console.log("Connected to socket!")
socket.fireAndForget({
data: Buffer.from(JSON.stringify({ someData: "Hello world!" })),
metadata: encodeCompositeMetadata([[MESSAGE_RSOCKET_ROUTING, encodeRoute("fire-and-forget")]])
});
},
onError: error => console.error(error),
onSubscribe: cancel => {/* call cancel() to abort */ }
});
Use #Payload annotation for your payload at spring backend
Also, to handle any data from the client and to let Spring know that the specified parameter argument is your incoming request data, you have to annotate it with the #Payload annotation:
#Controller
public class MockController {
private static final Logger LOGGER = LoggerFactory.getLogger(MockController.class);
#MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(#Payload MockData mockData) {
LOGGER.info("fireAndForget: {}", mockData);
return Mono.empty();
}
}

Spring Websocket convertAndSendToUser doesn't work

I successfully subscribe to a queue, but when sending a message, another user does not receive it
message broker configuration:
override fun configureMessageBroker(config: MessageBrokerRegistry) {
config.enableSimpleBroker("/queue", "/topic")
config.setApplicationDestinationPrefixes("/app")
}
How i send message
messageTemplate.convertAndSendToUser(recipientId.toString(), "/queue/chat", message)
Creating custom user principal on ws connection
override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
val accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor::class.java)
if (accessor != null && StompCommand.CONNECT == accessor.command) {
val customPrincipal = CustomPrincipal(userId)
val authentication = CustomAuthToken(customPrincipal, roles)
accessor.user = authentication
return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
}
return message
}
}
On client side i connect socket
this.stompClient.connect(headers, () => {
this.stompClient.subscribe(headers, "/user/queue/chat", (message) => {
console.log("connected");
console.log(message);
});
});
When i connect to stomp i see in browser console next message
<<< CONNECTED
user-name:1
heart-beat:0,0
version:1.2
content-length:0
user-name is my actual id for this user
I subscribe user with id 1 and user with id 2 to same queue, when i send a message from user 1 to user 2 second user doesn't receive it.
Did i miss something?
After 5 hours of debugging Spring i found how to fix this issue
Just set accessor (in preSend method) immutable
accessor.setLeaveMutable(false)
Still don't fully understand why this happend, would be useful if anyone could explain it
I didn't find anything in documentation about this

React and rSocket REQUEST_CHANNEL error with Spring Boot

We have a working demo between React and Spring Boot Data Geode using rSocket for fire & forget, request response and request stream but when we try and use request channel we get error:
org.springframework.messaging.MessageDeliveryException: Destination 'quotes' does not support REQUEST_CHANNEL. Supported interaction(s): [REQUEST_STREAM]
So far on web it looks like this ought to be possible from RSocket Git
It's a simple spring boot app with #Controller endpoints that run over rSocket like this:
#Controller
public class RSocketController {
private static final Logger log = LogManager.getLogger(RSocketController.class);
#Autowired
PriceService priceService;
#MessageMapping(value = "quotes")
public Flux<Quote> getQuotes() {
log.info("In getQuotes");
return priceService.generatePrices();
}
}
The generatePrices returns a Flux of prices which works fine in request stream but we would prefer to use request channel for bi-directional comms.
Client versions
"rsocket-core": "0.0.19"
"rsocket-flowable": "0.0.14"
"rsocket-tcp-client": "0.0.19"
"rsocket-websocket-client": "0.0.19"
Client code
const transport = new RSocketWebSocketClient(transportOptions);
const rSocketClient = new RSocketClient({serializers, setup, transport});
​
rSocketClient.connect().subscribe({
onComplete: socket => {
console.log('Client connected to the RSocket Server');
​
socket.requestChannel(Flowable.just({
data: 'foyss',
metadata: String.fromCharCode(6) + 'quotes'
})).subscribe({
onComplete: function() {
console.log(`Channel received end of server stream`);
},
onError: function(err) {
console.log("err", err);
},
onNext: payload => {
console.log(payload);
},
onSubscribe: function(subscription) {
console.log("got subscription");
subscription.request(0x7fffffff);
},
onError: error => {
console.log(error);
},
onSubscribe: cancel => {
console.log('onSubscribe cancel');
}})
},
onError: error => {
console.log(error);
},
onSubscribe: cancel => {
// console.log(cancel);
console.log('onSubscribe cancel');
}
})
Some JS libraries still don't support the request-channel model. Please check the official documentation for your JS lib, eg, for: https://www.npmjs.com/package/ng-rsocket-rxjs
Missing:
Lease Handling
Server Setup
Resume Support
Request Channel
.....

Flutter websocket disconnect listening

In Flutter, I wanna listen to websocket disconnect event, how to achieve that?
The websocket connect will be drop when app goes to background, I still not found a method to let it continuesly running in background (does anyone have solution?), So I have to detect if a websocket connect is lost or something, so that I can re-connect when lost connection.
Pls help if anyone knows how to achieve that.
You can find out if websocket is closed by implementing onDone callback. See the example below:
_channel = IOWebSocketChannel.connect(
'ws://yourserver.com:port',
);
///
/// Start listening to new notifications / messages
///
_channel.stream.listen(
(dynamic message) {
debugPrint('message $message');
},
onDone: () {
debugPrint('ws channel closed');
},
onError: (error) {
debugPrint('ws error $error');
},
);
Hope that helps.
If your server closes the connection just use pinginterval like this
ws.pingInterval = const Duration(seconds: 5);
onDone should be called.
basic ping pong is enough.
Other answers around SO and the web suggest that you can't just keep sockets open in the background (which seems reasonable, you'd be keeping open network connections that may affect battery life). Depending on your use case, you might be better looking at Push Notifications or something that checks on a schedule.
How to keep iphone ios xmpp connection alive while in the background?
Websocket paused when android app goes to background
https://www.quora.com/How-do-I-keep-Socket-IO-running-in-the-background-on-iOS
WebSocketChannel channel = WebSocketChannel.connect(uri );
Stream stream = channel.stream;
stream.listen((event) {
print('Event from Stream: $event');
},onError: (e){
Future.delayed(Duration(seconds: 10)).then((value) {
connectAndListen();
},);
},
onDone: (() {
Future.delayed(Duration(seconds: 10)).then((value) {
connectAndListen();
},);
})
);
I recommend you to use this multiplatform websocket package https://pub.dev/packages/websocket_universal , there you can even track all WS events happening (and even built-in ping measurment if you need any):
import 'package:websocket_universal/websocket_universal.dart';
/// Example works with Postman Echo server
void main() async {
/// Postman echo ws server (you can use your own server URI)
/// 'wss://ws.postman-echo.com/raw'
/// For local server it could look like 'ws://127.0.0.1:42627/websocket'
const websocketConnectionUri = 'wss://ws.postman-echo.com/raw';
const textMessageToServer = 'Hello server!';
const connectionOptions = SocketConnectionOptions(
pingIntervalMs: 3000, // send Ping message every 3000 ms
timeoutConnectionMs: 4000, // connection fail timeout after 4000 ms
/// see ping/pong messages in [logEventStream] stream
skipPingMessages: false,
/// Set this attribute to `true` if do not need any ping/pong
/// messages and ping measurement. Default is `false`
pingRestrictionForce: false,
);
/// Example with simple text messages exchanges with server
/// (not recommended for applications)
/// [<String, String>] generic types mean that we receive [String] messages
/// after deserialization and send [String] messages to server.
final IMessageProcessor<String, String> textSocketProcessor =
SocketSimpleTextProcessor();
final textSocketHandler = IWebSocketHandler<String, String>.createClient(
websocketConnectionUri, // Postman echo ws server
textSocketProcessor,
connectionOptions: connectionOptions,
);
// Listening to webSocket status changes
textSocketHandler.socketHandlerStateStream.listen((stateEvent) {
// ignore: avoid_print
print('> status changed to ${stateEvent.status}');
});
// Listening to server responses:
textSocketHandler.incomingMessagesStream.listen((inMsg) {
// ignore: avoid_print
print('> webSocket got text message from server: "$inMsg" '
'[ping: ${textSocketHandler.pingDelayMs}]');
});
// Listening to debug events inside webSocket
textSocketHandler.logEventStream.listen((debugEvent) {
// ignore: avoid_print
print('> debug event: ${debugEvent.socketLogEventType}'
' [ping=${debugEvent.pingMs} ms]. Debug message=${debugEvent.message}');
});
// Listening to outgoing messages:
textSocketHandler.outgoingMessagesStream.listen((inMsg) {
// ignore: avoid_print
print('> webSocket sent text message to server: "$inMsg" '
'[ping: ${textSocketHandler.pingDelayMs}]');
});
// Connecting to server:
final isTextSocketConnected = await textSocketHandler.connect();
if (!isTextSocketConnected) {
// ignore: avoid_print
print('Connection to [$websocketConnectionUri] failed for some reason!');
return;
}
textSocketHandler.sendMessage(textMessageToServer);
await Future<void>.delayed(const Duration(seconds: 30));
// Disconnecting from server:
await textSocketHandler.disconnect('manual disconnect');
// Disposing webSocket:
textSocketHandler.close();
}

Resources