WebSocket API

This commit is contained in:
2023-03-14 16:22:50 +01:00
parent b6dba62fa4
commit fd828d600e
16 changed files with 910 additions and 3 deletions

View File

@@ -0,0 +1,117 @@
package fr.pandacube.lib.ws.client;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.ThrowableUtil;
import fr.pandacube.lib.ws.AbstractWS;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocket.Listener;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicReference;
// TODO implement auto-reconnect
/**
* Minimal implementation of a Websocket client endpoint using the java.net.http Websocket API.
*/
public abstract class AbstractClientWS implements AbstractWS {
private final URI uri;
private boolean autoReconnect;
private final AtomicReference<WebSocket> socket = new AtomicReference<>();
private final Listener receiveListener = new Listener() {
@Override
public void onOpen(WebSocket webSocket) {
AbstractClientWS.this.onConnect();
Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
AbstractClientWS.this.handleReceivedMessage(data.toString());
return Listener.super.onText(webSocket, data, last);
}
@Override
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
AbstractClientWS.this.handleReceivedBinary();
return Listener.super.onBinary(webSocket, data, last);
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
AbstractClientWS.this.onClose(statusCode, reason);
return Listener.super.onClose(webSocket, statusCode, reason);
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
AbstractClientWS.this.onError(error);
Listener.super.onError(webSocket, error);
}
};
/**
* Creates a new Websocket client that connect to the provided url.
* @param uri the destination endpoint.
* @param autoReconnect if this websocket should automatically try to reconnect when disconnected.
* @throws URISyntaxException if the provided URI is invalid.
*/
public AbstractClientWS(String uri, boolean autoReconnect) throws URISyntaxException {
this.uri = new URI(uri);
this.autoReconnect = autoReconnect;
if (autoReconnect) {
Log.warning("Websocket client auto-reconnect is not yet implemented.");
}
connect();
}
private void connect() {
synchronized (socket) {
socket.set(HttpClient.newHttpClient()
.newWebSocketBuilder()
.connectTimeout(Duration.ofSeconds(2))
.buildAsync(uri, receiveListener)
.join()
);
}
}
@Override
public final void sendString(String message) throws IOException {
try {
synchronized (socket) {
socket.get().sendText(message, true).join();
}
} catch (CompletionException ce) {
if (ce.getCause() instanceof IOException ioe)
throw ioe;
throw ThrowableUtil.uncheck(ce.getCause(), false);
}
}
@Override
public String getRemoteIdentifier() {
return uri.toString();
}
@Override
public final void sendClose(int code, String reason) throws IOException {
synchronized (socket) {
autoReconnect = false; // if we ask for closing connection, dont reconnect automatically
socket.get().sendClose(code, reason);
}
}
}

View File

@@ -0,0 +1,97 @@
package fr.pandacube.lib.ws.client;
import fr.pandacube.lib.ws.payloads.ErrorPayload;
import fr.pandacube.lib.ws.payloads.LoginPayload;
import fr.pandacube.lib.ws.payloads.LoginSucceedPayload;
import fr.pandacube.lib.ws.payloads.Payload;
import java.net.URISyntaxException;
/**
* Websocket client that implements the login logic with a key protected server endpoint.
*/
public abstract class KeyProtectedClientWS extends AbstractClientWS {
private final String key;
private boolean loginSucceed = false;
/**
* Creates a new Websocket client that connect to the provided url and uses the provided key.
* @param uri the destination endpoint.
* @param autoReconnect if this websocket should automatically try to reconnect when disconnected.
* @param key the login key.
* @throws URISyntaxException if the provided URI is invalid.
*/
public KeyProtectedClientWS(String uri, boolean autoReconnect, String key) throws URISyntaxException {
super(uri, autoReconnect);
this.key = key;
}
@Override
public final void onConnect() {
trySendAsJson(new LoginPayload(key));
}
@Override
public final void onReceivePayload(Payload payload) {
if (loginSucceed) {
onReceivePayloadLoggedIn(payload);
}
else if (payload instanceof LoginSucceedPayload) {
loginSucceed = true;
onLoginSucceed();
}
else if (payload instanceof ErrorPayload err){
logError("Received ErrorPayload instead of LoginSuccessPayload: " + err.message, err.throwable);
trySendClose();
}
else {
logError("Received unexpected Payload instead of LoginSuccessPayload: " + payload.getClass().getSimpleName());
trySendClose();
}
}
@Override
public final void onClose(int code, String reason) {
if (loginSucceed) {
loginSucceed = false;
onCloseLoggedIn(code, reason);
}
}
@Override
public final void onError(Throwable cause) {
if (loginSucceed) {
loginSucceed = false;
onErrorLoggedIn(cause);
}
}
/**
* Called when this Websocket is succesfully logged in to the server.
*/
public abstract void onLoginSucceed();
/**
* Called on reception of a valid payload when already logged in.
* @param payload the received payload.
*/
public abstract void onReceivePayloadLoggedIn(Payload payload);
/**
* Called on reception of a websocket Close packet, only if this client is already logged in.
* The connection is closed after this method call.
* @param code the close code. 1000 for a normal closure.
* @param reason the close reason.
*/
public abstract void onCloseLoggedIn(int code, String reason);
/**
* Called when an error occurs with the websocket API, only if this client is already logged in.
* The connection is already closed when this method is called.
* @param cause the error cause.
*/
public abstract void onErrorLoggedIn(Throwable cause);
}