Creating different Websocket Chat Clients in Java

November 9th, 2014 by

Having written two articles about different websocket-based chat server implementations in Java, I was recently asked how an implementation of the client side would look like in Java.

That’s why I added this article to demonstrate how to create a websocket chat client aplications within a few steps with the Java API for Websocket.

In the following tutorial, we’re going to write a text-based chat client for the console first and afterwards we’re going to program a chat client with a graphical user interface, implemented in JavaFX.

JavaFX Chat Client

JavaFX Chat Client

 

Chat Server

To keep it easy we’re using a pre-built chat-server from one of my articles – so on the one hand, there is a solution using vert.x from my article “Creating a Websocket Chat Application with Vert.x and Java” or on the other hand a solution based on Java EE 7 with an embedded GlassFish server from my tutorial “Creating a Chat Application using Java EE 7, Websockets and GlassFish 4“.

Which one we chose does not matter as both variants allow us to start a full-blown websocket chat server with only Java and Maven as prerequisites.

Java API for WebSockets JSR 356

The Java API for Websockets specifies not only the server- but also the client side API to handle a websocket connection.

I found a nice tutorial, written by Jiji Sasidharan that explains the client implementation in detail here.

Dependencies

The following dependencies are needed for the following examples. The important ones areĀ  javax.websocket-client-api for the API and the tyrus dependencies for the implementation.

The two last dependencies are needed because the server implementation of our chat demands a specific JSON structure and we’re using the Java API for JSON Processing aka JSR 353 here to handle this.

<dependency>
	<groupId>javax.websocket</groupId>
	<artifactId>javax.websocket-client-api</artifactId>
	<version>1.0</version>
</dependency>
<dependency>
	<groupId>org.glassfish.tyrus</groupId>
	<artifactId>tyrus-client</artifactId>
	<version>1.1</version>
</dependency>
<dependency>
	<groupId>org.glassfish.tyrus</groupId>
	<artifactId>tyrus-container-grizzly</artifactId>
	<version>1.1</version>
</dependency>
<dependency>
	<groupId>javax.json</groupId>
	<artifactId>javax.json-api</artifactId>
	<version>1.0</version>
</dependency>
<dependency>
	<groupId>org.glassfish</groupId>
	<artifactId>javax.json</artifactId>
	<version>1.0.1</version>
</dependency>

Client Endpoint

This is our client endpoint, marked as an endpoint by the javax.websocket.ClientEndpoint annotation.

There are several lifecycle annotations that allow us to listen for specific states of our designated connection.

Our implementation accepts a message handler for incoming messages.

package com.hascode.tutorial;
 
import java.net.URI;
 
import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.ContainerProvider;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;
 
@ClientEndpoint
public class ChatClientEndpoint {
	private Session userSession = null;
	private MessageHandler messageHandler;
 
	public ChatClientEndpoint(final URI endpointURI) {
		try {
			WebSocketContainer container = ContainerProvider.getWebSocketContainer();
			container.connectToServer(this, endpointURI);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
 
	@OnOpen
	public void onOpen(final Session userSession) {
		this.userSession = userSession;
	}
 
	@OnClose
	public void onClose(final Session userSession, final CloseReason reason) {
		this.userSession = null;
	}
 
	@OnMessage
	public void onMessage(final String message) {
		if (messageHandler != null) {
			messageHandler.handleMessage(message);
		}
	}
 
	public void addMessageHandler(final MessageHandler msgHandler) {
		messageHandler = msgHandler;
	}
 
	public void sendMessage(final String message) {
		userSession.getAsyncRemote().sendText(message);
	}
 
	public static interface MessageHandler {
		public void handleMessage(String message);
	}
}

Using the Endpoint

We’re now ready to use our endpoint by initiating a new endpoint with the chat server’s target URL.

ChatClientEndpoint clientEndPoint = new ChatClientEndpoint(new URI("ws://url:port/path"));
 
// handler for incoming messages
clientEndPoint.addMessageHandler(message -> {
	// do stuff ...
});
 
// send message
clientEndPoint.sendMessage(newMessage);

This code is used for both implementations – the console chat client as well as the GUI chat client.

Console Chat Client

Let’s first start with the simple solution – a non-graphical console chat application.

The application asks for our user name and the chat room’s name and initializes a connection to the chat server.

Afterwards we’re able to type our messages in the console and read the response from the chat room.

One-Class-Application

package com.hascode.tutorial.console;
 
import java.io.Console;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
 
import javax.json.Json;
import javax.json.JsonObject;
 
import com.hascode.tutorial.ChatClientEndpoint;
 
public class ConsoleChatClient {
	public static void main(final String[] args) throws InterruptedException, URISyntaxException {
		Console console = System.console();
		final String userName = console.readLine("Please enter your user name: ");
		final String roomName = console.readLine("Please enter a chat-room name: ");
		System.out.println("connecting to chat-room " + roomName);
 
		final ChatClientEndpoint clientEndPoint = new ChatClientEndpoint(new URI("ws://0.0.0.0:8080/hascode/chat/" + roomName));
		clientEndPoint.addMessageHandler(responseString -> {
			System.out.println(jsonMessageToString(responseString, roomName));
		});
 
		while (true) {
			String message = console.readLine();
			clientEndPoint.sendMessage(stringToJsonMessage(userName, message));
		}
	}
 
	private static String stringToJsonMessage(final String user, final String message) {
		return Json.createObjectBuilder().add("sender", user).add("message", message).build().toString();
	}
 
	private static String jsonMessageToString(final String response, final String roomName) {
		JsonObject root = Json.createReader(new StringReader(response)).readObject();
		String message = root.getString("message");
		String sender = root.getString("sender");
		String received = root.getString("received");
		return String.format("%s@%s: %s [%s]", sender, roomName, message, received);
	}
 
}

The two helper methods stringToJsonMessage and jsonMessageToString are needed to convert our in- and output into the format of the chat server.

They are later used again for the graphical chat client.

Running the Console Application

The Exec Maven Plugin lets us run the console application directly from the project directory:

$ mvn exec:java -Dexec.mainClass=com.hascode.tutorial.console.ConsoleChatClient
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building websocket-chat-client 1.0.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.3.1:java (default-cli) @ websocket-chat-client ---
[WARNING] Warning: killAfter is now deprecated. Do you need it ? Please comment on MEXEC-6.
Please enter your user name: tim
Please enter a chat-room name: java
connecting to chat-room java
hey there
tim@java: hey there [Sun Nov 09 18:54:50 CET 2014]

Screenshot

That’s what our console chat looks like in a terminal

Console Chat Client

Console Chat Client

GUI Chat Client with JavaFX

Now we’re ready for some more eye candy and that’s why we’re bringing JavaFX into play!

Our graphical chat client consists of four parts: the application starter, the model, the controller and an externalized template in FXML markup.

Model

The model is our representation of specific states in our application and thanks to the powerful one-way or two-way binding capabilities of JavaFX it is really easy to bind properties and states of other components to this model.

The model itself is a simple POJO, the interesting part is the observable API in JavaFX..

package com.hascode.tutorial.gui;
 
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
 
public class ChatModel {
	public final BooleanProperty connected = new SimpleBooleanProperty(false);
	public final BooleanProperty readyToChat = new SimpleBooleanProperty(false);
	public final ObservableList<String> chatHistory = FXCollections.observableArrayList();
	public final StringProperty currentMessage = new SimpleStringProperty();
	public final StringProperty userName = new SimpleStringProperty();
	public final StringProperty roomName = new SimpleStringProperty();
}

Controller

The controller is bound to elements from the FXML template, we’re referencing them using the @FXML annotation (references an element with fx:id).

In addition, the controller binds different events, and model states to specific properties of our UI components.

package com.hascode.tutorial.gui;
 
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.util.ResourceBundle;
 
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
 
import javax.json.Json;
import javax.json.JsonObject;
 
import com.hascode.tutorial.ChatClientEndpoint;
 
public class ChatController implements Initializable {
	@FXML
	private MenuItem exitItem;
 
	@FXML
	private ChoiceBox<String> roomSelection;
 
	@FXML
	private Button connectButton;
 
	@FXML
	private TextField userNameTextfield;
 
	@FXML
	private TextField messageTextField;
 
	@FXML
	private Button chatButton;
 
	@FXML
	private MenuItem aboutMenuItem;
 
	@FXML
	private ListView<String> chatListView;
 
	private final ChatModel model = new ChatModel();
 
	private ChatClientEndpoint clientEndPoint;
 
	@Override
	public void initialize(final URL url, final ResourceBundle bundle) {
		exitItem.setOnAction(e -> Platform.exit());
		roomSelection.setItems(FXCollections.observableArrayList("arduino", "java", "groovy", "scala"));
		roomSelection.getSelectionModel().select(1);
		model.userName.bindBidirectional(userNameTextfield.textProperty());
		model.roomName.bind(roomSelection.getSelectionModel().selectedItemProperty());
		model.readyToChat.bind(model.userName.isNotEmpty().and(roomSelection.selectionModelProperty().isNotNull()));
		chatButton.disableProperty().bind(model.connected.not());
		messageTextField.disableProperty().bind(model.connected.not());
		messageTextField.textProperty().bindBidirectional(model.currentMessage);
		connectButton.disableProperty().bind(model.readyToChat.not());
		chatListView.setItems(model.chatHistory);
		messageTextField.setOnAction(event -> {
			handleSendMessage();
		});
		chatButton.setOnAction(evt -> {
			handleSendMessage();
		});
		connectButton.setOnAction(evt -> {
			try {
				clientEndPoint = new ChatClientEndpoint(new URI("ws://0.0.0.0:8080/hascode/chat/" + model.roomName.get()));
				clientEndPoint.addMessageHandler(responseString -> {
					Platform.runLater(() -> {
						model.chatHistory.add(jsonMessageToString(responseString, model.roomName.get()));
					});
				});
				model.connected.set(true);
			} catch (Exception e) {
				showDialog("Error: " + e.getMessage());
			}
 
		});
		aboutMenuItem.setOnAction(event -> {
			showDialog("Example websocket chat bot written in JavaFX.\n\n Please feel free to visit my blog at www.hascode.com for the full tutorial!\n\n2014 Micha Kops");
		});
	}
 
	private void handleSendMessage() {
		clientEndPoint.sendMessage(stringToJsonMessage(model.userName.get(), model.currentMessage.get()));
		model.currentMessage.set("");
		messageTextField.requestFocus();
	}
 
	private void showDialog(final String message) {
		Stage dialogStage = new Stage();
		dialogStage.initModality(Modality.WINDOW_MODAL);
		VBox box = new VBox();
		box.getChildren().addAll(new Label(message));
		box.setAlignment(Pos.CENTER);
		box.setPadding(new Insets(5));
		dialogStage.setScene(new Scene(box));
		dialogStage.show();
	}
}

FXML Template

This is our externalized template named chat.fxml in src/main/resources/template.

The template is bound to our controller via fx:controller attribute in the root element.

<?xml version="1.0" encoding="UTF-8"?>
 
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
 
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="500.0" prefWidth="800.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.hascode.tutorial.gui.ChatController">
   <top>
      <MenuBar BorderPane.alignment="CENTER">
        <menus>
          <Menu mnemonicParsing="false" text="File">
            <items>
              <MenuItem fx:id="exitItem" mnemonicParsing="false" text="Exit" />
            </items>
          </Menu>
            <Menu mnemonicParsing="false" text="?">
              <items>
                <MenuItem mnemonicParsing="false" text="About" fx:id="aboutMenuItem"/>
              </items>
            </Menu>
        </menus>
      </MenuBar>
   </top>
   <left>
      <VBox prefHeight="371.0" prefWidth="125.0" BorderPane.alignment="CENTER">
         <children>
            <Separator orientation="VERTICAL" prefHeight="50.0" visible="false" />
            <Label text="Username" />
            <TextField fx:id="userNameTextfield" />
            <Separator orientation="VERTICAL" prefHeight="50.0" visible="false" />
            <Label text="Chatroom" />
            <ChoiceBox fx:id="roomSelection" prefWidth="150.0" />
            <Separator orientation="VERTICAL" prefHeight="50.0" visible="false" />
            <Button fx:id="connectButton" mnemonicParsing="false" text="Connect" />
         </children>
         <padding>
            <Insets left="10.0" right="10.0" />
         </padding>
         <BorderPane.margin>
            <Insets />
         </BorderPane.margin>
      </VBox>
   </left>
   <center>
      <ListView prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" fx:id="chatListView"/>
   </center>
   <bottom>
      <VBox prefHeight="83.0" prefWidth="800.0" BorderPane.alignment="CENTER">
         <children>
            <HBox prefHeight="100.0" prefWidth="200.0">
               <children>
                  <TextField fx:id="messageTextField" prefHeight="40.0" prefWidth="281.0" />
                  <Button fx:id="chatButton" mnemonicParsing="false" prefHeight="40.0" prefWidth="60.0" text="Send">
                     <HBox.margin>
                        <Insets left="5.0" />
                     </HBox.margin></Button>
               </children>
               <VBox.margin>
                  <Insets left="126.0" top="10.0" />
               </VBox.margin>
            </HBox>
            <Label prefHeight="33.0" prefWidth="177.0" text="Micha Kops - www.hascode.com" textFill="#9e9e9e">
               <font>
                  <Font size="11.0" />
               </font>
               <VBox.margin>
                  <Insets left="300.0" />
               </VBox.margin>
            </Label>
         </children>
         <padding>
            <Insets top="10.0" />
         </padding>
      </VBox>
   </bottom>
   <right>
      <Pane prefHeight="334.0" prefWidth="18.0" BorderPane.alignment="CENTER" />
   </right>
</BorderPane>

Scene Builder

The Scene Builder allows us to compose our chat application layout with ease.

Downloads can be found at the Oracle.com website here.

JavaFX Scene Builder

JavaFX Scene Builder

Application Starter

This is the final part to start our JavaFX application.

package com.hascode.tutorial.gui;
 
import java.io.IOException;
 
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
 
public class GuiChatClient extends Application {
	private static final String VIEW_GAME = "/template/chat.fxml";
 
	@Override
	public void start(final Stage stage) throws Exception {
		initGui(stage);
	}
 
	private void initGui(final Stage stage) throws IOException {
		Parent root = FXMLLoader.load(getClass().getResource(VIEW_GAME));
		Scene scene = new Scene(root);
		scene.setFill(Color.GRAY);
		stage.setScene(scene);
		stage.setTitle("hasCode.com - Websocket Chat Client");
		stage.show();
	}
 
	public static void main(final String... args) {
		Application.launch(args);
	}
 
}

Running the GUI Application

Running our graphical chat client is easy – the fastest way is to use the Exec Plugin for Maven here:

mvn exec:java -Dexec.mainClass=com.hascode.tutorial.gui.GuiChatClient

or if packaging the application first is the preferred way:

mvn clean package && java -cp target/websocket-chat-client-1.0.0.jar com.hascode.tutorial.gui.GuiChatClient

Screenshot

This is what our JavaFX chat client looks like

JavaFX Chat Client

JavaFX Chat Client

Chat Clients in Action

This is an example of an interaction of three chat users – one using a browser, the second using the console chat client and the last one using the graphical chat application.

Troubleshooting

  • java.lang.IllegalStateException: Not on FX application thread: JavaFX requires you to handle work in the JavaFX thread. Either wrap your unit of work in a JavaFX Task or use the simpler version like this:
    Platform.runLater(() ->
     // do work
    );

Tutorial Sources

Please feel free to download the tutorial sources fromĀ my Bitbucket repository, fork it there or clone it using Git:

git clone https://bitbucket.org/hascode/websocket-chat-client.git

Resources

Search
Tags
Categories