Suppose I have this simple Websocket handler for chat messages:
#Override
public Mono<Void> handle(WebSocketSession webSocketSession) {
webSocketSession
.receive()
.map(webSocketMessage -> webSocketMessage.getPayloadAsText())
.map(textMessage -> textMessageToGreeting(textMessage))
.doOnNext(greeting-> greetingPublisher.push(greeting))
.subscribe();
final Flux<WebSocketMessage> message = publisher
.map(greet -> processGreeting(webSocketSession, greet));
return webSocketSession.send(message);
}
What is needed to be done here in general as such it will use the rsocket protocol?
RSocket controller in the Spring WebFlux looks more like a RestController than WebSocketHandler. So the example above is simple like that:
#Controller
public class RSocketController {
#MessageMapping("say.hello")
public Mono<String> saHello(String name) {
return Mono.just("server says hello " + name);
}
}
and this is equivalent to requestResponse method.
If this answer doesn't satisfy you, please describe more what you want to achieve.
EDIT
If you want to broadcast messages to all clients, they need to subscribe to the same Flux.
public class GreetingPublisher {
final FluxProcessor processor;
final FluxSink sink;
public GreetingPublisher() {
this.processor = DirectProcessor.<String>create().serialize();
this.sink = processor.sink();
}
public void addGreetings(String greeting) {
this.sink.next(greeting);
}
public Flux<String> greetings() {
return processor;
}
}
#Controller
public class GreetingController{
final GreetingPublisher greetingPublisher = new GreetingPublisher();
#MessageMapping("greetings.add")
public void addGreetings(String name) {
greetingPublisher.addGreetings("Hello, " + name);
}
#MessageMapping("greetings")
public Flux<String> sayHello() {
return greetingPublisher.greetings();
}
}
Your clients have to call the greetings endpoint with the requestStream method. Wherever you send the message with the greetingPublisher.addGreetings() it's going to be broadcasted to all clients.
Related
I have a client side app ( ReatJS ) which instantiates a webSocket connection with the server ( Spring Boot ) and waits to receive data.
At a certain moment the server has to send data to a specific user on the WebSocket channel, the call to the function that starts the sending is inside a class annotated with #Service
How do I send a message using WebFlux via WebSocket to a specific user? I identify the user by id or by his connection-id
These are some classes
#Configuration
public class WebSocketConfig {
#Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>(){{
put("/data", new MyHandler());
}};
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
return mapping;
}
}
public class MyHandler implements WebSocketHandler {
#Override
public Mono<Void> handle(WebSocketSession webSocketSession) {
return webSocketSession.send( ... );
}
}
#Service
public class MyService {
public void sendToUser(String userId, String connectionId, String message) {
// send message to connected user through WebSocket
}
}
In SpringBoot I use RabbitTemplate and #RabbitListener to produce and receive Messages.
I have an abstract class to send and receive RabbitMQ message of a specific type and send those messages to a STOMP web socket.
I use one RabbitMQ topic exchange which is bind to one RabbitMQ queue. In the topic-exchange I send messages (Java objects) of 2 different types and I am using 2 #RabbitListener to consume these messages.
#Configuration
public class WsRabbitMqConfiguration {
public static final String WS_EXCHANGE_NAME = "websocket.replication";
#Getter
#Value(value = "${spring.application.name}")
private String routingKey;
#Bean
TopicExchange wsReplicationExchange() {
return new TopicExchange(
WS_EXCHANGE_NAME,
true,
false,
null);
}
#Bean
public Queue wsReplicationQueue() {
return new AnonymousQueue();
}
#Bean
public Binding bindingWsAttributeValueQueue(TopicExchange wsReplicationExchange,
Queue wsReplicationQueue) {
return BindingBuilder
.bind(wsReplicationQueue)
.to(wsReplicationExchange)
.with(routingKey);
}
}
#RequiredArgsConstructor
public abstract class AbstractWebsocketService<T> implements WebSocketService {
public static final String HEADER_WS_DESTINATION = "x-Websocket-Destination";
private final RabbitTemplate rabbitTemplate;
private final WsRabbitMqConfiguration wsRabbitMqConfiguration;
#Override
public final <T> void send(String destination, T payload) {
rabbitTemplate.convertAndSend(WsRabbitMqConfiguration.WS_EXCHANGE_NAME, wsRabbitMqConfiguration.getRoutingKey(), payload,
message -> putDestination(message, destination));
}
protected abstract void handleWebsocketSending(T payload, String destination);
#Service
public class FooWebSocketService extends AbstractWebsocketService<Foo> {
public FooWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Foo payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
#Service
public class BarWebSocketService extends AbstractWebsocketService<Bar> {
public BarWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Bar payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
From another service class I want to send RMQ messages, but randomly wrong #RabbitListener is activated.
Eg.
#Service
#RequiredArgsConstructor
public class TestService {
private final BarWebSocketService barWebSocketService;
public void sendMessage(Bar bar) {
// 1st message
barWebSocketService.send("bar-destination", bar);
// 2nd message
barWebSocketService.send("bar-destination2", bar);
}
}
For the 1st message #RabbitListener in FooWebSocketService is activated (which is wrong) and for the 2nd message #RabbitListener in BarWebSocketService (which is right).
Any suggestions what I am doing wrong? Thank you!
This question already has answers here:
Principal is null for every Spring websocket event
(2 answers)
Closed last month.
I was going over the basic Spring Boot WebSocket Tutorial: https://spring.io/guides/gs/messaging-stomp-websocket/
I decided to modify it to print out how many users are subscribed to a channel in the console but couldn't figure it out for hours. I've seen a few StackOverflow posts but they don't help. The last one I check was this: https://stackoverflow.com/a/51113021/11200149 which says to add try this:
#Autowired private SimpUserRegistry simpUserRegistry;
public Set<SimpUser> getUsers() {
return simpUserRegistry.getUsers();
}
So, I added the above to my controller, and here is the change:
#Controller
public class GreetingController {
#Autowired
private SimpUserRegistry userRegistry;
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Set<SimpUser> subscribedUsers = userRegistry.getUsers();
System.out.println("User amount: " + subscribedUsers.size()); // always prints: 0
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
This always prints 0:
System.out.println("User amount: " + subscribedUsers.size());
I'm coming from Socket.IO so maybe things work a bit differently because I've seen people implement their own manual Subscription Service classes. In socket.io this would be a piece of cake so I would assume Spring Boot would have this, but I just can't seem to find it.
Edit: This post does a great explanation for this problem.
Principal is null for every Spring websocket event
Maybe you can try to add custom HandshakeHandler class into registry and override the determineUser method to return the Principal object that containing subscriber name so that the SimpUserRegistry can work properly.
If you would like to see the effect, the below is what I'm trying.
app.js (sending out a user name through request parameter)
function connect() {
var socket = new SockJS('/gs-guide-websocket?name=' + $('#name').val());
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
custom class extends DefaultHandshakeHandler.class
#Component
public class WebSocketHandShakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
String name = httpServletRequest.getParameter("name");
return new MyPrincipal(name);
}
}
custom object implement Principal.class
#Data
#NoArgsConstructor
#AllArgsConstructor
public class MyPrincipal implements Principal {
private String name;
}
WebSocketConfig.class
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Autowired
private WebSocketHandShakeHandler webSocketHandShakeHandler;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket")
.setHandshakeHandler(webSocketHandShakeHandler)
.withSockJS();
}
}
Show all subscribers
#RestController
public class ApiController {
#Autowired
private SimpUserRegistry simpUserRegistry;
#GetMapping("/users")
public List<String> connectedEquipments() {
return this.simpUserRegistry
.getUsers()
.stream()
.map(SimpUser::getName).toList();
}
}
Result
By the way, you can check the DefaultSimpUserRegistry.class to observe the process of putting name into subscribers user map.
I have a simple controller which return name . I have websocket handler which return message to client as: Hey there, presentation recieved from user. whenever http://localhost:8080/sample is called, i need to display the above message to <ws://localhost:8080/presentation>, using https://websocketking.com/ to connect to websocket.
#RestController
public class WebController {
#RequestMapping("/sample")
public SampleResponse Sample(#RequestParam(value = "name",
defaultValue = "Robot") String name) {
SampleResponse response = new SampleResponse();
response.setId(1);
response.setMessage("Your name is "+name);
return response;
}
}
#Component
public class WebSocketHandler extends AbstractWebSocketHandler {
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
System.out.println("New Text Message Received from presetation");
String payload = message.getPayload();
System.out.println(payload);
session.sendMessage(new TextMessage("Hey there, presentation recieved from user"));
}
}
public class WebSocketConfiguration implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WebSocketHandler(), "/presentation").setAllowedOrigins("*");
}
}
I have a working example of TCPSocketClient using Spring Integration with hard coded host name and the port.
How to modify this example to accept the localhost and the port 5877 to be passed dynamically?
i.e. Is it possible to call the exchange method like ExchangeService.exchange(hostname, port, request) instead of ExchangeService.exchange(request)
If so how does those parameter be applied on to the client bean?
#Configuration
public class AppConfig {
#Bean
public IntegrationFlow client() {
return IntegrationFlows.from(ApiGateway.class).handle(
Tcp.outboundGateway(
Tcp.netClient("localhost", 5877)
.serializer(codec())
.deserializer(codec())
).remoteTimeout(10000)
)
.transform(Transformers.objectToString())
.get();
}
#Bean
public ByteArrayCrLfSerializer codec() {
ByteArrayCrLfSerializer crLfSerializer = new ByteArrayCrLfSerializer();
crLfSerializer.setMaxMessageSize(204800000);
return crLfSerializer;
}
#Bean
#DependsOn("client")
public ExchangeService exchangeService(ApiGateway apiGateway) {
return new ExchangeServiceImpl(apiGateway);
}
}
public interface ApiGateway {
String exchange(String out);
}
public interface ExchangeService {
public String exchange(String request);
}
#Service
public class ExchangeServiceImpl implements ExchangeService {
private ApiGateway apiGateway;
#Autowired
public ExchangeServiceImpl(ApiGateway apiGateway) {
this.apiGateway=apiGateway;
}
#Override
public String exchange(String request) {
String response = null;
try {
response = apiGateway.exchange(request);
} catch (Exception e) {
throw e;
}
return response;
}
}
For dynamic processing you may consider to use a Dynamic Flows feature from Spring Integration Java DSL: https://docs.spring.io/spring-integration/docs/current/reference/html/#java-dsl-runtime-flows
So, whenever you receive a request with those parameters, you create an IntegrationFlow on the fly and register it with the IntegrationFlowContext. Frankly, we have exactly sample in the docs for your TCP use-case:
IntegrationFlow flow = f -> f
.handle(Tcp.outboundGateway(Tcp.netClient("localhost", this.server1.getPort())
.serializer(TcpCodecs.crlf())
.deserializer(TcpCodecs.lengthHeader1())
.id("client1"))
.remoteTimeout(m -> 5000))
.transform(Transformers.objectToString());
IntegrationFlowRegistration theFlow = this.flowContext.registration(flow).register();