{#========================================== Docs : "WebSockets" ==========================================#}

WebSockets

WebSockets allow you to establish a permanent connection between your application and your users. Doing so, you can receive messages from them, but you can also send messages to them, at any time. This is very different than standard HTTP which is: one request by the user => one response by the application.

WebSockets are mostly used when...

WebSockets's terminology is quite simple: an endpoint is a group of peers (users) connected together and that your application manages. A WebSocket endpoint can receive and send text messages and binary messages from and to the peers.

Your application can manage multiple endpoints, each of them with its own set of peers. Grouping peers into separate endpoints can be useful so you can easily send a specific message to a specific group of peers only. Also, each endpoint may have some different level of security associated with it: some users may be allowed to connect to some endpoints, but not to some others.

{#========================================== Quick Example ==========================================#}

Quick Example

Here's a quick example on how to use WebSockets. Each part of this example will be explained in more details in following sections. You can try this example live on the WebSockets demo page.

The source code for this example is:

First, we define a WebSocket route:

router.websocket("/chat").save(chatWebsocketController);

The "chatWebsocketController" is an instance of a class that implements the IWebsocketController interface. This component is responsible for handling all the WebSocket events:

public class ChatWebsocketController 
        implements IWebsocketController<IDefaultRequestContext, IDefaultWebsocketContext> {

    private IWebsocketEndpointManager endpointManager;

    protected IWebsocketEndpointManager getEndpointManager() {
        return this.endpointManager;
    }

    @Override
    public IWebsocketConnectionConfig onPeerPreConnect(IDefaultRequestContext context) {
        
        return new IWebsocketConnectionConfig() {

            @Override
            public String getEndpointId() {
                return "chatEndpoint";
            }

            @Override
            public String getPeerId() {
                return "peer_" + UUID.randomUUID().toString();
            }
        };
    }

    @Override
    public void onEndpointReady(IWebsocketEndpointManager endpointManager) {
        this.endpointManager = endpointManager;
    }
    
    @Override
    public void onPeerConnected(IDefaultWebsocketContext context) {
        context.sendMessageToCurrentPeer("Your peer id is " + context.getPeerId());
    }
    
    @Override
    public void onPeerMessage(IDefaultWebsocketContext context, String message) {
        getEndpointManager().sendMessage("Peer '" + context.getPeerId() + 
                "' sent a message: " + message);
    }

    @Override
    public void onPeerMessage(IDefaultWebsocketContext context, byte[] message) {
    }

    @Override
    public void onPeerClosed(IDefaultWebsocketContext context) {
    }

    @Override
    public void onEndpointClosed(String endpointId) {
    }
}

Explanation :

Here's a quick client-side HTML/javascript code example, for a user to connect to this endpoint:

<script>
    var app = app || {};
    
    app.showcaseInit = function() {
        
        if(!window.WebSocket) {
            alert("Your browser does not support WebSockets.");
            return;
        }

        app.showcaseWebsocket = new WebSocket("ws://" + location.host + "/chat");
        
        app.showcaseWebsocket.onopen = function(event) {
            console.log("WebSocket connection established!"); 
        };
        
        app.showcaseWebsocket.onclose = function(event) {
            console.log("WebSocket connection closed."); 
        };
        
        app.showcaseWebsocket.onmessage = function(event) {
            console.log(event.data); 
        };   
    };
    
    app.sendWebsocketMessage = function sendWebsocketMessage(message) {
        
        if(!window.WebSocket) {
            return;
        }
        if(app.showcaseWebsocket.readyState != WebSocket.OPEN) {
            console.log("The WebSocket connection is not open."); 
            return;
        }
        
        app.showcaseWebsocket.send(message);
    };
    
    app.showcaseInit();
    
</script>

<form onsubmit="return false;">
    <input type="text" name="message" value="hi!"/>
    <input type="button" value="send" 
           onclick="app.sendWebsocketMessage(this.form.message.value)"/>
</form>

{#========================================== WebSocket Routing ==========================================#}

WebSocket Routing

The WebSocket routes are defined similarly to regular routes, using Spincast's router (interface IRouter). But, instead of beginning the creation of the route with the HTTP method, like GET(...) or POST(...), you use websocket(...):

router.websocket("/chat") ...

There are fewer options available when creating a WebSocket route compared to a regular HTTP route. Here are the available ones...

You can set an id for the route. This allows you to identify the route so you can refer to it later on, delete it, etc:

router.websocket("/chat")
      .id("chat-endpoint") ...

You can also add "before" filters, inline. Note that you can not add "after" filters to a WebSocket route because, as soon as the WebSocket connection is established, the HTTP request is over. But "before" filters are perfectly fine since they applied to the HTTP request before it is upgraded to a WebSocket connection. For the same reason, global "before" filters (defined using router.before(...)) will be applied during a WebSocket route processing, but not the global "after" filters (defined using router.after(...)).

Here's an example of inline "before" filters, on a WebSocket route:

router.websocket("/chat")
      .id("chat-endpoint")
      .before(beforeFilter1) 
      .before(beforeFilter2) ...

Finally, like you do during the creating of a regular route, you save the WebSocket route. The save(...) method for a WebSocket route takes a WebSocket controller, not a route handler as regular HTTP routes do.

router.websocket("/chat")
      .id("chat-endpoint")
      .before(beforeFilter1) 
      .before(beforeFilter2)
      .save(chatWebsocketController);

{#========================================== WebSocket Controllers ==========================================#}

WebSocket Controllers

WebSocket routes require a dedicated controller as a handler. This controller is responsible to received the various WebSocket events occurring during the connection.

You create a WebSocket controller by implementing the IWebsocketController interface.

The WebSocket events

Here are the methods a WebSocket controller must implement, each of them associated with a specific WebSocket event:

The onPeerPreConnect(...) event

The onPeerPreConnect(...) is called before the WebSocket connection is actually established with the user. The request, here, is still the original HTTP one, so you receive a request context as regular route handlers do.

In that method, you have access to the user's cookies and to all the information about the initial HTTP request. This is a perfect place to decide if the requesting user should be allowed to connect to a WebSocket endpoint or not. You may check if it is authenticated, if he has enough rights, etc.

If you return null from this method, the WebSocket connection process will be cancelled, and you are responsible for sending a response that makes sense to the user.

For example:

public IWebsocketConnectionConfig onPeerPreConnect(IDefaultRequestContext context) {

    ICookie sessionIdCookie = context.cookies().getCookie("sessionId");
    if(sessionIdCookie == null || !canUserAccessWebsocketEndpoint(sessionIdCookie.getValue())) {
        context.response().setStatusCode(HttpStatus.SC_FORBIDDEN);
        return null;
    }

    return new IWebsocketConnectionConfig() {

        @Override
        public String getEndpointId() {
            return "someEndpoint";
        }

        @Override
        public String getPeerId() {
            return "peer_" + encrypt(sessionIdCookie.getValue());
        }
    };
}

Explanation :

The IWebsocketConnectionConfig(...) object

Once you decided that a user can connect to a WebSocket endpoint, you return an instance of IWebsocketConnectionConfig from the onPeerPreConnect(...) method.

In this object, you have to specify two things:

Multiple endpoints

Note that a single WebSocket controller can manage multiple endpoints. The endpoints are not hardcoded when the application starts, you dynamically create them, on demand. Simply by connecting a first peer using a new endpoint id, you create the required endpoint. This allows your controller to "group" some peers together, for any reason you may find useful. For example, you may have a chat application with multiple "rooms": each room would be a specific endpoint, with a set of peers connected to it.

If the endpoint id you return in the IWebsocketConnectionConfig object is the one of an existing endpoint, the user will be connected to it. Next time you send a message using the associated manager, this new peer will receive it.

If your controller creates more than one endpoint, you have to keep the managers for each of those endpoints!

For example:

public class MyWebsocketController 
        implements IWebsocketController<IDefaultRequestContext, IDefaultWebsocketContext> {

    private final Map<String, IWebsocketEndpointManager> 
            endpointManagers = new HashMap<String, IWebsocketEndpointManager>();

    protected Map<String, IWebsocketEndpointManager> getEndpointManagers() {
        return this.endpointManagers;
    }

    protected IWebsocketEndpointManager getEndpointManager(String endpointId) {
        return getEndpointManagers().get(endpointId);
    }

    @Override
    public IWebsocketConnectionConfig onPeerPreConnect(IDefaultRequestContext context) {
        
        return new IWebsocketConnectionConfig() {

            @Override
            public String getEndpointId() {
                return "endpoint_" + RandomUtils.nextInt(1, 11);
            }

            @Override
            public String getPeerId() {
                return null;
            }
        };
    }

    @Override
    public void onEndpointReady(IWebsocketEndpointManager endpointManager) {
        getEndpointManagers().put(endpointManager.getEndpointId(), endpointManager);
    }

    @Override
    public void onPeerMessage(IDefaultWebsocketContext context, String message) {
        getEndpointManager(context.getEndpointId()).sendMessage(message);
    }
    
    @Override
    public void onEndpointClosed(String endpointId) {
        getEndpointManagers().remove(endpointId);
    }

    @Override
    public void onPeerConnected(IDefaultWebsocketContext context) {
    }

    @Override
    public void onPeerMessage(IDefaultWebsocketContext context, byte[] message) {
    }

    @Override
    public void onPeerClosed(IDefaultWebsocketContext context) {
    }
}

Explanation :

Finally, note that a controller can manage multiple WebSocket endpoints, but only one controller can create and manage a given WebSocket endpoint! If a controller tries to connect a peer to an endpoint that is already managed by another controller, an exception is thrown.

The onEndpointReady(...) method should not block

It's important to know that the onEndpointReady(...) method is called synchronously by Spincast, when the connection with the very first peer is being established. This means that this method should not block or the connection with the first peer will never succeed!

Spincast calls onEndpointReady(...) synchronously to make sure you have access to the endpoint manager before the first peer is connected and therefore before you start receiving events from him.

You may be tempted to start some kind of loop in this onEndpointReady(...) method, to send messages to the connected peers, at some interval. Instead, start a new Thread to run the loop, and let the current thread continue.

For example, this example will send the current time to all peers connected to the endpoint, every second. It does so without blocking the onEndpointReady(...) method, which is the correct way to do it:

public void onEndpointReady(IWebsocketEndpointManager endpointManager) {

    getEndpointManagers().put(endpointManager.getEndpointId(), endpointManager);

    final String endpointId = endpointManager.getEndpointId();

    Thread sendMessageThread = new Thread(new Runnable() {

        @Override
        public void run() {

            while(true) {
            
                IWebsocketEndpointManager manager = getEndpointManager(endpointId);
                if(manager == null) {
                    break;
                }

                manager.sendMessage("Time: " + new Date().toString());

                try {
                    Thread.sleep(1000);
                } catch(InterruptedException e) {
                    break;
                }
            }
        }
    });
    sendMessageThread.start();
    
}

Automatic pings and other configurations

By default, pings are automatically sent to each peer every 20 seconds or so. This validates that the peers are still connected. When those pings find that a connection has been closed, onPeerClosed(...) is called on the WebSocket controller.

You can turn on/off those automatic pings and change other configurations, depending on the server implementation you use. Here are the configurations available when using the default implementation.

{#========================================== WebSocket context ==========================================#}

The WebSocket context

Most methods of a WebSocket controller receive a WebSocket context. This context object is similar to a request context received by a regular route handler: it gives access to information about the event (the endpoint, the peer, etc.) and also provides easy access to utility methods and add-ons.

WebSocket specific methods:

Utility methods and add-ons:

{#========================================== Extending the WebSocket context ==========================================#}

Extending the WebSocket context

The same way you can extend the request context type, which is the object passed to your route handlers for regular HTTP requests, you can also extend the WebSocket context type, passed to your WebSocket controller, when an event occurs.

First, make sure you read the Extending the request context section: it contains more details and the process of extending the WebSocket context is very similar!

The first thing to do is to create a custom interface for the new WebSocket context type:

public interface IAppWebsocketContext extends IWebsocketContext<IAppWebsocketContext> {
    public void customMethod(String message);
}

Explanation :

Then, we provide an implementation for that custom interface:

public class AppWebsocketContext extends WebsocketContextBase<IAppWebsocketContext>
                                        implements IAppWebsocketContext {

    @AssistedInject
    public AppWebsocketContext(@Assisted("endpointId") String endpointId,
                               @Assisted("peerId") String peerId,
                               @Assisted IWebsocketPeerManager peerManager,
                               WebsocketContextBaseDeps<IAppWebsocketContext> deps) {
        super(endpointId,
              peerId,
              peerManager,
              deps);
    }

    @Override
    public void customMethod(String message) {
        sendMessageToCurrentPeer("customMethod: " + message);
    }
}

Explanation :

Finally, you must let Spincast know about your custom WebSocket context type. You do this by overriding the getWebsocketContextImplementationClass() method defined in Spincast's SpincastCoreGuiceModule Guice module. For example, in a custom AppModule:

@Override
protected Class<? extends IWebsocketContext<?>> getWebsocketContextImplementationClass() {
    return AppWebsocketContext.class;
}

If you both extended the request context type and the WebSocket context type, the parameterized version of your router would look like: IRouter<IAppRequestContext, IAppWebsocketContext>.

But you could also create an unparameterized version of it, for easier usage:

public interface IAppRouter extends IRouter<IAppRequestContext, IAppWebsocketContext> {
    // nothing required
}

Note that if you use the Quick Start to start your application, both the request context type and the WebSocket context type have already been extended and the unparameterized routing components have already been created for you!