Site iconJava PDF Blog

WebSocket: Multiple Users, Encoders, Decoders and Dogs.

In my previous tutorial, we were left with a WebSocket server endpoint which bounced back our message. In this tutorial I will show you how to make a chat system using websockets which can handle multiple users, send text and more importantly, send people pictures of my pet, aptly named Socket. I’ll also be breaking an IDR Solutions golden rule by not using a picture of a cat (Although Socket gives cats a run for their money in laziness).

Truly the reason the internet was created!

Multiple User Chat

So without further ado, let’s jump into the WebSocketEndpoint class. The simplest way to do this is to keep a collection of all the connected users. This is where Java’s Set class comes into play, allowing us to keep a list of unique sessions. So at the top of the class insert the set:

private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());

By iterating trough this set we can iterate through all the connected sessions and send them the same message. To simplify this, insert a new method into the class:

    private void sendMessageToAll(String message){
        for(Session s : sessions){
            try {
                s.getBasicRemote().sendText(message);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

Using a set also makes managing sessions very easy, when we get a new connection, we add the session to the set and when the session ends, we remove it.

    @OnOpen
    public void onOpen(Session session){
        System.out.println(session.getId() + " has opened a connection"); 
        sendMessageToAll("User has connected");
        try {
            session.getBasicRemote().sendText("Connection Established");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        sessions.add(session);
    }
    @OnClose
    public void onClose(Session session){
        sessions.remove(session);
        System.out.println("Session " +session.getId()+" has ended");
        sendMessageToAll("User has disconnected");
    }

Thanks to the sendMessagesToAll() method, we can also modify our onMessage method to:

    @OnMessage
    public void onMessage(String message, Session session){
        System.out.println("Message from " + session.getId() + ": " + message);
        sendMessageToAll(message);
    }

Now when we deploy this to the server and load the web page, we can talk to other users.

Cross-browser goodness!

Sending Images

Now where do images come into this? The easiest way to do this is to modify the client to send the image data as a string and to place it into an image element when we get the message. This comes at a major disadvantage though, we’ll lose the ability to send text via the endpoint, more on this later. The first thing to do it to change the type of input from “text” to “input”.

Next, we need to modify our send function to handle the image:

function send(){
    var file = document.getElementById("messageinput").files[0];

    var reader = new FileReader();
    // Sends the result of the file read as soon as the reader has
    // completed reading the image file.
    reader.onloadend = function(){
        webSocket.send(reader.result);
    };
    // Make sure the file exists and is an image
    if(file && file.type.match("image")){
        reader.readAsDataURL(file);
    }

}

We also need to update the writeResponse() method:
function writeResponse(image){
messages.innerHTML += "
" + "<img alt="" src='" + image + "' />";
}

Now when we send an image we get the following:

Note the broken image where the server sends back text not intended as an image.

So, how do we send both text and images over websockets I hear you ask…

Json, Encoders and Decoders

One of the easiest ways is to send off data as a string of Json and let the server handle it.

Server Side

The first thing we should do is set up the server endpoint to handle the incoming Json. For this we need to create three classes; The Json wrapper class, the Encoder class and the decoder class.

The wrapper class is needed as we can use the @OnMessage annotated method to accept the object as a method as the decoder class will handle the conversion from String to our object. The wrapper class is a simple java class with a modified toString() method, which will convert the JsonObject to a string representation, which is sent back to the users as a string via the encoder class.

package com.simon.websocket.data;

import java.io.StringWriter;
import javax.json.Json;
import javax.json.JsonObject;

public class Message {
    private JsonObject json;

    public Message(JsonObject json) {
        this.json = json;
    }

    public JsonObject getJson() {
        return json;
    }

    public void setJson(JsonObject json) {
        this.json = json;
    }

    @Override
    public String toString(){
        StringWriter writer = new StringWriter();

        Json.createWriter(writer).write(json);

        return writer.toString();
    }

}

The MessageDecoder class implements the Decoder.Text<T> interface.

package com.simon.websocket.data;

import java.io.StringReader;
import javax.json.Json;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

/**
 * Decodes a client-sent string into a Message class
 */public class MessageDecoder implements Decoder.Text{
    /**
     * Transform the input string into a Message
     */    @Override
    public Message decode(String string) throws DecodeException {
        JsonObject json = Json.createReader(new StringReader(string)).readObject();
        return new Message(json);
    }

    /**
     * Checks whether the input can be turned into a valid Message object
     * in this case, if we can read it as a Json object, we can.
     */    @Override
    public boolean willDecode(String string) {
        try{
            Json.createReader(new StringReader(string)).read();
            return true;
        }catch (JsonException ex){
            ex.printStackTrace();
            return false;
        }
    }

    /**
     * The following two methods are placeholders as we don't need to do anything
     * special for init or destroy.
     */    @Override
    public void init(EndpointConfig config) {
        System.out.println("init");
    }
    @Override
    public void destroy() {
        System.out.println("destroy");
    }

}

And our MessageEncoder class, which prepares the message object for transmission to the clients. It implements the interface Encoder.Text<T>.

package com.simon.websocket.data;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

/**
 * Converts the message class to a string.
 */public class MessageEncoder implements Encoder.Text {

    @Override
    public String encode(Message message) throws EncodeException {
        return message.getJson().toString();
    }

    @Override
    public void init(EndpointConfig config) {
        System.out.println("Init");
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
    }

}

We now need to set up our actual endpoint so that it knows to use our new classes. Below is what our Endpoint now looks like. Some things to note are:

import com.simon.websocket.data.Message;
import com.simon.websocket.data.MessageDecoder;
import com.simon.websocket.data.MessageEncoder;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.json.Json;
import javax.websocket.EncodeException;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/** 
 * @ServerEndpoint gives the relative name for the end point
 * This will be accessed via ws://localhost:8080/EchoChamber/echo
 * Where localhost is the address of the host
 */@ServerEndpoint(value="/echo", encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class}) 
public class EchoServer {
    // This might not look right as wordpress hates angle brackets.
    private static final Set sessions = Collections.synchronizedSet(new HashSet());

    /**
     * @OnOpen allows us to intercept the creation of a new session.
     * The session class allows us to send data to the user.
     * In the method onOpen, we'll let the user know that the handshake was 
     * successful.
     */    @OnOpen
    public void onOpen(Session session){
        System.out.println(session.getId() + " has opened a connection");    

        Message message = new Message(Json.createObjectBuilder()
            .add("type", "text")
            .add("data", "User has connected")
            .build());
        sendMessageToAll(message);

        try {
            Message connectedMessage = new Message(Json.createObjectBuilder()
            .add("type", "text")
            .add("data", "User has connected")
            .build());
            session.getBasicRemote().sendObject(connectedMessage);
        } catch (IOException | EncodeException ex) {
            ex.printStackTrace();
        }
        sessions.add(session);
    }

    /**
     * When a user sends a message to the server, this method will intercept the message
     * and allow us to react to it. For now the message is read as a String.
     */    @OnMessage
    public void onMessage(Message message, Session session){
        System.out.println("Message from " + session.getId() + ": " + message);
        sendMessageToAll(message);
    }

    /**
     * The user closes the connection.
     * 
     * Note: you can't send messages to the client from this method
     */    @OnClose
    public void onClose(Session session){
        sessions.remove(session);
        System.out.println("Session " +session.getId()+" has ended");
        Message message = new Message(Json.createObjectBuilder()
            .add("type", "text")
            .add("data", "User has disconnected")
            .build());
        sendMessageToAll(message);
    }

    private void sendMessageToAll(Message message){
        for(Session s : sessions){
            try {
                s.getBasicRemote().sendObject(message);
            } catch (IOException | EncodeException ex) {
                ex.printStackTrace();
            }
        }
    }
}

Client Side

We now need to modify our client html to send and receive Json objects. While we’re at it I also added inputs for both images and text. The things to note in here are:

Pastebin link

With this we can now send text and images using websockets.

Now they know I’m not just sending a picture of any old dog.

And now we have a chat client which can send both text and images via websockets. Got any comments? leave them below.

If you want to see the full source code, check out my GitHub repository.