Tutorials‎ > ‎

Simple Chat Web Application using Java Web Socket

posted Sep 14, 2014, 9:23 PM by Kenrick Satrio Sahputra   [ updated Aug 15, 2016, 11:35 PM by Surya Wang ]

In this tutorial, I will guide you to create a chat web application using java websocket technology. I assumed you already know about basic java, html and javascript.
I've already include the final application in the attachment (chat.war). To try the application, you can place chat.war into tomcat's webapp directory (Minimum tomcat version 7).

Features of the chat application:
  • Connect and disconnect from the chat application
  • List all people who has been connected to the chat application
  • Broadcast a message to all people
  • Chat to a specific people
Applications used in this tutorial: 
  • JDK 7
  • Tomcat 8
  • Chrome
1. Introduction
Websocket is web technology that enable a full duplex connection between client and server, so that client and server can communicate seamlessly. This persistent connection will reduce the overhead of HTTP request over the network compare to other techniques which allow server to push data to client (such as long polling, comet, etc). We will use websocket to push a new data seamlessly. The technology we are going to use is specified in JSR-356 which is java specification for web socket, and Tomcat 8 as the servlet container, also as the implementation of JSR-356. For more information about websocket, you can visit this link: http://www.html5rocks.com/en/tutorials/websockets/basics/.

When using websocket, we need define minimal one endpoint from the server side. The endpoints – that are represented as URLs, are used by clients to establish a connection. After a websocket has been created, there are 4 methods that are provided to capture a specific event, which are onopen, onmessage, onerror, onclose. This is method is available both on the client and the server side. Below are the purpose for each method
 Method    Purpose 
 OnOpen    an event when a websocket connection has been established
 OnMessage  an event when a websocket receive a message
 OnError  an event when a websocket capture an error
 OnClose  an event when a websocket connection has been closed

But in this tutorial we are not going to implements the onerror event.

2. Server Web Socket Endpoint

Create new class in source directory, name it "MyWebSocketEndPoint.java". Add @ServletEndPoint annotation at class level to tell servlet container that this class is a websocket endpoint. Then add a "value" attribute and initialize it with "/chat" which value represents the endpoint path.

@ServerEndpoint(value = "/chat")
public class MyWebSocketEndPoint { }

Since we need  a capability to broadcast a message to all clients that are connected, we need to store each socket's session object in a collection. We will store the socket 's session in a Map structure, so that we can map between the username and its socket object. I use a synchronized map so if there are two or more request at the same time, the request will be synchronized. 

private static final String USERNAME_KEY = "username";
private static Map<String, Session> clients = Collections.synchronizedMap(new LinkedHashMap<String, Session>());

Below are use cases for our server endpoint class :
 Method    Use Case
 OnOpen
  • Get the new socket's username from url
    (e.g. if the url is "ws://localhost:8080/chat?username=Andi", then the username is "Andi")
  • Add the new socket to the collection
  • Give a list current online users to the new socket connection
  • Broadcast message, that a new people has join the chat room
 OnMessage   
  • Extract the information of incoming message.
    The message format is "to|message".
    (e.g. a message "Hello, world" to "Andi" will be formatted to be "Andi|Hello, world")
  • Deliver the message according to the destination 
 OnClose
  •  Broadcast a message to all online users, that current user has been disconnected

Now we will implements onopen, onmessage, and onclose method with annotation @OnOpen, @OnMessage, @OnClose respectively. The method name does not important here, but the annotation and the parameters does. 


@OnOpen
public void onOpen(Session session) throws Exception {

    //Get the new socket's username from url
    //e.g. url: ws://localhost:8080/chat?username=Andi, so Andi is the username
    Map<String, List<String>> parameter = session.getRequestParameterMap();
    List<String> list = parameter.get(USERNAME_KEY);
    String newUsername = list.get(0);

    //Add the new socket to the collection
    chatRooms.put(newUsername, session);

    //also set username property of the session.
    //so when there a new message from a particular socket's session obj
    //we can get the username whom send the message
    session.getUserProperties().put(USERNAME_KEY, newUsername);

    //Give a list current online users to the new socket connection
    //because we store username as the key of the map, we can get all
    //  username list from the map's keySet
    String response = "newUser|" + String.join("|", chatRooms.keySet());
    session.getBasicRemote().sendText(response);

    //Loop through all socket's session obj, then send a text message
    for (Session client : chatRooms.values()) {
        if(client == session) continue;
        client.getBasicRemote().sendText("newUser|" + newUsername);
    }
}

@OnMessage
public void onMessage(Session session, String message) throws Exception {
    //Extract the information of incoming message.
    //Message format: 'From|Message'
    //so we split the incoming message with '|' token
    //to get the destination and message content data
    String[] data = message.split("\\|");
    String destination = data[0];
    String messageContent = data[1];

    //Retrieve the sender's username from it's property
    String sender = (String)session.getUserProperties().get(USERNAME_KEY);

    //Deliver the message according to the destination
    //Outgoing Message format: "message|sender|content|messageType?"
    //the message type is optional, if the message is intended to be broadcast
    //  then the message type value is "|all"
    if(destination.equals("all")) {
        //if the destination chat is 'all', then we broadcast the message
        for (Session client : chatRooms.values()) {
            if(client.equals(session)) continue;
            client.getBasicRemote().sendText("message|" + sender + "|" + messageContent + "|all" );
        }
    } else {
        //find the username to be sent, then deliver the message
        Session client = chatRooms.get(destination);
        String response = "message|" + sender + "|" + messageContent;
        client.getBasicRemote().sendText(response);
    }
}

@OnClose
public void onClose(Session session) throws Exception {
    //remove client from collecton  
    String username = (String)session.getUserProperties().get(USERNAME_KEY);
    chatRooms.remove(username);
    
    //broadcast to all people, that the current user is leaving the chat room
    for (Session client : chatRooms.values()) {
        client.getBasicRemote().sendText("removeUser|" + username);
    }
}

3. Client Side Web Socket

That's for the server side, now we going to do with the client side, starting from the html first. 
3.1. Create a new html file in the root of web folder, named it index.html.

<html>
    <head>
        <title>Chat Tutorial</title>
        <link rel="stylesheet" href="style.css"/>
    </head>
    <body>
        <header>
            <input id="username" placeholder="Username..." autofocus>
            <button id="connect">Connect</button>
            <button id="disconnect" disabled>Disconnect</button>
        </header>
        <aside>
            <h5>Online User(s)</h5>
            <ul id="userList">
                <li id="all" class="hoverable">All</li>
            </ul>
        </aside>
        <article>
            <div id="dialog">
                <span>Chat to <span id="chatTo">All</span></span>
                <div id="message-board"></div>
                <hr>
                <textarea id="message" placeholder="message.."></textarea>
                <button id="send">Send</button>
            </div>
        </article>
        <script src="script.js"></script>
    </body>
</html>

3.2 Create a new javascript file, named it "script.js"
-. First of all, define all the variables that we need.

//DOM Element
var usernameInputEl = document.querySelector("#username");
var connectBtnEl = document.querySelector('#connect');
var disconnectBtnEl = document.querySelector('#disconnect');
var usernameListEl = document.querySelector("#userList");
var articleEl = document.querySelector('article');
var messageBoardEl = articleEl.querySelector('#message-board');
var messageInputEl = articleEl.querySelector('#message');
var sendBtnEl = articleEl.querySelector('#send');
//label in the message board title
var chatToEl = articleEl.querySelector('#chatTo'); 
// All btn, to chat to all people in the room
var chatToAllEl = document.querySelector('#all');

// current chat destination
var chatTo = 'all'; 

//Chat room that holds every conversation
var chatRoom = {
    'all': []
};

//socket object.
var socket = undefined;

-. Add a listener to connect and disconnect button

connectBtnEl.onclick = connect;
disconnectBtnEl.onclick = disconnect;

function connect() {
    //ws is a websocket protocol
    //location.host + location.pathname is the current url
    //new WebSocket(url) will immediately open a websocket connection
    socket = new WebSocket("ws://"+ location.host + location.pathname +"chat?username=" + usernameInputEl.value);

    //add the event listener for the socket object
    socket.onopen = socketOnOpen;
    socket.onmessage = socketOnMessage;
    socket.onclose = socketOnClose;
}

function disconnect() {
    //close the connection and the reset the socket object
    socket.close();
    socket = undefined;
}

Now we will implement the socket's event – onopen, onmessage, onclose. The implementation is simple, when the socket has opened, we just simply disable username input field, connect button and also enable the disconnect button. Same as onopen, in the onclose function we just do the reverse. In addition on onclose function we also reset all input fields. 

For onmessage event handler, we use some formatting strategy to distinguish each incoming message. The format that we used looks like this: "eventname|data". We only have 3 event in this chat application, which are:
 Event Name Data
 newUser     list of online users seperated by '|' token. 
e.g. Andi|Budi|Carlie
 removeUser a user that has been disconnected.
e.g. Andi
 messagecontains sender username, message, and destination
sender: username who send the message
message: the message that is sent
destination: the value is null or 'all'. when the value is all, it means, this message is being broadcasted by the sender. So treat this conversation as a broadcast message not an individual message from the sender,

e.g. Andi|Hello, world|all



function socketOnOpen(e) {
    usernameInputEl.disabled = true;
    connectBtnEl.disabled = true;
    disconnectBtnEl.disabled = false;
}

function socketOnMessage(e) {
    var eventName = e.data.substr(0, e.data.indexOf("|"));
    var data = e.data.substr(e.data.indexOf("|") + 1);

    var fn;
    if(eventName == 'newUser') fn = newUser;
    else if(eventName == 'removeUser') fn = removeUser;
    else if(eventName == 'message') fn = getMessage;

    fn.apply(null, data.split('|'));
}

function socketOnClose(e) {
    usernameInputEl.disabled = false;
    connectBtnEl.disabled = false;
    disconnectBtnEl.disabled = true;
    usernameInputEl.value = '';
    messageBoardEl.innerHTML = '';
    chatToEl.innerHTML = 'All';
    usernameListEl.innerHTML = '';
}

4. Utility Function

function newUser() {
    //if there is no users, return from the function
    if(arguments.length == 1 && arguments[0] == "") return;
    var usernameList = arguments;

    //Loop through all online users and create a list from it
    var documentFragment = document.createDocumentFragment();
    for(var i = 0; i < usernameList.length; i++) {
        var username = usernameList[i];
        var liEl = document.createElement("li");
        liEl.id = username;
        liEl.textContent = username;
        liEl.onclick = chatToFn(username);
        if(username != usernameInputEl.value) liEl.classList.add('hoverable');
        documentFragment.appendChild(liEl);
    };
    usernameListEl.appendChild(documentFragment);
}

function getMessage(sender, message, to) {
        to = to || sender;

        if(chatTo == to) {
            var newChatEl = createNewChat(sender, message);
            messageBoardEl.appendChild(newChatEl);
        } else {
            var toEl = usernameListEl.querySelector('#' + to);
            addCountMessage(toEl);
        }

        if(chatRoom[to]) chatRoom[to].push(newChatEl);
        else chatRoom[to] = [newChatEl];
}

function removeUser(removedUsername) {
    var usernameList = usernameListEl.children;
    for(var i = 0; i < usernameList.length; i++) {
        var username = usernameList[i].textContent;
        if(username == removedUsername) {
            usernameListEl.removeChild(usernameList[i]);
        }
    }
}

function createNewChat(sender, message) {
    var newChatDivEl = document.createElement('div');
    var senderEl = document.createElement('span');
    var messageEl = document.createElement('span');

    if(sender == usernameInputEl.value)
        sender = 'me';

    senderEl.textContent = sender;
    messageEl.textContent = message;

    newChatDivEl.appendChild(senderEl);
    newChatDivEl.appendChild(messageEl);
    return newChatDivEl;
}

function addCountMessage(toEl) {
    var countEl = toEl.querySelector('.count');
    if(countEl) {
        var count = countEl.textContent;
        count = +count;
        countEl.textContent = count + 1;
    } else {
        var countEl = document.createElement('span');
        countEl.classList.add('count');
        countEl.textContent = '1';
        toEl.appendChild(countEl);
    }
}

sendBtnEl.onclick = sendMessage;
chatToAllEl.onclick = chatToFn('all');

function sendMessage() {
    var message = messageInputEl.value;
    if(message == '') return;
    socket.send(chatTo + '|' + message );
    messageInputEl.value = '';

    var sender = usernameInputEl.value;
    getMessage(sender, message, chatTo);
    messageBoardEl.scrollTop = messageBoardEl.scrollHeight;
}

function chatToFn(username) {
    return function(e) {
        if(username == usernameInputEl.value) return;

        var countEl = usernameListEl.querySelector('#' + username + '>.count');
        if(countEl) {
            countEl.remove();
        }

        chatToEl.textContent = username;
        chatTo = username;
        messageBoardEl.innerHTML = '';

        var conversationList = chatRoom[chatTo];
        if(!conversationList) return;
        var df = document.createDocumentFragment();
        conversationList.forEach(function (conversation) {
            df.appendChild(conversation);
        });
        messageBoardEl.appendChild(df);
    }
}

5. Finishing
Last but not least we will style our page using css. Because styling is not the main part of this tutorial, you can just download the style.css from the attachment and locate it to the root of your web application project.



Thanks for reading the tutorial until finish, i hope you can understand :).



ċ
chat.war
(6k)
Kenrick Satrio Sahputra,
Sep 15, 2014, 8:45 PM
ċ
style.css
(3k)
Kenrick Satrio Sahputra,
Sep 15, 2014, 8:49 AM