Minimal Java library for communication between server and web browser, designed as a drop-in replacement for the java.net
ServerSocket
and Socket
classes
- Drop-in replacement for
java.net
ServerSocket
andSocket
classes, able to interact with regularjava.net
Socket
clients and web browsers - Automatic HTTP Upgrade handling to WebSocket protocol upon web browser handshake
- Automatic encoding and decoding of traffic between server and web browser client after WebSocket handshaking with the help of background processes
- Implement various Opcodes for frames other than text
- Implement sending data in individual frames instead of all at once
- Encode traffic from server to client (only encoding from client to server is mandatory)
As of version 1.0-3
the minimal-jwebsocket
library is hosted in the Maven Central OSSRH repository. Installation can be done by including the following dependency in your pom.xml
file:
<dependency>
<groupId>com.antonowycz</groupId>
<artifactId>minimal-jwebsocket</artifactId>
<version>1.0-3</version>
</dependency>
When building your application, you can replace the java.net.ServerSocket
class with the com.antonowycz.websocket.ServerSocket
class and the java.net.Socket
class with the com.antonowycz.websocket.Socket
class:
import com.antonowycz.websocket.ServerSocket; // Extension of java.net.ServerSocket
import com.antonowycz.websocket.Socket; // Extension of java.net.Socket
Using the library to communicate with web browsers is designed to be as straight-forward as interacting normal java.net
Socket
clients. Below is a simple example server allowing only once consecutive client to connect at a time, to handle more than one client at a time implement a client handler class with a thread for every client.
...
public class TestServer implements Runnable {
...
@Override
public void run() {
while (!socket.isClosed()) {
// Accept a client
try (Socket client = socket.accept();
var br = new BufferedReader(new InputStreamReader(client.getInputStream()));
var pw = new PrintWriter(client.getOutputStream(), true);
var sc = new Scanner(br)) {
// After creating a buffered reader, scanner and printerwriter for the client
while (!socket.isClosed()) {
String input;
if ((input = sc.next()) == null) break; // Read input from scanner
switch (input) {
case "ping":
pw.println("pong"); // Write output to printwriter
break;
default:
System.out.println("Received: " + input);
}
}
} catch (NoSuchElementException e) {
System.out.println("Client disconnected"); // Scanner is empty -- client disconnected
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
...
}
The code above works both when the client on the receiving end is a regular java.net.Socket
class, or a web browser with the RFC 6455 protocol implemented, e.g. Chrome 16+, Firefox 11+, IE 10+.
The following unit test from src/test/java/ServerTest.class
ensures backwards compatibility between the extended websocket.Socket
class and the java.net.Socket
class:
@Test
public void backwardsCompatibleResponseTest() {
try (var client = new java.net.Socket(InetAddress.getLocalHost(), server.getPort());
var br = new BufferedReader(new InputStreamReader(client.getInputStream()));
var pw = new PrintWriter(new OutputStreamWriter(client.getOutputStream()), true)) {
pw.println("ping");
assertEquals("pong", br.readLine());
pw.println("a".repeat(126));
assertEquals("b".repeat(126), br.readLine());
pw.println("a".repeat(65536));
assertEquals("b".repeat(65536), br.readLine());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
The web browser communication is tested through the Selenium UI testing toolkit, using the following web page:
<p id="response"></p>
<button class="websocket-communication disabled" id="ping" onclick="ws.send('ping');">ping</button>
<button class="websocket-communication disabled" id="long2b" onclick="ws.send('a'.repeat(126));">long2b</button>
<button class="websocket-communication disabled" id="long8b" onclick="ws.send('a'.repeat(65536));">long8b</button>
<script>
function displayOutput(output) {
document.getElementById('response').innerHTML = output;
}
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = function(message) {
displayOutput(message.data);
switch (message.data) {
case 'ping': ws.send('pong'); break;
}
}
ws.onopen = function() {
Array.from(document.querySelectorAll('.websocket-communication.disabled')).forEach((el) => el.classList.remove('disabled'));
}
</script>
The following unit test from src/test/ServerTest.class
ensures the handshake and bidirectional communication between the web server and server are working correctly:
@Test
public void websocketResponseTest() throws InterruptedException {
Thread.sleep(2000);
FirefoxOptions options = new FirefoxOptions();
options.addArguments("--headless");
WebDriver driver = new FirefoxDriver(options);
driver.get(WS_HTML_CLIENT);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(webDriver -> ((JavascriptExecutor) webDriver).executeScript("return document.readyState").equals("complete"));
wait.until(ExpectedConditions.elementToBeClickable(By.id("ping"))).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("response"), "pong"));
driver.findElement(By.id("long2b")).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("response"), "b".repeat(126)));
driver.findElement(By.id("long8b")).click();
wait.until(ExpectedConditions.textToBePresentInElementLocated(By.id("response"), "b".repeat(65536)));
driver.quit();
}