Blogs
Liferay 7 supports Websockets; see how you can implement your own Push support.
Introduction
I was recently polling my coworkers looking for new blog ideas, and Filipe Melo asked about Websockets. I haven't seen any other documentation or blogs about building Websocket solutions for Liferay, so I figured this would be an interesting challenge.
Websockets was introduced to provide a full duplex and realtime client/server communication path between remote clients and your server-side framework. JSR 356 was introduced to add Websocket support to Java applications, and Liferay added support for Websockets in 7.0.
Most of the time when you happen upon an article about Websockets, they present an implementation of a chat room, and we'll be implementing a similar thing here. All users will be in the chat room, anyone can send a message, and the messages will be broadcast to everyone. Unlike other examples though, our server is going to run in Liferay and will be cluster-aware and our client will be a simple portlet that can be added to a portal page.
Yeah I know, it's a lame example, but it is just an example. The model that I'm presenting here can be used for anything you have a need for.
The Websocket Whiteboard
Liferay 7 introduced an HTTP Whiteboard, and it allows OSGi
modules to create servlets that live under the
/o/my-servlet
sort of path.
Liferay also introduced a Websocket Whiteboard to provide
similar capabilities for OSGi modules to define new Websocket
endpoints that will be available under the /o/websocket
path.
To create our server-side endpoint, we need to extend the
javax.websocket.Endpoint
class, and since we're using
OSGi we'll also need to add the @Component
annotation:
@Component( immediate = true, property = { "org.osgi.http.websocket.endpoint.path=/o/websocket/chat/{username}", "org.osgi.http.websocket.endpoint.decoders=" + "com.dnebinger.websockets.chat.ChatMessageDecoder", "org.osgi.http.websocket.endpoint.encoders=" + "com.dnebinger.websockets.chat.ChatMessageEncoder" }, service = Endpoint.class ) public class ChatEndpoint extends Endpoint { /** * onOpen: We must implement this method to be a real endpoint. * * @param session the session that has just been activated. * @param config the configuration used to configure this endpoint. */ @Override public void onOpen(final Session session, EndpointConfig config) { } }
This is our definition for the ChatEndpoint
class.
It extends Endpoint
, and it is also registered as an @Component
.
The properties set the main configuration for the endpoint.
Property | Description |
---|---|
org.osgi.http.websocket.endpoint.path
| Defines the path for the endpoint to match on. Paths must be unique in the Websocket Whiteboard. In this case we are registering under /o/websocket/chat, and no other Websocket can handle this same path. It also supports named path parameters. I recommend paths using the /o/websocket prefix to avoid conflict with other portal or portlet URLs. |
org.osgi.http.websocket.endpoint.decoders
| A comma-separated list of class names that implement the Decoder interface to transform incoming raw message data into Java objects. |
org.osgi.http.websocket.endpoint.encoders
| Comma-separated list of class names that implement the Encoder interface to transform Java objects into raw types that can be sent as a Websocket response. |
org.osgi.http.websocket.endpoint.subprotocol
| An optional comma-separated list of supported subprotocols. |
Receiving Messages
in the onOpen()
method of our Endpoint
implementation, we can register a new message handler:
public void onOpen(final Session session, EndpointConfig config) { final String username = session.getPathParameters().get("username"); // add a message handler for new chat messages session.addMessageHandler(new MessageHandler.Whole<ChatMessage>() { /** * onMessage: Called when the message has been fully received. * * @param message the message data. */ @Override public void onMessage(ChatMessage message) { message.setFrom(username); // send to all active sessions on the current node broadcast(message); // broadcast to the cluster so they also will broadcast to // their websocket sessions chatMessageClusterSender.sendChatMessage( message.getFrom(), message.getMessage()); } }); session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String text) { JSONObject json = JSONFactoryUtil.createJSONObject(text); ChatMessage message = new ChatMessage(username, json.getString("message")); broadcast(message); chatMessageClusterSender.sendChatMessage(username, message.getMessage()); } }); }
We can access the defined path parameters from the
Session
's getPathParameters()
method and
extract the one we want.
We then add a new MessageHandler
implementation for
a whole ChatMessage
object (this will be created from
the Decoders
property). You can add multiple new
MessageHandler
implementations for different types of
objects or even for the raw types as we did here by adding a
MessageHandler
using a raw String type (with the bug I
spoke of above, the only MessageHandler
I could register
was this String method).
In the implementation above, we're using a static public method
broadcast()
(not shown) to send the message to all of the
active websocket sessions on this node.
And, to be cluster-friendly, we're also going to use the cluster-wide
messaging technique to get the chat message to all of the other
nodes, and they too will invoke the broadcast()
method to
broadcast the message to the active websocket sessions they have registered.
We need to do this because, in a cluster, a client will establish a websocket session with only one node in the cluster. This means that each node in your cluster can have one or more active sessions on their own that the other nodes will not know about.
Leveraging Liferay's built-in cluster-wide messaging we can send the chat message to all of the other nodes in the cluster, and they can be responsible for sending the chat message to the active sessions they each are tracking.
Test Portlet
I created a super-simple portlet to just demonstrate how to create a websocket client that interacted with our new Chat server-side component. Here's the view.jsp:
<%@ include file="/init.jsp" %> <form> <input id="chatMessageText" type="text"> <input onclick="wsSendMessage();" value="Chat" type="button"> <input onclick="wsCloseConnection();" value="Disconnect" type="button"> </form> <br> <textarea id="echoText" rows="5" cols="30"></textarea> <script type="text/javascript"> var chatUrl = "ws://localhost:8080/o/websocket/chat/<%= user.getScreenName() %>"; var webSocket = new WebSocket(chatUrl); var echoText = document.getElementById("echoText"); echoText.value = ""; var message = document.getElementById("chatMessageText"); webSocket.onopen = function(message){ wsOpen(message);}; webSocket.onmessage = function(message){ wsGetMessage(message);}; webSocket.onclose = function(message){ wsClose(message);}; webSocket.onerror = function(message){ wsError(message);}; function wsOpen(message){ echoText.value += "Connected ... \n"; } function wsSendMessage(){ var chatMsg = { from: "<%= user.getScreenName() %>", message: message.value }; webSocket.send(JSON.stringify(chatMsg)); echoText.value += "Message sent to the server : " + message.value + "\n"; message.value = ""; } function wsCloseConnection(){ webSocket.close(); } function wsGetMessage(message){ echoText.value += "Message received from to the server : " + message.data + "\n"; } function wsClose(message){ echoText.value += "Disconnect ... \n"; } function wsError(message){ echoText.value += "Error ... \n"; } </script>
First I have a simple form for a message, a Chat button to send the message, a Disconnect button to drop the connection, and finally a text area to show the received messages.
There's a bunch of javascript to establish the Websocket connection and then a bunch of method wiring for sending the message and what not.
In the wsSendMessage()
method, I'm building the JS
object that the server side expects and sending it on the socket. In
the wsGetMessage()
method I'm just dumping the data, but
I know I'm getting back the same JSON object that I'm sending so I
could use the fields in a smarter way.
In my local environ I created a few different accounts and used different browsers to log in and share messages, here's an example from the chat:
Connected ... Message received from to the server : {"from":"test","message":"Connected!"} Message received from to the server : {"from":"37410","message":"Connected!"} Message received from to the server : {"from":"alpha","message":"Connected!"} Message received from to the server : {"from":"alpha","message":"hey who is on?"} Message sent to the server : I'm on! Message received from to the server : {"from":"test","message":"I'm on!"} Message received from to the server : {"from":"alpha","message":"Have you seen beta?"} Message sent to the server : No but I hear he might log in later... Message received from to the server : {"from":"test","message":"No but I hear he might log in later..."} Message received from to the server : {"from":"37410","message":"Connected!"} Message received from to the server : {"from":"beta","message":"Connected!"} Message received from to the server : {"from":"alpha","message":"Looks like beta has joined"} Message received from to the server : {"from":"beta","message":"yeah I'm here too"}
Conclusion
So pretty simple, right? I was actually kind of surprised at how easy it was to get rolling...
Using this as a model, you could build duplex systems like this where the browsers are sending data and the server side is pushing data or you could focus on one-way data sending from the browser to the server or a push from the server to the browsers.
And with the cluster support that Liferay has, you can handle your Websockets across all of the nodes in the cluster; the browsers won't know or care what node they're connected to because all nodes will be able to send the same data.
The repo for these modules is available here: https://github.com/dnebing/websockets
Hope you find this useful! Let me know what kind of Websocket solutions you come up with...