See this tutorial of creating a Java websocket server and a Java websocket client.

Configuration
Java Compilation:
Java Runtime:
Web Server:
Maven:
Java Compilation:
Java Runtime:
Web Server:
Gradle:
JDK 21.0.1
OpenJDK 21.0.1
Tomcat 10.1.16
Maven 3.9.6
JDK 21.0.1
OpenJDK 21.0.1
Tomcat 10.1.16
Maven 3.9.6
Socket programs are suitable for 2-way communications, where both the server and client could initiate sending message. Websocket mimic socket on this behavior but is built over web protocol, making it suitable for web application.
Create the following folder structure:
folder structure of Java Websocket server and client
pom.xml: (if using Gradle, replace pom.xml with build.gradle)
parent pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.maxjava</groupId> <artifactId>Websocket</artifactId> <packaging>pom</packaging> <version>1.0</version> <modules> <module>WebSocketServer</module> <module>WebSocketClient</module> </modules> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>21</source> <target>21</target> </configuration> </plugin> </plugins> </build> </project>
WebSocketServer pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>Websocket</artifactId> <groupId>net.maxjava</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <packaging>war</packaging> <artifactId>WebSocketServer</artifactId> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build> </project>
WebSocketClient pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>Websocket</artifactId> <groupId>net.maxjava</groupId> <version>1.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>WebSocketClient</artifactId> <dependencies> <dependency> <groupId>org.glassfish.tyrus.bundles</groupId> <artifactId>tyrus-standalone-client</artifactId> <version>2.1.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
parent build.gradle:
plugins { id 'java' } group 'net.maxjava' version '1.0' repositories { mavenCentral() }
parent settings.gradle:
rootProject.name = 'Websocket' include 'WebSocketClient' include 'WebSocketServer'
WebSocketServer build.gradle:
plugins { id 'java' id 'war' } sourceCompatibility = 21 targetCompatibility = 21 group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { compileOnly group: 'jakarta.platform', name: 'jakarta.jakartaee-api', version: '10.0.0' }
WebSocketClient build.gradle:
plugins { id 'java' id 'com.github.johnrengelman.shadow' version "8.1.1" } sourceCompatibility = 21 targetCompatibility = 21 group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation group: 'org.glassfish.tyrus.bundles', name: 'tyrus-standalone-client', version: '2.1.3' }
Some notes about parent pom.xml:
  • maven-compiler-plugin is for compiling the code using source version 21 and target version 21
  • A war artifact will be created for deployment to Java web server
Some notes about WebSocketServer pom.xml:
  • jakarta.jakartaee-api is required for building the artifact only, not required to be included in the artifact, as the web server like Tomcat will provide the web socket implementation jars
  • maven-war-plugin is to make web.xml optional, which will not be included here
Some notes about WebSocketClient pom.xml:
  • tyrus-standalone-client is required to run as a stand-alone web socket client
  • maven-shade-plugin is used to pack all the required jars into a single jar
Some notes about WebSocketServer build.gradle:
  • jakarta.jakartaee-api is required for building the artifact only, not required to be included in the artifact, as the web server like Tomcat will provide the web socket implementation jars
  • sourceCompatibility and targetCompatibility is for compiling the code using source version 21 and target version 21
  • A war artifact will be created for deployment to servlet containers
Some notes about WebSocketClient pom.xml:
  • tyrus-standalone-client is required to run as a stand-alone web socket client
  • sourceCompatibility and targetCompatibility is for compiling the code using source version 21 and target version 21
  • com.github.johnrengelman.shadow is used to pack all the required jars into a single jar
WebSocketServer is the web socket server packaged in war, ready to be deployed in any Java web server
MyServerConfigurator.java:
package configurator; import jakarta.websocket.HandshakeResponse; import jakarta.websocket.server.HandshakeRequest; import jakarta.websocket.server.ServerEndpointConfig; public class MyServerConfigurator extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { if (request.getHeaders().containsKey("Authorization")) { config.getUserProperties().put("Authorization", request.getHeaders().get("Authorization").get(0)); } if (request.getHeaders().containsKey("CustomerName")) { config.getUserProperties().put("CustomerName", request.getHeaders().get("CustomerName").get(0)); } } }
Some notes about MyServerConfigurator.java:
  • MyServerConfigurator extends ServerEndpointConfig.Configurator which allows it to be used as the configurator for the web socket server end point
  • It reads the request header "Authorization" and "CustomerName" and put them into a map which could be read later
MyWebsocketServer.java:
package websocket; import configurator.MyServerConfigurator; import jakarta.websocket.*; import jakarta.websocket.server.ServerEndpoint; import java.io.IOException; @ServerEndpoint(value = "/mywebsocket", configurator = MyServerConfigurator.class) public class MyWebsocketServer { private String customerName = null; @OnOpen public void onOpen(Session session, EndpointConfig endpointConfig) { String authorization = (String)endpointConfig.getUserProperties().get("Authorization"); if (!"my_code".equals(authorization)) { try { CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "Not Authorized"); session.close(closeReason); } catch (IOException ioe) { ioe.printStackTrace(); } } customerName = (String)endpointConfig.getUserProperties().get("CustomerName"); System.out.println("Session " + session.getId() + " opened for " + customerName); } @OnMessage(maxMessageSize = 1048576) public void onMessage(String message, Session session) { String reply = "Hi " + customerName + ", thanks for your message - " + message; try { session.getBasicRemote().sendText(reply); } catch (IOException ioe) { ioe.printStackTrace(); } } @OnClose public void onClose(Session session) { System.out.println("Session " + session.getId() + " closed"); } @OnError public void onError(Throwable exception, Session session) { System.out.println("Session " + session.getId() + " had error:"); exception.printStackTrace(); } }
Some notes about MyWebsocketServer.java:
  • Every time a connection request coming, the opOpen function will be called, and from there the headers can be read from the endpointConfig
  • OnMessage will be called every time a message received. 1048576 is in byte and is the maximum message size could be received
  • session.getBasicRemote().sendText() is used to send the message back to the client
  • All the variables defined in the class level are thread-safe to the session. Like in the example here we have the String variable "customerName" being assigned when the session is opened. Its value would not be impacted by other sessions.
WebSocketClient is a stand-alone application
  • Running by using: java -cp target/WebSocketClient-1.0.jar MainClass
  • It will take a text from the prompt and send to the web socket server. The server will send back to the text, which the client will display
MyClientConfigurator.java:
package configurator; import jakarta.websocket.ClientEndpointConfig; import java.util.HashMap; import java.util.List; import java.util.Map; public class MyClientConfigurator extends ClientEndpointConfig.Configurator { private Map<String, List<String>> headers = new HashMap<>(); public void setHeaders(Map<String, List<String>> headers) { this.headers = headers; } @Override public void beforeRequest(Map<String, List<String>> headers){ for (String key : this.headers.keySet()) { headers.put(key, this.headers.get(key)); } } }
Some notes about MyClientConfigurator.java:
  • MyClientConfigurator overrides the beforeRequest function to add custom values to the headers
MyWebSocketClient.java:
package websocket; import configurator.MyClientConfigurator; import jakarta.websocket.*; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; public class MyWebSocketClient extends Endpoint { private final String SERVER_URL = "ws://localhost:8080/WebSocketServer-1.0/mywebsocket"; private Session session = null; public void connect(String authorization, String customerName) { Map<String, List<String>> headers = new HashMap<>(); headers.put("Authorization", List.of(authorization)); headers.put("CustomerName", List.of(customerName)); MyClientConfigurator myClientConfigurator = new MyClientConfigurator(); myClientConfigurator.setHeaders(headers); ClientEndpointConfig clientEndpointConfig = ClientEndpointConfig.Builder.create().configurator(myClientConfigurator).build(); WebSocketContainer container = ContainerProvider.getWebSocketContainer(); try { container.connectToServer(this, clientEndpointConfig, new URI(SERVER_URL)); } catch (Exception e) { e.printStackTrace(); } } public void disconnect() { try { this.session.close(); } catch (Exception e) { e.printStackTrace(); } } public void sendText(String text) { try { this.session.getBasicRemote().sendText(text); } catch (Exception e) { e.printStackTrace(); } } @Override public void onOpen(final Session session, EndpointConfig endpointConfig) { this.session = session; session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String text) { System.out.println("!!" + text + "!!"); } }); } @Override public void onClose(Session userSession, CloseReason reason) { System.out.println("Connection closed with reason: " + reason.getReasonPhrase()); } @Override public void onError(Session session, Throwable t) { System.out.println("Had error:"); t.printStackTrace(); } }
Some notes about NoDuplicationValidator.java:
  • The connect(String authorization, String customerName) function is used to create a web socket connection to the server, with the 2 parameters sent as headers by using MyClientConfigurator
  • The sendText(String text) function is for sending String to the server once the session is opened
MainClass.java:
import websocket.MyWebSocketClient; import java.io.BufferedReader; import java.io.InputStreamReader; public class MainClass { public static void main(String[] args) { MyWebSocketClient myWebSocketClient = new MyWebSocketClient(); myWebSocketClient.connect("my_code","Sam"); BufferedReader br = new BufferedReader((new InputStreamReader(System.in))); try { while (true) { System.out.println("Please enter command ('Send' or 'End')"); String command = br.readLine(); if (command.startsWith("Send")) { System.out.println("What to send?"); command = br.readLine(); myWebSocketClient.sendText(command); // A little sleep to make sure the ack message from the server will be received and printed out before going for next loop. // Synchronization code like wait() and notify() could be used to precisely control the event flow if needed. Thread.sleep(2000); } else if (command.startsWith("End")) { myWebSocketClient.disconnect(); break; } else { System.out.println("Please enter 'Send' or 'End' only"); } } } catch (Exception e) { e.printStackTrace(); } } }
Some notes about MainClass.java:
  • It is the main class that initiate MyWebSocketClient and call its functions
  • An infinite loop is used to take command line input, either for sending the entered text to the web socket server or ends the application
Example:
Please enter command ('Send' or 'End') Send What to send? Good to see you !!Hi Sam, thanks for your message - Good to see you!! Please enter command ('Send' or 'End') End Connection closed with reason: