Thursday, June 5, 2014

A chat application with WebSockets, Java and jQuery



I'm not going to describe the WebSockets protocol here - you can find zillions of tutorials online. In this short article I show how to build a simple chat application based on WebSockets.

The good news is that you don't have to worry about the nitty-gritty details of parsing WebSockets headers and packets, that's already implemented by open source libraries. On top of those you can write a server in Java, C#, Python, etc.
I use Tyrus, which makes it so easy to implement the server side of my chat application.
First I need to create a host for my endpoint:

package server;

import java.io.BufferedReader;
import java.io.InputStreamReader;
 
import org.glassfish.tyrus.server.Server;
 
public class WebSocketServer {
 
    public static void main(String[] args) {
        runServer();
    }
 
    public static void runServer() {
        Server server = new Server("localhost", 8025, "/chat", ChatEndpoint.class);
 
        try {
            server.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("Please press a key to stop the server.");
            reader.readLine();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            server.stop();
        }
    }
}

Look at the line where I create the Server object: I'll deploy ChatEndpoint to localhost:8025/chat.
Now I need to create the server itself - this ChatEndpoint type:
package server;

import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
 
import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.CloseReason.CloseCodes;
import javax.websocket.server.ServerEndpoint;
 
@ServerEndpoint(value = "/test")
public class ChatEndpoint {
 
    private Logger logger = Logger.getLogger(this.getClass().getName());
    // keep all open WebSocket sessions (from all users)
    private static Queue queue = new ConcurrentLinkedQueue<>();

    @OnOpen
    public void onOpen(Session session) {
        logger.info("Connect with session: " + session.getId());
        queue.add(session);
        logger.log(Level.INFO, "Connected with " +  session.getId());
    }
 
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.log(Level.INFO, "Mesage received " + message);
        try {
            // broadcast message to all open WebSocket sessions
            for (Session s : queue) {
             // include the original sender
             s.getBasicRemote().sendText(message);
             logger.log(Level.INFO, "Message sent: " + message);
            }
         } catch (IOException e) {
            logger.log(Level.INFO, e.toString());
         }
        // we could have a return here, in which case the returned string
        // would be the one sent from server to client,
        // as if doing s.getBasicRemote().sendText(message)
        //return message + "TEST";
    }
 
    @OnClose
    public void onClose(Session session, CloseReason closeReason) {
        logger.info(String.format("Session %s closed because of %s", session.getId(), closeReason));
        queue.remove(session);
        logger.log(Level.INFO, "Connection closed with " + session.getId());
    }
}
Tyrus makes things easy for us. Look at the annotation @ServerEndpoint(value = "/test") - here I'm saying that my server is accessible under the relative path "/test" under the URL mentioned before, thus the whole URL becomes: localhost:8025/chat/test.

We have three more annotations:
@OnOpen to mark the method for opening the connection from a client to this server; here session uniquely identifies the connecting client
@OnClose to mark the callback triggered when the connection with a particular client closes
@OnMessage for the method that does the most important job: whenever a message is received from a client, this method broadcasts the message to all clients (including the sender)

That's all on the server side.
So now the client is a simple web page with some jQuery embellishments:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script type='text/javascript' src='jquery.js'></script>
<script type='text/javascript' src='jquery-ui-1.8.23.custom.min.js'></script>
<script type='text/javascript'>
var wsocket;
function connect() {
   wsocket = new WebSocket("ws://localhost:8025/chat/test");
   wsocket.onmessage = onMessage;
   alert("Connect");
}

function onMessage(evt) {
   $("#chatText").append("\n" + evt.data);
}

$(document).ready(
  function() {
$("#connect").click(
function() {
wsocket = new WebSocket("ws://localhost:8025/chat/test");
wsocket.onmessage = onMessage;
}
);

$("#send").click(
function() {
wsocket.send($("#username").val() + ":" + $("#tosend").val());
$("#tosend").val("");
}
);
  }
);
</script>

<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Chat</title>
</head>
<body>
Username: <input type="text" id="username"><br>
<input type="submit" value="Connect" id="connect">
<input type="submit" value="Disconnect"><br><br>

<textarea rows="30" cols="50" id="chatText">
</textarea><br>
<textarea rows="4" cols="50" id="tosend">
</textarea>
<br>
<input type="submit" value="Send" id="send">

</body>
</html>


Note how we open the connection with the server:  new WebSocket("ws://localhost:8025/chat/test").
Sending messages from the client to the server is simple too: wsocket.send($("#username").val() + ":" + $("#tosend").val());

Incredibly easy, isn't it ? 
Oh, by the way, this is the list of jars I need for the application to compile (you can use Maven to simplify the deployment a bit):





No comments:

Post a Comment