How to broadcast a message using raw Spring 4 WebSockets without STOMP? - spring

In this great answer https://stackoverflow.com/a/27161986/4358405 there is an example of how to use raw Spring4 WebSockets without STOMP subprotocol (and without SockJS potentially).
Now my question is: how do I broadcast to all clients? I expected to see an API that I could use in similar fashion with that of pure JSR 356 websockets API: session.getBasicRemote().sendText(messJson);
Do I need to keep all WebSocketSession objects on my own and then call sendMessage() on each of them?

I found a solution. In the WebSocket handler, we manage a list of WebSocketSession and add new session on afterConnectionEstablished function.
private List<WebSocketSession> sessions = new ArrayList<>();
synchronized void addSession(WebSocketSession sess) {
this.sessions.add(sess);
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
addSession(session);
System.out.println("New Session: " + session.getId());
}
When we need to broadcast, just enumerate through all session in list sessions and send messages.
for (WebSocketSession sess : sessions) {
TextMessage msg = new TextMessage("Hello from " + session.getId() + "!");
sess.sendMessage(msg);
}
Hope this help!

As far as i know and can gather from the documentation here you can't broadcast using the WebSocketHandler.
Instead you should use Stomp over WebSocket configured by a WebSocketMessageBrokerConfigurer as described here.
Use a SimpMessagingTemplate anywhere in your code to send messages to subscribed clients as described here

Related

Camel url listener

I want to make a route that will be triggered from client request.
For example I have a route http://localhost:8080/get where I have some json object.
I want to create a route when I send a request to http://localhost:8080/get to send the data to ActiveMQ. Like event listener. I want to send to activeMq only when there is request to that URL.
I have searched that I cant use http or http4 in from() and that makes me a problem. I have tried from timer to url and then to activemq, but that is not what I really need.
This is what I have tried.
#GetMapping(value = "/shit")
public String getIt(#RequestParam(value = "url") String url, #RequestParam(value = "activemq") String activeMq) throws Exception {
CamelContext camelContext = new DefaultCamelContext();
RouteBuilder builder = new RouteBuilder() {
public void configure() {
from(url).convertBodyTo(String.class)
.process(exchange -> {
String body = exchange.getIn()
.getBody()
.toString();
System.out.println("The body is: " + body);
})
.pollEnrich()
.simple("activemq://" + activeMq);
}
};
builder.addRoutesToCamelContext(camelContext);
camelContext.start();
return "";
}
And I want the route to be active untill I stop it.
Yeah, camel-http4 is to produce only, it is not usable as a consumer because it is based an Apache HTTP client.
But you don't need special things like a timer or enricher. You can just use another Camel http-component that can act as a server. For example camel-jetty.
After a long discussion I finally realized that you would like to "branch off" requests within your other, already existing applications, i.e. you would like to send an incoming request, additionally to process them, to ActiveMQ.
Unfortunately you cannot do this from outside your applications because you do not know about incoming requests in other applications without modifying those other applications.
However, if you can modify your other applications so that they send their payloads to your new Camel application, the route would be quite simple:
from("jetty:http://localhost:[port]/yourApp")
.to("activemq:queue:myQueueName")
This route acts as a webserver for /yourApp
and sends the message body to a message queue of the configured ActiveMQ broker.

Server cannot respond to more requests for same session

I have the event stream code block below:
#RequestMapping(value = "/stream/{columnId}/data", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
#ResponseBody
public Flux<Activity> streamingData(#PathVariable String columnId, HttpSession httpSession) {
try {
ColumnObject columnObject = streamHelper.findColumnObjectInListById(columnId);
return streamHelper.getStreamData(httpSession.getId(), columnObject);
} catch (Exception e) {
...
}
}
After creating 6 columns through the endpoint, spring server will put all subsequent requests in the pending state.
( get, post methods for example)
#RequestMapping(value = "/session/metrics", method = RequestMethod.GET)
public ResponseEntity<?> keepSessionAliveMetrics(HttpSession httpSession) {
return new ResponseEntity<Void>(HttpStatus.OK); // STATE ONLY PENDING
}
As pointed out by Marten Deinum, this is a typical hard limit: browsers tend to limit the number of concurrent connections to a given domain.
If your application requires a lot of multiplexing, maybe using WebSockets is a better option as many message channels are established on a single TCP connection.
To bypass this limitation of the browsers, you can use Server-Sent Events over HTTP/2: HTTP/2 provides a multiplexing feature that enables to circumvent this HTTP/1 issue.
There is an excellent article about that: https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/
SpringBoot has an HTTP/2 support but may be not Spring WebFlux (with Netty, at least, at the time of writing): https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-http2
Using SSE over HTTP/2 is certainly the best way to overcome the limitations your are experiencing. You can find more detailed information here: https://www.infoq.com/articles/websocket-and-http2-coexist

Spring integration headers on message events

I have a simple TCP connection factory implemented in Spring Integration:
#Bean
#ServiceActivator(inputChannel = "toTcpChannel")
public TcpSendingMessageHandler tcpOutClient() throws Exception {
TcpSendingMessageHandler sender = new TcpSendingMessageHandler();
sender.setConnectionFactory(clientFactory());
sender.setClientMode(false);
sender.afterPropertiesSet();
return sender;
}
#Bean
public AbstractClientConnectionFactory clientFactory() {
final TcpNioClientConnectionFactory factory = new TcpNioClientConnectionFactory(tcpHost, tcpPort);
factory.setSingleUse(true);
return factory;
}
#EventListener
public void handleTcpConnectionOpenEvent(TcpConnectionOpenEvent event) throws Exception {
LOGGER.info("TCP connection OPEN event: {}", event.getConnectionId());
// HERE I would like to have "myCustomID" header here.
}
I am looking for getting the custom ID that I am providing via Gateway in the produced TcpConnectionOpenEvent (or similar via interceptors)
#Gateway(requestChannel="toTcpChannel")
public void sendToTcp(#Payload String message, #Header("myCustomID") Long myCustomID);
I know this is an event not a message but I do know how to get the Connection ID that I will receive in the input channel in any other way.
I am creating a type of hash map of my custom id – connection id.
I cannot use a custom correlation via aggregator because the response message will not contain any information about the previously sent message. Any suggestions will be welcome.
Oh! I see. Not sure what you are going to do from your custom TcpSendingMessageHandler, but as far as ApplicationEventPublisher is single-threaded, you can store the connectionId in the ThreadLocal variable and obtain it from there after send operation.

Spring send message to Websocket Message Broker

I want to send a message to websocket subscribers of a specific record - when an action takes place in one of my service class.
I'm trying to read the Spring Websocket documentation but it's kind of ambiguous to the point of how to get all these things working together.
Here are my setup files (this is extending jHipster btw):
WebsocketConfiguration.java
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/queue/", "/topic/", "/exchange/");
config.setApplicationDestinationPrefixes("/app");
config.setPathMatcher(new AntPathMatcher("."));
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
WebsocketSecurity.java
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// message types other than MESSAGE and SUBSCRIBE
.nullDestMatcher().authenticated()
// matches any destination that starts with /rooms/
.simpDestMatchers("/topic/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
.simpDestMatchers("/topic/**").authenticated()
// (i.e. cannot send messages directly to /topic/, /queue/)
// (i.e. cannot subscribe to /topic/messages/* to get messages sent to
// /topic/messages-user<id>)
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()
// catch all
.anyMessage().denyAll();
}
Controller class (attempt at implementing a simple broker I can test subscribing to from sockjs and recieving messages generated elsewhere in the application:
#MessageMapping("/ws")
#SendTo("/topic/sendactivity.{id}")
public void activity(#DestinationVariable string id, #Payload String message){
log.debug("Sending command center: "+message);
}
#RequestMapping(value = "/updateactivity", method = RequestMethod.PUT)
public ResponseEntity<Membership> updateMembership(
#RequestBody Membership membership) throws URISyntaxException {
// ...
String testString = "test";
messagingTemplate.convertAndSend("/topic/commandcenter"+membership.getId().toString(), testString);
// ...
}
When I put a breakpoint on the public void activity method, I don't get anything?
Sending a message to "/topic/commandcenterID" using the messaging template will send that message to the message broker, which will dispatch that message to clients subscribed to that topic. So it won't flow through your activity method.
When using #MessageMapping annotated methods, you're declaring those as application destinations. So sending a message to "/app/ws" should map to that method. Note that in that case I doubt it'll work since the destination variable you're expecting as a method argument is missing from the path definition in the #MessageMapping annotation.
Also, the #SendTo annotation in fact tells Spring that the value returned by the method should be converted to a message and sent to the given destination.
It seems you're mixing things up here, and I think you should:
read carefully the flow of messages in Spring STOMP support
look at a few example apps like the websocket portfolio and websocket chat

Building custom API over Spring Websockets

I'm have to implement custom API over Websockets that requires:
Custom WAMP-like subprotocol
Path parameters in socket URI
So I've following questions:
Is there any documentation or guides on implementing custom subprotocols in Spring? Protocol requires that exact version must be specified in the Sec-Websocket-Protocol field. Where this field could be read on server side?
What is a proper way to pass path parameters into a message handler? I could use ant patterns in handler registration
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(customHandler(), "/api/custom/{clientId}");
}
but those seems not available at TextWebSocketHandler. I'm solved this for now by extending default HttpSessionHandshakeInterceptor in a following way:
public class CustomHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
private static final UriTemplate URI_TEMPLATE = new UriTemplate("/api/custom/{clientId}");
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
Map<String, String> segments = URI_TEMPLATE.match(request.getURI().getPath());
attributes.put("CLIENTID", segments.get("clientId"));
return super.beforeHandshake(request, response, wsHandler, attributes);
}
}
and then accessing it in TextWebSocketHandler:
public class CustomHandler extends TextWebSocketHandler {
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
String clientId = session.getAttributes().get("CLIENTID");
...
session.sendMessage(response);
}
}
but this method, in my opinion, is a bit clunky. Is there more proper way to solve this?
Thanks.
The best advice I could give is to follow the example of the sub-protocol support that's built in -- starting with SubProtocolWebSocketHandler and the SubProtocolHandler's it delegates to including the StompSubProtocolHandler implementation. The SubProtocolWebSocketHandler is further connected to "clientInbound" and "clientOutbound" channels which are then used to form a processing flow as well as to provide thread boundaries.
There is a description for the processing flow for STOMP http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow which includes delegating messages to annotated controllers and/or to a message broker which can also send messages back downstream to clients.
Essentially the StompSubProtocolHandler translates to and from a WebSocketMessage and a Spring Message with protocol-specific content. So that controllers, message brokers, or any other consumer of the messages from the client inbound channel are decoupled and unaware from the WebSocket transport layer. Many of the facilities built around the building, sending, and processing of such sub-protocol messages are meant to be potentially usable for support of other STOMP-like protocols. That includes all the classes in the org.springframework.messaging.simp package.
As for URL path parameters, Spring doesn't provide anything at the WebSocket level which is mostly a transport layer. Most of the interesting stuff happens at the sub-protocol level. For example for STOMP a MessageMapping is supported based on the destination header along with a #DestinationVariable which is comparable to using #PathVariable in Spring MVC but based on the destination header, not the URL.

Resources