Java 2

Week 11

Web Sockets

  • Up to this point, our web applications have required a single request from a client and a response from the server. Web page content remains static until a new request is made.

  • WebSockets are built into web browsers and provide 2-way communication between the browser and the server. This allows them to talk back and forth continuously, like a phone call.

  • With web sockets, applications can allow a communication channel to be left open after the initial request and response is made.

  • Multiple users who connect can interact live. When a user sends a new request, the server can respond by sending data out to everyone connected.

  • In this lesson, we will create a chat app where each client makes a request to a shared server.

How it works

  • In this diagram, we have a server and we have multiple subscribers. Each subscriber is connected to the server and is added to a subscriber list.
  • The subscriber list is the list of clients that need to be notified if something changes.
  • One subscriber will send a message via their chat client. That sends an update to the server. The server then goes through its subscriber list and sends that message out to the other subscribers.

GroupChat Client Setup

  • Create a GroupChat servlet ("/group-chat") with the following inside the doGet method.
  • We don't need to put this JSP in the WEB-INF folder because the doGet method doesn't request any data from the database.
req.getRequestDispatcher("group-chat.jsp").forward(req, resp);
  • Create a group-chat.jsp file. Add the following HTML to the file.
  • Don't forget to add a new link to the main-nav and include it in this file.
<div class="container mt-5">
  <div class="row justify-content-center">
    <div class="col-md-6">
      <h2>Chat</h2>
      <form id="messageForm">
        <div class="form-group">
          <label for="userName">Your Name</label>
          <input type="text" class="form-control" id="userName">
        </div>
        <div class="form-group">
          <label for="message">Message</label>
          <textarea class="form-control" id="message" name="message" rows="5"></textarea>
        </div>
        <button type="submit" class="btn btn-primary">Send</button>
      </form>
      <!-- Error Notification Placeholder -->
      <div id="errorText" class="alert alert-danger d-none" role="alert"></div>
      <!-- Message Placeholder -->
      <div id="messages"></div>
    </div>
  </div>
</div>

GroupChat Client Setup

  • The input consists of two fields: one for the user name and one for the message.
  • Note that the form has no action attribute. When the form is submitted, the user name and message will be sent through the WebSocket, not a servlet.
  • The <div id="messages"> block is where both incoming and outgoing messages show up. The new messages appear appended to the bottom of the existing text. A scroll bar appears as necessary.

GroupChat Client Setup

  • Create a group-chat.js file in a scripts folder.  Add a simple console.log statement indicating that the file is found.
console.log("file found");
  • Add a script tag before the closing body tag of the JSP.
<script src="scripts/group-chat.js"></script>
  • Create a group-chat.css file. Add the following code. We will discuss what this does later.
#messages {
    border: 1px solid black;
    height: 300px;
    padding: 10px;
    overflow-y: auto;
}
#messages div {
    margin-bottom: 10px;
}
#messages p {
    padding: 5px 10px;
    margin: 0;
    border-radius: 10px;
}
#messages span {
    padding: 5px;
    font-size: 12px;
    font-style: italic;
}
#messages .in {
    margin-right: 100px;
}
#messages .in p {
    background-color: hsl(210, 0%, 70%);
}
#messages .out {
    margin-left: 100px;
}
#messages .out p {
    background-color: hsl(210, 100%, 50%);
    color: white;
}
  • Add a link tag before the closing head tag in the JSP.
<link rel="stylesheet" href="styles/group-chat.css">

Dependencies and JSON

  • In order to support servlets and websockets, we need to include this Jakarta EE dependency in the pom.xml file.
    • We are using Tomcat 10.1, so use Jakarta 10.0
  • We also will need to include this JSON dependency.
  • In the browser, JavaScript is used to handle all of the logic in the processing. To communicate with WebSockets the browser will use JSON (JavaScript Object Notation) as its protocol.
  • The JSON for this project will look like this:
{"name":"Marc","message":"Hello world"}

MyJson Objects

  • Client-side JavaScript will read the form data, package it in a JSON string and send that to the server.

  • The server will interpret the JSON object to find all the data inside.

  • The server will create a repackaged JSON object and respond by sending it back to all subscribed client's browser.

  • To implement this on the server, create a Java class called MyJson. Put it in the "shared" package.

import jakarta.json.Json;
import jakarta.json.JsonObject;

import java.io.StringWriter;

public class MyJson {

    private JsonObject json;

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

    public JsonObject getJson() {
        return json;
    }

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

    public String toString() {
        StringWriter writer = new StringWriter();
        Json.createWriter(writer).write(this.json);
        return writer.toString();
    }
}

MyJson Objects

  • The constructor takes a JsonObject as a parameter. Getter and setter methods are added to handle that JsonObject.

  • The toString() method converts the JsonObject to a String format that resembles JSON. It uses the createWriter method of the Json class to create a JsonWriter object by passing it a StringWriter.

    • A StringWriter is better suited for transmission than a standard String object.

  • The JsonWriter write method then takes our JsonObject and converts it to a String representing the JSON version of the data.

  • The MyJson class can be used for all Websocket-related projects.

MyDecoder

  • Create a new Java class called MyDecoder to handles the decoding of incoming json data. Data is read in as JSON Strings. The result is a MyJson object.

    • When a client sends json through the WebSocket, the web server uses the MyDecoder class to translate JSON into a MyJson object.

  • Have this class implement the Decoder.Text<T> interface.

  • Implement the abstract methods decode(String s) and willDecode(String s)

     

public class MyDecoder implements Decoder.Text<MyJson> {
  @Override
  public MyJson decode(String s) throws DecodeException {
      JsonObject jsonObject = Json.createReader(new StringReader(s)).readObject();
      return new MyJson(jsonObject);
  }

  @Override
  public boolean willDecode(String s) {
      boolean result;
      try {
          JsonObject jsonObject = Json.createReader(new StringReader(s)).readObject();
          result = true;
      } catch (JsonException jex) {
          result = false;
      }
      return result;
  }
}

MyDecoder

  • If the init(EndpointConfig config) and destroy() methods are added, simply remove or comment any code inside them since they return void.
  • The willDecode method will read in a String and check to see if it can be converted to a JsonObject. It does this by first converting the String to a StringReader object.
    • The Json class has a createReader method that takes the StringReader object to create a JsonReader object.
    • The JsonReader class has a readObject method that returns a JsonObject. A JsonException error will be thrown if a JSON object cannot be created.
  • The decode method does the same thing the willDecode method does but passes the JsonObject to the MyJson constructor to create a Java representation of JSON.
  • This class can be reused for all Websocket-related projects.

MyEncoder

  • Create a new Java class called MyEncoder to handles converting a MyJson object into a JSON string for transmission.

  • Have this class implement the Encoder.Text<T> interface.
  • Implement the abstract method encode(MyJson object).
public class MyEncoder implements Encoder.Text<MyJson> {
  @Override
  public String encode(MyJson myJson) throws EncodeException {
      return myJson.toString();
  }
}
  • If the init(EndpointConfig config) and destroy() methods are added, simply remove or comment any code inside them since they return void.
  • The encode method will take a MyJson object, call the toString method and return the String representation of JSON.
  • This class can be reused for all Websocket-related projects.

Group Chat Endpoint

  • An endpoint is where the socket connects to the server. Tomcat will direct requests to the endpoint as they arrive.

  • Create a new WebSocket Endpoint class called GroupChatEndpoint. The server will use this file to respond to requests over the WebSocket.

  • Change the annotation to include the endpoint value along with a list of one or more encoder and decoder classes.

@ServerEndpoint(
        value = "/group-chat/endpoint",
        encoders = {MyEncoder.class},
        decoders = {MyDecoder.class}
)
  • This annotation tells the web server that we want the ChatEndpoint class to handle connections on WebSockets where the request URL ends with "/group-chat/endpoint".
  • It also tells the web server what classes are to be used to encode and decode Json strings.

Group Chat Endpoint

  • This class needs to maintain a collection of connected Session objects. They are different from the HttpSession object. They are the session connections that will be notified when a new message comes in.

private static final Set<Session> subscribers = Collections.synchronizedSet(new HashSet<Session>());
  • HashSet is one of the fundamental data structures in the Java Collections API. It is similar to a HashMap, but does not store keys/value pairs.

  • Both HashSet and HashMap do not allow duplicate values. There can be at most one null value. It doesn't maintain insertion order and is not thread-safe.

  • The Collections API has several synchronized methods that takes a Collection object and returns a thread-safe version back.

Group Chat Endpoint

  • Create an onOpen method to be called every time a client connects (from a web browser) the web server. 

@OnOpen
public void onOpen(Session session) {
    subscribers.add(session);
    System.out.println("Subscriber count: " + subscribers.size());
}
  • This class will eventually maintain a collection of connected Session objects. A session is a client connection that will be notified when a new message comes in. They are different from the HttpSession object. 

    • Note that printing here is for demonstration purposes only so you can see the what is being passed through each method.
    • Annotations, like @OnOpen, provide instructions for the compiler, preprocessor, and other background processes. We use annotations to let the server know we want to use certain built in features.

Group Chat Endpoint

  • The onClose method is called when a connected client asks to cancel the connection.

     

@OnClose
public void onClose(Session session) {
    subscribers.remove(session);
    System.out.println("Subscriber count: " + subscribers.size());
}
  • The parameter is a Session object that represents the connection with that client.
  • This method removes the Session from the Set.
  • The onError method handles errors as they show up. Throwable is the parent class for all Exception and Error objects.
@OnError
public void onError(Throwable throwable) {
    System.err.println("ERROR: " + throwable.getMessage());
}

JavaScript

  • onOpen() describes what to do when the WebSocket first opens. We will log the opening of the connection. To see the log output, open the browser's developer tools Console tab.
  • onClose() will display a message on the webpage when the WebSocket connection times out.
  • onError() describes the actions to perform when something goes wrong. In this case we will display an error one the webpage.  This will be called if the endpoint URL is incorrect.

  • We now have a server that allows multiple clients to connect. Run the program. Open multiple browsers or open a Private browsing window. For each session, view the browser's console and IntelliJ's terminal.

JavaScript

  • A JavaScript file will contain all the WebSocket functionality required by the client side of the application.

  • Declare a variable representing the full URi to the Group Chat Endpoint. When deployed to Azure, "ws:" needs to be "wss:".

var wsProtocol = 'ws://';
if (window.location.protocol === 'https:') {
    wsProtocol = 'wss://';
}
const wsUri = wsProtocol + document.location.host + document.location.pathname + "endpoint";
const websocket = new WebSocket(wsUri); 

JavaScript

  • Next, the file establishes onopen, close, and onerror functions that respond to the WebSocket’s events. 

websocket.onopen = function (event) {
    console.log("opened websocket: " + wsUri);
};

function displayError(msg) {
    const errorText = document.getElementById("errorText");
    errorText.innerText = msg;
    errorText.classList.remove("d-none"); // Displays the errors message
}

// You do not need websocket.close or websocket.onerror

onMessage

  • The onMessage method is called every time a client sends a message.
@OnMessage
public void onMessage(MyJson myJson, Session session) throws IOException, EncodeException {
    System.out.println(myJson);
    for (Session subscriber : subscribers) {
        if (!subscriber.equals(session)) {
            subscriber.getBasicRemote().sendObject(myJson);
        }
    }
}
  • When a client sends a message through the WebSocket, the web server uses the MyDecoder class to translate it into a MyJson object.
  • The server then calls the GroupChat Endpoint onMessage method, passing the MyJson and the Session of the client who sent it.
  • This method then loops through all the other clients in the subscribers Set and sends the MyJson object to the MyEncoder class. The MyEncoder class converts the MyJson object to a Json string. The result is that all the subscribed clients get a copy of the Json string.
  • The if statement will not send the message to the session from which the message originated.

Observer Pattern

  • This use of WebSockets follows the design pattern called Observer Pattern. In this pattern, one object, called the subject, keeps track of a collection of subscribed observers. When something of interest changes, the subject notifies all the subscribed observers. This is fairly common in web-based applications today.

JavaScript, onMessage

  • In the JavaScript file, add code to handle form submission.
 
const messageForm = document.getElementById("messageForm");
messageForm.addEventListener("submit", function(event) {
    event.preventDefault(); // Do not send WS data to a servlet
  	// Remove any previous error message
  	resetErrorMessage();
  	// Require the user's name
    const userName = document.getElementById("userName").value;
    if(userName === "") {
        displayError("Name is required");
        return;
    }
  	// Require the message form field
    const message = document.getElementById("message").value;
    if(message === "") {
        displayError("Message is required");
        return;
    }

    // Build a JSON object and convert it to a string so it can be sent
    const json = JSON.stringify({"name": userName, "message": message});
  	// Send the message
    sendMessage(json);
  	// Reset the next message so it's ready for the next message
    resetMessageBox();
    // Update the message output box just like we would with an incoming message
});

function resetErrorMessage() {
    const errorText = document.getElementById("errorText");
    errorText.classList.add("d-none"); // Hides the errors message
    errorText.innerText = ""; // Resets the previous error message
}

// this function is called to send a message to the server endpoint
function sendMessage(json) {
    // Only send JSON if the websocket is open
    if(websocket.readyState === websocket.OPEN) {
        websocket.send(json);
    }
}

function resetMessageBox() {
    const message = document.getElementById("message");
    message.value = ""; // removes any existing text from message box
    message.focus(); // moves the cursor's focus to the message form field
}

JavaScript, onMessage

  • When you run the program, test it by using two browsers or private browsing. Open the browser's console. Enter your name and message and submit the form. The Json data should appear in the console of other browsers, not the one that sent the message.

 

Receiving Json

  • Lastly, the JavaScript file needs to establish what to do when a Json message is received.
websocket.onmessage = function (event) {
    updateTextArea(event.data, "in");
};
  • onMessage() describes what to do when a message comes in.  We will add the incoming message to the div textarea.

Receiving Json

  • The updateTextArea function reads the incoming message, parses it into JSON, strips out the contained data, and then appends the data to the bottom of the div text area.
 
/*
 * Update the textarea by appending the supplied text to the text that is
 * already there.  The text shows up as JSON, so it has to be parsed into
 * a JSON object to let us retrieve the data.
 */
function updateMessages(data, inOut) {
  	// Parse the data as JSON so the fields can be accessed
    const json = JSON.parse(data);
    // Use the JSON notation to retrieve the data fields
    const name = json.name;
    const message = json.message;
  	// Build the text to display then show it
    let result = (inOut === "in") ? '<div class="in">' : '<div class="out">';
    result += `<p>${message}</p>`;
    result += `<span>${(inOut === "in") ? name : "Me"}</span>`;
    result += "</div>";
    const messageBox = document.getElementById("messages");
    messageBox.innerHTML += result;
    // Attempt to move the scrolling of the textarea to show the lowest item
    messageBox.scrollTop = messageBox.scrollHeight;
  	// TODO: Extra Credit, only scroll to the bottom if the scrollbar is already at the bottom. Don't scroll down if the user has scrolled up.
}

Receiving Json

  • There are only two fields, the user name and the message text, so they make for a simple JavaScript object. 
  • At the end of the event listener, call the updateTextArea method

 

messageForm.addEventListener("submit", function(event) {
    // Code omitted

    // Update the message output box just like we would with an incoming message
    updateTextArea(json, "out");
}); 
  • We now have a server that allows multiple clients to connect and share data and information. If something changes each client automatically gets notified.
  • When you run the program, test it by using two browsers or private browsing.
  • Study the CSS file to understand how the messages are being styled.
  • Deploy and test your project on Azure. Share your link with others to interact live!

Java 2 Week 11

By Marc Hauschildt

Java 2 Week 11

  • 127