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 applications 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.
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". Update: In between I have also written an implementation in Go: "Writing a Websocket Chat in Go".
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
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.
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
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 GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/websocket-chat-client.git
Resources
Websocket Articles of Mine
Please feel free to have a look at other articles of mine about websocket implementations in different languages and using different libraries/frameworks:
Article Updates
-
2018-06-01: Embedded YouTube video removed (GDPR/DSGVO).
-
2016-10-29: Typos fixed, link to Go implementation of websocket chat added.
-
2015-10-07: Direct downloads for the chat server either as Vertx fat-jar or as Java EE 7 war added.