Netty ChannelHandlerContext not unique? - websocket

I have been using Netty for a while but mainly for using normal sockets when their channels always unique, thus I can map channels to know who are connecting to my server.
Now I have managed to implement http communication. The problem is that values of ChannelHandlerContext handlers (and either channels from those handlers) are not unique, I cannot detect who are connecting just by their handlers.
Questions:
Is that behaviour (ChannelHandlerContext handler values not
unique) normal or do I have some bugs in code?
Any idea, solution?
Many thanks
My ChannelInitializer looks like the following:
public class NettyHttpServerInitializer extends ChannelInitializer<SocketChannel> {
#Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http", new HttpServerCodec()));
pipeline.addLast("dechunker", new HttpObjectAggregator(65536));
pipeline.addLast("handler", new HttpServerHandler());
}
}
My server handler looks like (values of ctx and ctx.channel() are not unique even trigged from same client):
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
#Override
protected void messageReceived(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
...
}
}

When making a http protocol, a connection can be reused, that means that 1 connection can handle multiple requests. You should not the priciple of a connection is a person for your game, but you should use cookies or some sort of access token in your protocol.
Under normal circumstances, browsers will keep a maximum of 2 connections to the same ip.

Related

How to limit the number of stomp clients in Spring, subscribing to a specific topic, based on a condition?

I have been researching for a way to limit the number of clients who can subscribe to a specific stomp topic but have not yet understood, which could be the right approach according to my needs.
My use case is a game, which I am developing in Angular (ng2-stompjs stomp client) and Spring Boot Websockets (for the moment, the Spring in-memory message broker is in use).
The idea is that a user can be connected and subscribed to a "/lobby" stomp topic, and there he sees the opened game rooms, that could be in different statuses. for example, in-play or not started yet due to the low number of players joined.
I'd like to intercept and programmatically restrict a possible subscription of a client, to a specific "/room/{roomId}" topic, IF the MAX number of players has been reached, for example, 4. There could also be some simple client-side validation to restrict that, but I believe only client-side is not sufficient
So my main questions are:
How can a specific stomp topic subscription be intercepted in Spring?
Is it possible to return to the client-requestor some kind of error message that subscription could not be done?
I'd really appreciate your help, thank you in advance!
You could implement a StompEventListener which listens for subscriptions, in this we can have map mapping a destination(room number) versus the count of number of players in that particular room. if the count is already at max reject the subscription.
#Service
class StompEventListener() {
private Map<String, int> roomIdVsPlayerCount = new HashMap<>();
#EventListener
public void handleSubscription(SessionSubscribe event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
String destination = accessor.getDestination();
String roomId = destination.substring(...); //Parsed RoomID
if(roomIdVsPlayerCount.get(roomId) == MAX_ALLOWED_PLAYERS) {
//Throw exception which will terminate that client connection
or, send an error message like so:
simpMessagingTemplate.convertAndSend(<some_error_message>);
return;
}
//So it is not at maximum do further logic to actually subscribe
user and
roomIdVsPlayerCount.get(roomId) += 1;
}
#EventListener
public void handleUnsubscription(SessionUnsubscribe event) {
...
}
}
Useful References:
SessionSubscribeEvent (For handling the subscriptions)
ConvertAndSend. (For sending the error messages to client.)
EDIT
Please try sending the exception from a channel Interceptor since the above did not send the exception , so that it gets propagated to the client. The map we defined earlier can be defined as a bean in a separate class accessible(with #Autowired) to both event handler(for incrementing and decrementing) and TopicSubscriptionInterceptor(for validation).
#Component
class TopicSubscriptionInterceptor implements ChannelInterceptor {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel){
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
String destination = accessor.getDestination();
String roomId = destination.substring(...); //Parsed RoomID
if(roomIdVsPlayerCount.get(roomId) == MAX_ALLOWED_PLAYERS) {
//Throw exception which will terminate that client connection
}
//Since it is not at limit continue
}
}
Useful reference for implementing a TopicSubscriptionInterceptor: TopicSubscriptionInterceptor

How do I throttle the amount of data sent to Stomp queue (handling websockets) so that I can guarantee that I don't overflow the buffer?

I have two Java processes and I am connecting them using a websocket in spring boot. One process acts as the client and connects like this:
List<Transport> transports = new ArrayList<Transport>(1);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
WebSocketClient client = new SockJsClient(transports);
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
StompSessionHandler firstSessionHandler = new MyStompSessionHandler("Philip");
stompClient.connect("ws://localhost:8080/chat", firstSessionHandler);
The session handler extends StompSessionHandlerAdapter and provides these methods (I am subscribing by username so each client can receive its own messages):
#Override
public void afterConnected(
StompSession session, StompHeaders connectedHeaders) {
session.subscribe("/user/" + userName + "/reply", this);
session.send("/app/chat", getSampleMessage());
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
Message msg = (Message) payload;
// etc.....
}
On the server side I have a Controller exposed and I am writing data by calling the endpoint from a worker thread.
#Autowired
private SimpMessagingTemplate template;
#MessageMapping("/chat")
public void send(
Message message)
throws Exception {
template.convertAndSendToUser(message.getFrom(),
"/reply",
message);
}
In the websocket config I am overriding the method to set the limits:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/user");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(500 * 1024);
registration.setSendBufferSizeLimit(1024 * 1024);
registration.setSendTimeLimit(20000);
}
My question is this, if the load on the server gets high enough and I overrun the limit, the websocket fails catastrophically, and I want to avoid this. What I would like to do is for the controller to have the ability to ask the message broker "will this message fit in the buffer?", so that I can throttle to stay under the limit. I searched the API documentation but I don't see any way of doing that. Are there any other obvious solutions that I am missing?
Thanks.
Actually I found a solution, so if anyone is interested, here it is.
On the server side configuration of the websockets I installed an Interceptor on the Outbound Channel (this is part of the API), which is called after each send from the embedded broker.
So I know how much is coming in, which I keep track of in my Controller class and I know how much is going out through the Interceptor that I installed, and this allows me to always stay under the limit.
The controller, before accepting any new messages to be queued up for the broker first determines if enough room is available and if not queues up the message in external storage until such time as room becomes available.

Spring creating dashboard using web sockets for live updates

The front page of my web app is a dashboard, where I pull out information such as:
Latest entered clients, 5 top male clients, and female and also some calculated statistics.
The approach I went with first, was creating endpoints in my controller that would calculate such things and return a JSON file with the results, and then make an Ajax call and put the data in the html file. But I was told the best way to do this, would be to use Web Sockets, considering that when another users puts a client in, simultaniously he needs to show up on the dashboard.
Following a tutorial what I have so far is this :
public class MyMessageHandler extends TextWebSocketHandler {
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// The WebSocket has been closed
}
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// The WebSocket has been opened
// I might save this session object so that I can send messages to it outside of this method
// Let's send the first message
session.sendMessage(new TextMessage("You are now connected to the server. This is the first message."));
}
#Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception {
// A message has been received
System.out.println("Message received: " + textMessage.getPayload());
}
}
And the websocket endpoint :
#Configuration
#EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {
#Bean
public WebSocketHandler myMessageHandler() {
return new MyMessageHandler();
}
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myMessageHandler(), "/my-websocket-endpoint");
}
}
But this tutorial shows how a message will be sent-received real time.
My controller has and endpoint like this:
#RequestMapping(value="getDashboard/{gender}",method=RequestMethod.GET)
public #ResponseBody List<Client> getTopClients(#PathVariable("gender")
char gender) {
return clientService.findTop5ByGenderOrderByResult_DataResults_ScoreDesc(gender);
}
What I'm trying to understand is, where do this service so that it is called as long as the connection is open, so if there's any update, it updates the dashboard real time.
I guess what I'm trying to understand is the skeleton, of what this would look like using web sockets.
Just an overview of its logic/example.
Any help is appreciated and I apologize as I'm a beginner, I dont fully understand how to switch from what I had done before, and I'm trying to learn best practices.

Disconnect client session from Spring websocket stomp server

I've searched quite a bit and been unable to find this: Is there a way that a spring websocket stomp server can disconnect a client based on the sessionId (or really based on anything at all)?
It seems to me that once a client connects to a server there is nothing that allows the server to disconnect the client.
Actually using some workarounds you can achieve what you want.
For that you should do:
Use java configuration (not sure if it is possible with XML config)
Extend your config class from WebSocketMessageBrokerConfigurationSupport and implement WebSocketMessageBrokerConfigurer interface
Create custom sub-protocol websocket handler and extend it from SubProtocolWebSocketHandler class
In your custom sub-protocol websocket handler override afterConnectionEstablished method and you will have access to WebSocketSession :)
I've created sample spring-boot project to show how we can disconnect client session from server side:
https://github.com/isaranchuk/spring-websocket-disconnect
You can also disconnect session by implementing a custom WebSocketHandlerDecorator:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S> {
#Override
public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
#Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
#Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
session.close(CloseStatus.NOT_ACCEPTABLE);
super.afterConnectionEstablished(session);
}
};
}
});
super.configureWebSocketTransport(registration);
}
#Override
protected void configureStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/home")
.setHandshakeHandler(new DefaultHandshakeHandler(
new UndertowRequestUpgradeStrategy() // If you use undertow
// new JettyRequestUpgradeStrategy()
// new TomcatRequestUpgradeStrategy()
))
.withSockJS();
}
}
As far as I know the API doesn't provide what you are looking for, on server-side you can only detect disconnect events. If you want to disconnect a certain client I think you must go for a litte workaround, e.g. this one:
Write a client-side javascript function that is able to trigger a disconnect
As soon as your client is connected to the server, generate a client ID in your javascript and send it to the server. Remember the ID on the client, you'll need it in step (4).
At the time you want the server to disconnect the connection to the specific client (identified by the ID), send a message containing the ID back to the client.
Now your client javascript evaluates the message send from the server and decides to call the disconnect function you wrote in step (1).
Your client disconnects itself.
The workaround is a bit cumbersome but it'll work.
I relied on the idea of #Dániel Kis and implemented the websocket session management with the key point of storing websocket sessions for authenticated users in Singleton-like object.
// WebSocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
#Override
public WebSocketHandler decorate(final WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
#Override
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
// We will store current user's session into WebsocketSessionHolder after connection is established
String username = session.getPrincipal().getName();
WebsocketSessionHolder.addSession(username, session);
super.afterConnectionEstablished(session);
}
};
}
});
}
}
Class to store websocket users' sessions WebsocketSessionHolder. I use 'synchronized' blocks for thread safety. Actually this blocks are not expensive operations because each of methods (addSession and closeSessions) are used not so often (On establishing and terminating connection). No need to use ConcurrentHashMap or SynchronizedMap here because we perform bunch of operations with the list in these methods.
// WebsocketSessionHolder.java
public class WebsocketSessionHolder {
static {
sessions = new HashMap<>();
}
// key - username, value - List of user's sessions
private static Map<String, List<WebSocketSession>> sessions;
public static void addSession(String username, WebSocketSession session)
{
synchronized (sessions) {
var userSessions = sessions.get(username);
if (userSessions == null)
userSessions = new ArrayList<WebSocketSession>();
userSessions.add(session);
sessions.put(username, userSessions);
}
}
public static void closeSessions(String username) throws IOException
{
synchronized (sessions) {
var userSessions = sessions.get(username);
if (userSessions != null)
{
for(var session : userSessions) {
// I use POLICY_VIOLATION to indicate reason of disconnecting for a client
session.close(CloseStatus.POLICY_VIOLATION);
}
sessions.remove(username);
}
}
}
}
And the final touch - terminating (disconnecting) specified user websocket sessions ("ADMIN" in the example), say in some Controller
//PageController.java
#Controller
public class PageController {
#GetMapping("/kill-sessions")
public void killSessions() throws Exception {
WebsocketSessionHolder.closeSessions("ADMIN");
}
}
In case of xml configuration you can use <websocket:decorator-factories> in the <websocket:transport> of your <websocket:message-broker>.
Create custom WebSocketHandlerDecorator and WebSocketHandlerDecoratorFactory which implement decorate method.
This may seem brief but I am not certain what the implementation would look like in your case. But, I think there are some circumstances that would warrant this workaround/solution:
Set a timeout on the back-end (say 30 seconds):
This is how you would do it with Spring Boot Websocket (and Tomcat):
#Bean
public ServletServerContainerFactoryBean websocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxSessionIdleTimeout(MAX_SESSION_IDLE_TIMEOUT);
return container;
}
If you want to keep the session open - continue to send messages or else actively send ping/pongs. In the case that you want the session to disconnect, stop the ping/pong interaction somewhere suitable in you application.
Of course, if you are wanting to disconnect immediately, this doesn't seem to be an appropriate solution. But if you are simply trying to reduce the number of active connections, ping/pong may be a good fit since it keeps a session open only so long as messages are actively being sent, preventing the session from being closed prematurely.
first you have to introduce a class as your User class by inheritance then use it like this:
if (userObject instanceof User) {
User user = (User) userObject;
if (user.getId().equals(userDTO.getId())) {
for (SessionInformation information : sessionRegistry.getAllSessions(user, true)) {
information.expireNow();
}
}
}

How can I send a message on connect event (SockJS, STOMP, Spring)?

I am connection through SockJS over STOMP to my Spring backend. Everything work fine, the configuration works well for all browsers etc. However, I cannot find a way to send an initial message. The scenario would be as follows:
The client connects to the topic
function connect() {
var socket = new SockJS('http://localhost:8080/myEndpoint');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/notify', function(message){
showMessage(JSON.parse(message.body).content);
});
});
}
and the backend config looks more or less like this:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketAppConfig extends AbstractWebSocketMessageBrokerConfigurer {
...
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/myEndpoint").withSockJS();
}
I want to send to the client an automatic reply from the backend (on the connection event) so that I can already provide him with some dataset (e.g. read sth from the db) without the need for him (the client) to send a GET request (or any other). So to sum up, I just want to send him a message on the topic with the SimMessagingTemplate object just after he connected.
Usually I do it the following way, e.g. in a REST controller, when the template is already autowired:
#Autowired
private SimpMessagingTemplate template;
...
template.convertAndSend(TOPIC, new Message("it works!"));
How to achieve this on connect event?
UPDATE
I have managed to make it work. However, I am still a bit confused with the configuration. I will show here 2 configurations how the initial message can be sent:
1) First solution
JS part
stompClient.subscribe('/app/pending', function(message){
showMessage(JSON.parse(message.body).content);
});
stompClient.subscribe('/topic/incoming', function(message){
showMessage(JSON.parse(message.body).content);
});
Java part
#Controller
public class WebSocketBusController {
#SubscribeMapping("/pending")
Configuration
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
... and other calls
template.convertAndSend("/topic/incoming", outgoingMessage);
2) Second solution
JS part
stompClient.subscribe('/topic/incoming', function(message){
showMessage(JSON.parse(message.body).content);
})
Java part
#Controller
public class WebSocketBusController {
#SubscribeMapping("/topic/incoming")
Configuration
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
// NO APPLICATION PREFIX HERE
}
... and other calls
template.convertAndSend("/topic/incoming", outgoingMessage);
SUMMARY:
The first case uses two subscriptions - this I wanted to avoid and thought this can be managed with one only.
The second one however has no prefix for application. But at least I can have a single subscription to listen on the provided topic as well as send initial message.
If you just want to send a message to the client upon connection, use an appropriate ApplicationListener:
#Component
public class StompConnectedEvent implements ApplicationListener<SessionConnectedEvent> {
private static final Logger log = Logger.getLogger(StompConnectedEvent.class);
#Autowired
private Controller controller;
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
log.debug("Client connected.");
// you can use a controller to send your msg here
}
}
You can't do that on connect, however the #SubscribeMapping does the stuff in that case.
You just need to mark the service method with that annotation and it returns a result to the subscribe function.
From Spring Reference Manual:
An #SubscribeMapping annotation can also be used to map subscription requests to #Controller methods. It is supported on the method level, but can also be combined with a type level #MessageMapping annotation that expresses shared mappings across all message handling methods within the same controller.
By default the return value from an #SubscribeMapping method is sent as a message directly back to the connected client and does not pass through the broker. This is useful for implementing request-reply message interactions; for example, to fetch application data when the application UI is being initialized. Or alternatively an #SubscribeMapping method can be annotated with #SendTo in which case the resulting message is sent to the "brokerChannel" using the specified target destination.
UPDATE
Referring to this example: https://github.com/revelfire/spring4Test how would that be possible to send anything when the line 24 of the index.html is invoked: stompClient.subscribe('/user/queue/socket/responses' ... from the spring controllers?
Well, look like this:
#SubscribeMapping("/queue/socket/responses")
public List<Employee> list() {
return getEmployees();
}
The Stomp client part remains the same.

Resources