Creating different Websocket Chat Clients in Java
November 9th, 2014 by Micha KopsHaving 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.
Contents
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.
You may download the corresponding vertx-fat-jar or the war file from my repositories on Bitbucket here and there.
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 Bitbucket repository, fork it there or clone it using Git:
git clone https://bitbucket.org/hascode/websocket-chat-client.git
Resources
- Java API for WebSocket JSR 356
- Oracle.com: Concurrency in JavaFX
- Jiji Sasidharan: Java API for Websocket
- Java API for JSON Processing JSR 353
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:
- Creating a Chat Application using Java EE 7, Websockets and GlassFish 4
- Creating a Websocket Chat Application with Vert.x and Java
- Writing a Websocket Chat in Go
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.
Tags: chat, client, fxml, glassfish, java fx, javafx, json, jsr-353, jsr-356, maven, websocket, websockets
November 10th, 2014 at 5:02 pm
That’s pretty cool! I am working on a XMPP chat libary/client and WebSockets are on my todo list, too :-)
November 10th, 2014 at 5:21 pm
Thanks! Your Babbler project looks interesting, perhaps a good alternative to the smack library!
December 14th, 2014 at 6:13 pm
Wow this is way too cool, but is too advanced for me, hope someday i can do and understand all this stuff.
congrats for your work.
January 23rd, 2015 at 11:05 am
The console client app does is not able to send messages. It just connects to the server successfully. But cannot send/receive message. (Other client apps are fine though)
January 23rd, 2015 at 3:57 pm
Hi M,
thanks for your feedback! I’ve just tested the console client app against my GlassFish websocket chat server and it works without a problem – might it be an issue with your setup or security configuration? Please keep me up to date if you’ve got news there!
Cheers,
Micha
January 24th, 2015 at 12:23 am
I guess, something might be wrong the way I modified the code work on my JDK7.0 environment. Here are the changes:
The main method in ConsoleCharClient:
public class ConsoleChatClient {
public static void main(String[] args) {
Console console = System.console();
try {
System.out.println(“Please enter your user name:”);
final String userName = console.readLine();
System.out.println(“Please enter a chat-room name: “);
final String roomName = console.readLine();
System.out.println(“connecting to chat-room ” + roomName);
ChatClientEndpoint clientEndPoint;
clientEndPoint = new ChatClientEndpoint(new URI(“ws://localhost:8080//MyWebServerApp/chat/” + roomName));
// For JDK 8 – commented
// clientEndPoint.addMessageHandler(responseString -> {
// System.out.println(jsonMessageToString(responseString,
// roomName));
// });
// Updated for JDK 7. Do you think this change for JDK 7 compatibily is okay ?
clientEndPoint.addMessageHandler(new MyMessageHandler() {
@Override
public void handleMessage(String responseString) {
System.out.println(jsonMessageToString(responseString, roomName));
}
});
while (true) {
String message = console.readLine();
clientEndPoint.sendMessage(stringToJsonMessage(userName, message));
}
} catch (Exception e) {
e.printStackTrace();
}
// Other methods stringToJsonMessage and jsonMessageToString are unchanged
}
In ChatClientEndpoint, I simply changed the name MessageHandler to MyMessageHandler just to remove confusion (no big deal):
@ClientEndpoint
public class ChatClientEndpoint {
private Session userSession = null;
private MyMessageHandler messageHandler;
public ChatClientEndpoint(final URI endpointURI) {
try {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(this, endpointURI);
System.out.println(“container connectToServer “);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@OnOpen
public void onOpen(final Session session) {
this.userSession = session;
System.out.println(“Session opened ” + session.getId());
}
@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 sendMessage(final String message) {
userSession.getAsyncRemote().sendText(message);
}
public void addMessageHandler(final MyMessageHandler msgHandler) {
messageHandler = msgHandler;
}
public static interface MyMessageHandler {
public void handleMessage(String message);
}
}
The output from console:
<java org.chisty.ConsoleChatClient
Please enter your user name:
chisty
Please enter a chat-room name:
java
connecting to chat-room java
>>
That’s all. The console just waits there.
I am not very keen to use the lambda expression in JDK 8.0 at this moment (since it is still not a stable release). That’s why I needed to change the code to make it JDK 7.0 compatible. Do you think the change/modification is okay (within the main method)?
Thanks.
January 24th, 2015 at 8:39 pm
Hi M,
I’ve added a Java 7 version of the console client in a branch named ‘java7′: https://bitbucket.org/hascode/websocket-chat-client/src/78a3f0accfa612910a0315a503b76b00b6bae700/src/main/java/com/hascode/tutorial/console/ConsoleChatClient.java?at=java7
The only thing that as changed is the lambda expression vs an anonymous class here:
clientEndPoint.addMessageHandler(new ChatClientEndpoint.MessageHandler() {
@Override
public void handleMessage(final String responseString) {
System.out.println(jsonMessageToString(responseString, roomName));
}
});
April 6th, 2015 at 7:52 pm
Hello my friend, thanks for sharing your experiences!
I have a question for you, I am developing a javafx desktop application and I wanna install this on a computer and webstart it in another computer located in different room linked by local network, Is it possible accomplish this using only java websocket, or do I need to use tomcat/glassfish for that?
April 7th, 2015 at 5:17 am
Hi Ronaldo,
you don’t need to use Tomcat or GlassFish but you need to integrate or write something to handle the websocket handshake and the tcp-connection management for your .. every application server vendor has its own implementation of the websocket standard(s) but there are also other third-party libraries for handling web sockets e.g. (I’ve not tried them yet)
https://github.com/TooTallNate/Java-WebSocket
http://bristleback.pl/
As I don’t know all the requirements for your solution I am not sure if using websockets is the best approach as there are other alternatives for client communication. Please feel free to keep me up-to-date with your solution if you like to :)
September 25th, 2015 at 12:01 pm
Awesome… it worked like a charm
September 26th, 2015 at 2:31 pm
Thanks, you’re welcome :)
October 6th, 2015 at 10:26 am
Hi ,
Can you please send me the war file
October 7th, 2015 at 4:34 am
Hi Kutty,
sure, please feel free to download either the Vertx fat-jar or the Java EE 7 war file here and there.
Please keep in mind that you need to deploy the war file on a Java EE 7 compatible application server (it won’t run on a simple Tomcat or Jetty servlet container).
January 7th, 2021 at 5:02 pm
Thank u so much dude you saved my life