zyro23 / grails-spring-websocket

93 stars 28 forks source link

Feature request: ability to send to specific users not topics #51

Closed johnvlittle closed 6 years ago

johnvlittle commented 7 years ago

This plugin is great for doing complex stuff like topics, but there is also a need for the basic websocket features. e.g. with the ws plugin for node, you can just do the following code:

`

    wss.on("connection", myFunction(ws) {..}
    ws.on('message', function(message) {
            messageHandler(message, ws)
       })
    ws.on('error', function(er) {
            console.log(er)
        })
     ws.on('close', function() {
            console.log('Connection closed')
    })

`

It would be great if this plugin could also offer this simple functionality, i.e. to be able to handle messages from users (without any topics, publish and subscribe etc), and to be able to send messages back to a specific user (not to a topic or similar). Then we could write things like game servers which handle several players being in an instance of multi user game. Static topics etc have no user in this scenario.

The crux is this plugin is designed work similar to how web pages work where each message is handled by a controller as if it was a http request, and that controller sends back a reply synchronously. What we are looking for a is a way to have a service running permanently which accepts messages, and at specific time intervals, sends messages to specific users.

The crux is this plugin is designed work similar to how web pages work where each message is handled by a controller as if it was a http request, and that controller sends back a reply synchronously. What we are looking for a is a way to have a service running permanently which accepts simple web socket messages, and at the same time, at specific time intervals, sends web sockets messages to specific users.

zyro23 commented 7 years ago

you can of course just implement your own websocket handlers, ref. https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-handler

but the declarative annotations-based approach with stomp as protocol has been created for a reason, i.e. it simplifies things a lot.

and i would argue that the functionality you are asking is completely supported:

for receiving messages from a user you can just follow the readme examples. no user subscription required to receive messages from users on the server side.

using the controller layer to receive websocket messages and not directly in a service is intentional to conform to the common layering of an application. external requests/messages hit the controller layer and the those controllers use service beans to handle the logic.

imho, not using the pubsub model does not make life easier. if you just want to "send to the server" not differentiating between destinations, just use one destination for all messages.

to send messages to users, let your users subscribe to /user/queue/myQueue and then, e.g. in your service, you can of course send to specific users which is independent from an incoming request (user has to be subscribed to the destination, ofc - again quoting the readme):

class ExampleService {

    SimpMessagingTemplate brokerMessagingTemplate

    void hello() {
        brokerMessagingTemplate.convertAndSendToUser("myTargetUsername", "/queue/myQueue", "hello, target user!")
    }

}

if you want to do a programmatic lookup of which users are subscribed to which destinations - check SimpUserRegistry which you can inject into any spring bean as @Autowired SimpUserRegistry userRegistry or by name into a grails artefact like SimpUserRegistry userRegistry.

Or you can of course listen to the websocket lifecycle events directly, reacting to any user-related event (connect/subscribe/disconnect/etc.): https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events

spring 5 will bring reactive websocket support which enables a concise handler definition syntax that is a bit like the node.js examples you mentioned: http://docs.spring.io/spring/docs/5.0.0.RC2/spring-framework-reference/web.html#web-reactive-websocket-support

johnvlittle commented 7 years ago

Thanks Zyro, this is very helpful. for a multiplayer game server, the topic model doesn't fit well as you might have 1000 connected websockets, but you only want to send a message every tick (e.g. ever 2s) back to those people in that specific game (e.g. 4 people). If the topics could be dynamic, so the topic was the game ID or similar, it would help, but then it would be easy to spoof the wrong topic. I am trying your suggestion of using creating a websockethandler from spring, but I cant find how to resolve

import org.springframework.web.socket.config.annotation.WebSocketConfigurer

In the build.gradle I have tried all these:

dependencies { compile "org.springframework:spring-websocket:4.3.9.RELEASE" compile 'javax.websocket:javax.websocket-api:1.1' compileOnly "javax.servlet:javax.servlet-api"

Also, grails cannot resolve @Configuration nor @EnableWebSocket annotation nor EnableWebSocket etc.

e.g.

File Application.groovy

import org.springframework.web.socket.config.annotation.WebSocketConfigurer import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.context.annotation.Bean

@Configuration @EnableWebSocket class Application extends GrailsAutoConfiguration implements WebSocketConfigurer {

@Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(wsHandler(), "/ws"); }

@Bean public WebSocketHandler wsHandler() { return new TestSocket(); }

zyro23 commented 7 years ago

dynamic topic is no problem.

preventing spoofing of those topics (likely containing a username or an id or similar) is the job of websocket security. -> readme "security" -> "filtering messages". if you need logic to filter a subscription/message properly, you can reference any spring bean containing your filtering logic. e.g. for a subscription pattern /topic/myTopic-<userid>.foo:

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages
        .nullDestMatcher()
            .authenticated()
        .simpSubscribeDestMatchers("/topic/myTopic-*.foo")
            .access("""
                hasRole('MY_ROLE')
                && @myService.isTopicAllowed(message)
            """)
        .anyMessage()
            .denyAll()
}
boolean isTopicAllowed(Message<?> message) {
    def headers = StompHeaderAccessor.wrap message
    def matcher = headers.destination =~ "/topic/myTopic-(.*)\\.foo"
    def allowed = matcher[0][1] == securityService.currentUser.id.toString()
    return allowed
}
zyro23 commented 6 years ago

closing due to inactivity