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 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.

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“. *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

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.

View screencast on YouTube.

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

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.

Tags: , , , , , , , , , , ,

14 Responses to “Creating different Websocket Chat Clients in Java”

  1. Christian Schudt Says:

    That’s pretty cool! I am working on a XMPP chat libary/client and WebSockets are on my todo list, too :-)

  2. micha kops Says:

    Thanks! Your Babbler project looks interesting, perhaps a good alternative to the smack library!

  3. stefanbanu Says:

    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.

  4. M Says:

    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)

  5. micha kops Says:

    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

  6. M Says:

    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.

  7. micha kops Says:

    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));
    }
    });

  8. Ronaldo Says:

    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?

  9. micha kops Says:

    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 :)

  10. diwakar Says:

    Awesome… it worked like a charm

  11. micha kops Says:

    Thanks, you’re welcome :)

  12. Kutty Says:

    Hi ,
    Can you please send me the war file

  13. micha kops Says:

    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).

  14. sjors Says:

    Thank u so much dude you saved my life

Search
Tags
Categories