templates.docs.websockets.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{#==========================================
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...
-
You want your application to be able to push messages to the connected
users, without waiting for them to make requests.
-
You need your application to be the central point where multiple users can share real-time data. The classic example
is a chat room: when a user sends a message, your application echoes that message back to the other Peers.
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:
-
WebSocket controller: WebsocketsDemoEchoAllController.java
-
HTML page: websockets.html
First, we define a WebSocket Route
:
router.websocket("/chat").handle(chatWebsocketController);
The "chatWebsocketController" is an instance of a class that implements the WebsocketController
interface. This component is responsible for handling all the WebSocket events:
public class ChatWebsocketController
implements WebsocketController<DefaultRequestContext, DefaultWebsocketContext> {
private WebsocketEndpointManager endpointManager;
protected WebsocketEndpointManager getEndpointManager() {
return this.endpointManager;
}
@Override
public WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {
return new WebsocketConnectionConfig() {
@Override
public String getEndpointId() {
return "chatEndpoint";
}
@Override
public String getPeerId() {
return "peer_" + UUID.randomUUID().toString();
}
};
}
@Override
public void onEndpointReady(WebsocketEndpointManager endpointManager) {
this.endpointManager = endpointManager;
}
@Override
public void onPeerConnected(DefaultWebsocketContext context) {
context.sendMessageToCurrentPeer("Your peer id is " + context.getPeerId());
}
@Override
public void onPeerMessage(DefaultWebsocketContext context, String message) {
getEndpointManager().sendMessage("Peer '" + context.getPeerId() +
"' sent a message: " + message);
}
@Override
public void onPeerMessage(DefaultWebsocketContext context, byte[] message) {
}
@Override
public void onPeerClosed(DefaultWebsocketContext context) {
}
@Override
public void onEndpointClosed(String endpointId) {
}
}
Explanation :
-
10-25 : Without going into too many details (we will do
that in the following sections),
onPeerPreConnect(...)
is a method called
before a new user is connected. In this example, we specify that this user should
connect to the "chatEndpoint" Endpoint
and that
its Peer id
will be "peer_" followed by a random String.
-
27-30 : When a WebSocket Endpoint is ready to receive and send
messages, the onEndpointReady(...) method is called and gives us access
to an
Endpoint Manager
.
We keep a reference to this manager since we are going to use it to send messages.
-
32-35 : When the connection with a new Peer is established,
the
onPeerConnected(...)
method is called. In this example, as soon as the Peer is connected,
we send him a message containing his Peer id.
-
37-40 : When a Peer sends a message,
the onPeerMessage(...) method is called.
In this example, we use the
Endpoint Manager
(which was received in the onEndpointReady(...)
method [27-30])
and we broadcast this message to all the Peers of the Endpoint.
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;
}
// Use "ws://" instead of "wss://" for an insecure
// connection, without SSL.
app.showcaseWebsocket = new WebSocket("wss://" + 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 WebSocketRroutes are defined similarly to regular Routes, using
Spincast's Router. 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 something like
router.ALL(...).pos(-10)
) will be applied during a
WebSocket Route processing, but not the global "after" Filters
(defined using
a position greater than "0").
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
handle(...)
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)
.handle(chatWebsocketController);
{#==========================================
WebSocket Controllers
==========================================#}
WebSocket Controllers
WebSocket Routes require a dedicated Controller as an handler. This Controller
is responsible for receiving the various WebSocket events occurring during the
connection.
You create a WebSocket Controller by implementing the
WebsocketController
interface.
The WebSocket events
Here are the methods a WebSocket Controller must implement, each of them associated with a specific WebSocket event
:
-
WebsocketConnectionConfig onPeerPreConnect(R context)
Called when a user requests a WebSocket connection. At this moment, the connection is not
yet established and you can allow or deny the request. You can also decide on which Endpoint
to connect the user to, and which Peer id
to assign him.
-
void onEndpointReady(WebsocketEndpointManager endpointManager)
Called when a new Endpoint is created within your application. The Endpoint Manager
is passed
as a parameter on your should keep a reference to it. You'll use this Manager to send messages, to close
the connection with some Peers, etc.
Note that this method should not block! More details below...
-
void onPeerConnected(W context)
Called when a new Peer is connected. At this point, the WebSocket connection is established with the
Peer and you can send him messages.
-
void onPeerMessage(W context, String message)
Called when a Peer sends a text message.
-
void onPeerMessage(W context, byte[] message)
Called when a Peer sends a binary message.
-
void onPeerClosed(W context)
Called when the connection with a Peer is closed.
-
void onEndpointClosed(String endpointId)
Called when the whole Endpoint is closed.
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 he 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 WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {
String sessionId = context.request().getCookie("sessionId");
if(sessionId == null || !canUserAccessWebsocketEndpoint(sessionId)) {
context.response().setStatusCode(HttpStatus.SC_FORBIDDEN);
return null;
}
return new WebsocketConnectionConfig() {
@Override
public String getEndpointId() {
return "someEndpoint";
}
@Override
public String getPeerId() {
return "peer_" + encrypt(sessionIdCookie.getValue());
}
};
}
Explanation :
-
1 : When a user requests a WebSocket connection,
the
onPeerPreConnect(...)
method of the associated Controller is called. Note that here we receive the default DefaultRequestContext
request context, but if you are using a
custom request context type, you would
receive an object of your custom type (AppRequestContext
, for example).
-
3 : We get the session id of the current user using
a "sessionId" cookie (or any other way).
-
4-7 : If the "sessionId" cookie is not found or if
the user associated with this session doesn't have enough rights to
access a WebSocket Endpoint, we set the response status as
Forbidden
and we return null
. By returning null
, the WebSocket connection
process is cancelled and the HTTP
response is sent as is.
-
9-20 : If the user is allowed to access a WebSocket Endpoint, we
return the information required for that connection. We'll look at that
WebsocketConnectionConfig
object in the next section.
The WebsocketConnectionConfig(...) object
Once you decided that a user can connect to a WebSocket Endpoint, you return an instance of
WebsocketConnectionConfig from
the onPeerPreConnect(...)
method.
In this object, you have to specify two things:
-
The
Endpoint id
to which the user should be connected to.
Note that you can't use the id of an Endpoint that
is already managed by another Controller, otherwise an exception is thrown. If you use null
here, a random Endpoint id will be generated.
-
The
Peer id
to assign to the user. Each Peer id must be unique inside a
given Endpoint, otherwise an exception is thrown. If you return null
here, a random id will be generated.
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 WebsocketConnectionConfig
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 WebsocketController<DefaultRequestContext, DefaultWebsocketContext> {
private final Map<String, WebsocketEndpointManager>
endpointManagers = new HashMap<String, WebsocketEndpointManager>();
protected Map<String, WebsocketEndpointManager> getEndpointManagers() {
return this.endpointManagers;
}
protected WebsocketEndpointManager getEndpointManager(String endpointId) {
return getEndpointManagers().get(endpointId);
}
@Override
public WebsocketConnectionConfig onPeerPreConnect(DefaultRequestContext context) {
return new WebsocketConnectionConfig() {
@Override
public String getEndpointId() {
return "endpoint_" + RandomUtils.nextInt(1, 11);
}
@Override
public String getPeerId() {
return null;
}
};
}
@Override
public void onEndpointReady(WebsocketEndpointManager endpointManager) {
getEndpointManagers().set(endpointManager.getEndpointId(), endpointManager);
}
@Override
public void onPeerMessage(DefaultWebsocketContext context, String message) {
getEndpointManager(context.getEndpointId()).sendMessage(message);
}
@Override
public void onEndpointClosed(String endpointId) {
getEndpointManagers().remove(endpointId);
}
@Override
public void onPeerConnected(DefaultWebsocketContext context) {
}
@Override
public void onPeerMessage(DefaultWebsocketContext context, byte[] message) {
}
@Override
public void onPeerClosed(DefaultWebsocketContext context) {
}
}
Explanation :
-
4-5 : Here, our Controller will manage more than one
Endpoints, so we create a
Map
to keep the association between each Endpoint
and its WebSocket Manager
.
-
20-23 : As the
Endpoint id
to use, this example
returns a random id between 10 different possibilities, randomly distributed to the connecting Peers.
In other words, our Controller is going
to manage up to 10 Endpoints, from "endpoint_1" to "endpoint_10".
-
25-28 : By returning
null
as the
Peer id
, a random id will be generated.
-
32-35 : When an Endpoint is created, we receive its
Manager
and we add it to our endpointManagers
map, using the
Endpoint id
as the key. Our onEndpointReady
method may be called up
to 10 times, one time for each Endpoint our Controller may create.
-
37-40 : Since we manage more than one Endpoints,
we have to use the right
Manager
when sending a message!
Here, we echo back any message received by a Peer, to all Peers connected to the same
Endpoint.
-
42-45 : When an Endpoint is closed, we don't need its
Manager
anymore so we remove it from our endpointManagers
map.
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.
In the following example, we will send the current time to all Peers connected to the Endpoint, every second.
We do so without blocking the onEndpointReady(...)
method :
public void onEndpointReady(WebsocketEndpointManager endpointManager) {
getEndpointManagers().set(endpointManager.getEndpointId(), endpointManager);
final String endpointId = endpointManager.getEndpointId();
Thread sendMessageThread = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
WebsocketEndpointManager 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 Server, Undertow.
{#==========================================
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 :
-
getEndpointId()
: The id of the Endpoint the current Peer is connected to.
-
getPeerId()
: The id of the current Peer.
-
sendMessageToCurrentPeer(String message)
: Sends a text message to the
current Peer.
-
sendMessageToCurrentPeer(byte[] message)
: Sends a binary message to the
current Peer.
-
closeConnectionWithCurrentPeer()
: Closes the connection with the
current Peer.
Utility methods and add-ons:
-
getLocaleToUse()
: The best Locale to use for this Peer, as resolved during the
initial HTTP
request.
-
getTimeZoneToUse()
: The best TimeZone to use for this Peer.
-
json()
: Easy access to the JsonManager.
-
xml()
: Easy access to the XMLManager.
-
templating()
: Easy access to the TemplatingEngine.
-
guice()
: Easy access to the application's Guice context.
{#==========================================
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 AppWebsocketContext extends WebsocketContext<AppWebsocketContext> {
public void customMethod(String message);
}
Explanation :
-
1 : A custom WebSocket context type extends the
base WebsocketContext
interface and parameterizes it using its own type.
Then, we provide an implementation for that custom interface:
public class AppWebsocketContextDefault extends WebsocketContextBase<AppWebsocketContext>
implements AppWebsocketContext {
@AssistedInject
public AppWebsocketContextDefault(@Assisted("endpointId") String endpointId,
@Assisted("peerId") String peerId,
@Assisted WebsocketPeerManager peerManager,
WebsocketContextBaseDeps<AppWebsocketContext> deps) {
super(endpointId,
peerId,
peerManager,
deps);
}
@Override
public void customMethod(String message) {
sendMessageToCurrentPeer("customMethod: " + message);
}
}
Explanation :
-
1-2 : The implementation extends
WebsocketContextBase
so all the default methods/add-ons are kept. Of course, it also implements
our custom
AppWebsocketContext
.
-
4-13 : Don't worry about this scary constructor too much,
just add it as such and it should work. For the curious, the annotations indicate
that this object will be created using
an assisted factory.
-
15-18 : We implement our custom method. This dummy example simply
sends a message to the current Peer, prefixed with "customMethod: ". Note that the
sendMessageToCurrentPeer(...)
method is inherited from WebsocketContextBase
.
Finally, you must let Spincast know about your custom WebSocket Context
type.
This is done by using the
websocketContextImplementationClass(...)
of the
Bootstrapper :
public static void main(String[] args) {
Spincast.configure()
.module(new AppModule())
.requestContextImplementationClass(AppRequestContextDefault.class)
.websocketContextImplementationClass(AppWebsocketContextDefault.class)
.init(args);
//....
}
If you both extended the Request Context
type and the WebSocket Context
type, the
parameterized version of your Router would look like :
Router<AppRequestContext, AppWebsocketContext>
.
But you could also create an unparameterized version of it, for easier usage! :
public interface AppRouter extends Router<AppRequestContext, AppWebsocketContext> {
// 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!