Connection
Während der Nutzung des jadice web toolkit durch einen Anwender kommuniziert der im Browser ausgeführte Javascript-Client in unterschiedlichen Situationen mit dem im Application Server ausgeführten Java-Backend.
Diese Kommunikation erfolgt zum einen synchron über das Request-Response-basierte HTTP-Protokoll. Beispielsweise findet das Bootstrapping, also der initiale Download des Javascript-Clients über synchrones HTTP statt. Sobald der Anwender zu Beginn die URL der HTML-Datei aufruft, die das jadice web toolkit enthält, wird diese HTML-Datei und im Anschluss insbesondere auch der für den genutzten Browser passende Javascript-Client heruntergeladen.
Wie bei Single-Page-Webanwendungen üblich kommunizieren Client und Server daneben aber auch asynchron miteinander, um die gewünschte Usability zu erreichen. Beispielsweise wird beim Laden eines Dokuments durch den Benutzer die ID des Dokuments vom Client an den Server übertragen. Der Server lädt das zur ID passende Dokument daraufhin. Pro erfolgreich geladener Seite antwortet der Server mit Strukturinformationen für diese Seite und lädt parallel dazu die nächste Seite. Der Client kann damit die erste Seite des Dokuments schon anzeigen, während die Folgeseiten noch vom Server geladen werden.
Diese asynchrone Kommunikation wird durch eine eigene Transportschicht realisiert. Im Unterschied zu reinem HTTP, bei dem eine Kommunikation ausschließlich vom Client initiiert werden kann und der Server diese Anfrage ausschließlich mit einer einzelnen Nachricht beantworten kann, ermöglicht es die Transportschicht dem Server eine Anfrage des Clients mit mehreren Nachrichten zu beantworten (u.a. notwendig für den Ladevorgang von Dokumenten, siehe oben) und außerdem auch eine vom Server initiierte Kommunikation. Um diese verbindungsorientierte, asynchrone Kommunikation zu realisieren bietet die Transportschicht unterschiedliche Protokolle an. Abhängig von der kundenindividuellen Umgebung kann die Transportschicht unter Berücksichtigung der Fähigkeiten von Browser und Application Server das dafür passende Protokoll wählen.
Client und Server kommunizieren in folgenden Situationen über die Transportschicht miteinander:
-
Beim Laden eines Dokuments, in dessen Rahmen der Client die ID des zu ladenden Dokuments an den Server schickt und der Server mit Strukturinformationen pro Seite antwortet.
-
Beim Ausführen einer ServerOperation, in dessen Rahmen der Client die Parameter der ServerOperation an den Server schickt und der Server mit mehreren Zwischenergebnissen antwortet.
-
Beim Markieren von Text, in dessen Rahmen der Client die ID der zugehörigen Seite an den Server schickt und der Server mit dem Text antwortet, der sich auf dieser Seite befindet.
-
Beim Ausführen einer Volltextsuche innerhalb eines Dokuments, in dessen Rahmen der Client den zu suchenden Text an den Server schickt und der Server mit den gefundenen Textpassagen und deren Positionen im Dokument antwortet.
-
Beim Download eines Annotationsprofils, in dessen Rahmen der Client den Namen des Annotationsprofils an den Server schickt und der Server mit dem zugehörigen Profil antwortet.
Die Transportschicht hält dauerhaft eine Verbindung zum Server aufrecht, damit dieser zu jeder Zeit Nachrichten an den Client senden kann. Die unterschiedlichen Browser limitieren jedoch die Anzahl der gleichzeitig offenen Verbindungen zu einem Server. Dies gilt nicht nur für einen Tab, sondern Tab-übergreifend. Verwendet man das jadice web toolkit also in "zu vielen" Tabs gleichzeitig, kommt es irgendwann zu dem Problem, dass keine Anfragen mehr an den Server gesendet werden können. Je nach Browser ist die Beschränkung unterschiedlich (im ungünstigsten Fall nur 6 gleichzeitige Verbindungen).
Protokolle und Problematik
Seit HTML 5 ist WebSocket als Standard für bidirektionale Kommunikation mit geringem Overhead etabliert. Falls es zu Problemen kommt (z.B. nicht mehr unterstützte Browser oder Application Server) gibt es noch eine Fallback-Lösung, die überall funktioniert: Longpolling mit reinen HTTP-Anfragen und reinen HTTP-Antworten. Dies ist zwar die schlechteste Wahl, da sie den größten Overhead mit sich bringt, ist jedoch gerade für ältere Application Server und/oder Browser das einzig mögliche Protokoll.
Bei HTTPS mit Proxy-Servern kann es zudem zu Problemen mit WebSocket-Verbindungen kommen, da beim Aufbau einer WebSocket-Verbindung ein Upgrade-Verfahren zum Tragen kommt. Wenn eine Konfiguration der Proxy-Server nicht möglich ist, kann es sein, dass die einzige Möglichkeit darin besteht, auf Longpolling auszuweichen.
Die Wahl des Verfahrens ist folglich nicht trivial. Grundsätzlich empfehlen wir, WebSockets zu verwenden. Kommt es dabei zu Problemen, kann auf Longpoll ausgewichen werden.
Automatische Wahl des Verbindungsverfahrens
Die Transportschicht löst dieses Problem, indem es Implementationen für alle drei konfigurierbaren Verfahren bereitstellt und die Wahl des zu verwendenden Verfahrens selbstständig übernimmt. Dabei wählt der Client immer die effizienteste Möglichkeit, die zur Verfügung steht. Das Aushandeln des Verfahrens erfolgt dabei transparent für den Nutzer und kann vorab passend konfiguriert werden. Siehe Verfahren.
Das Downgrade-Verfahren:
-
Ermittlung der Protokolle, die vom Browser unterstützt werden
-
Verbinden:
-
Versuch mit WebSocket zu verbinden, insofern diese Methode aktiviert ist.
-
Schlägt die Kommunikation fehl, erfolgt nach einer bestimmten Anzahl Retrys ein Downgrade der Kommunikation auf dieses. Diese Parameter (setMaxReconnectAttempts, setMaxRetryDelay, etc.) lassen sich wie oben genannt frei konfigurieren.
Schritt 2 passiert so lange, bis das erste funktionierende Verfahren gefunden wurde, in der Reihenfolge:
- WebSocket basierend → Longpoll basierend
Ab dem Zeitpunkt, in dem ein funktionierendes Verfahren gefunden wurde, findet die Kommunikation ausschließlich über dieses Verfahren statt.
-
Es ist zu empfehlen, nicht verwendete Transportmechanismen via API zu deaktivieren, da das Downgrade Verfahren (je nach Konfiguration) eine gewisse Zeit in Anspruch nimmt und erst dann mit der Anwendung gearbeitet werden kann.
Vergleich der Verbindungsverfahren
Vorteil | Nachteil | |
WebSocket basierend |
|
|
Longpoll basierend |
|
|
Verwendete HTTP-Header
In machen Netzwerkinfrastrukturen kann es vorkommen, dass alle unbekannten HTTP-Header aus Sicherheitsgründen ausgefiltert werden. Das würde dazu führen, dass das jadice web toolkit nicht mehr korrekt funktioniert. Daher ist es nötig in diesem Fall die folgenden Header freizuschalten:
In jedem Fall wird der folgende HTTP-Header verwendet:
- X-GWT-Permutation
Lebenszyklus und Zustände einer Connection
Der Lebenszyklus einer Verbindung zwischen Client und Server umfasst mehrere Zustände. Im folgenden Teil werden diese Zustände beispielhaft aufgezeigt, inklusive bestehender Konfigurationsmöglichkeiten und Fehlerbehandlungsmechanismen.
Verbinden, Verbindungsfehler, Verbindungsabbruch oder Verbindungswiederherstellung
Der jadice web toolkit Client wird im Browser geöffnet. Daraufhin versucht sich der Client zum Backend des jadice web toolkit zu verbinden.
Der Server ist aufgrund einer kurzfristigen Überlastung des Netzwerks nicht erreichbar. Der Client versucht einige Male mit einer gewissen initialen Verzögerung neu zu verbinden. Bei jedem erneuten Verbindungsversuch wird randomisiert ein wenig länger gewartet um die Verbindungsanfragen besser über die Zeit zu verteilen und das Fluten des Servers mit Anfragen zu verhindern. Dieser Prozess wird Backoff genannt. Um die zunehmend längere, randomisierte Zeitspanne nach oben zu begrenzen, greift er auf eine konfigurierbare Zeitspanne zu, nach der er spätestens einen neuen Verbindungsversuch unternimmt. Wenn der Client erfolglos eine konfigurierbare Maximalanzahl an Verbindungsversuchen unternommen hat, bricht er den Backoff ab.
Alle Service-Anfragen (Laden eines Dokuments, Ausführen von
ServerOperations...), die während des Backoffs eingehen, werden
zwischengespeichert und nach dem erfolgreichen Wiederherstellen der
Verbindung erneut gesendet. Kann die Verbindung während des Backoffs
nicht wiederhergestellt werden, so werden die Service-Anfragen
abgebrochen und es wird die onError()
-Methode des Services aufgerufen.
Schlägt der Backoff fehl, wechselt der Client in den Staus FAILED bzw. versucht bei aktiviertem Longtime Reconnect weiterhin, die Verbindung wiederherzustellen. Während des Longtime Reconnect wird bis zum Schließen des Browsertabs oder bis zum erfolgreichen Verbinden mit dem Server in festen Abständen versucht, eine Verbindung zum Server aufzubauen.
Befindet sich der Client im Staus FAILED oder "Longtime Reconnect", so führt das Auslösen eines neuen Service Calls zu einem unmittelbaren neuen Verbindungsaufbau und der Client wechselt in den Zustand "Reconnect with Backoff".
Die möglichen Zustände der Verbindung und deren Übergange sind in der folgenden Grafik visualisiert.
API und Konfiguration
Die Transportschicht kann sowohl clientseitig als auch serverseitig konfiguriert und angesprochen werden.
Clientseitig
Eine Verbindung wird durch die Instanz einer ServerConnection abgebildet. Diese wird mit dem ServerConnectionBuilder erzeugt:
ServerConnection serverConnection = new ServerConnectionBuilder().build();
Die ServerConnection über den ServerConnectionBuilder zu bauen sollte das erste sein, was im EntryPoint der Anwendung gemacht wird, da verschiedenste Stellen des jadice web toolkit versuchen, die ServerConnection abzufragen. Ist diese nicht vorhanden kommt es zu NullPointerExceptions.
Dadurch erzeugt der Builder eine Verbindung zur selben URL unter der auch die Web Application des jadice web toolkit liegt.
Zudem kann über den ServerConnectionBuilder eine Verbindung zu einer beliebigen Zieladresse hergestellt werden, z.B. zu einem Loadbalancer oder einem jadice web toolkit Server-Backend auf einer anderen Maschine:
ServerConnection serverConnection = new ServerConnectionBuilder("https://www.yourcompany.com/webtoolkit").build();
Der Builder kann dabei auch so konfiguriert werden, dass verschiedene Protokolle im Voraus (de)aktiviert sind:
-
setWebSocketEnabled
-
setLongPollingEnabled
Außerdem kann das Verbindungsverhalten über die folgenden Konfigurationseinstellungen kundenspezifisch angepasst werden:
-
setMaxReconnectAttempts
-
Konfiguriert die maximale Anzahl an möglichen Neuversuchen eine Verbindung aufzubauen. Sollte diese Zahl erreicht sein, so wechselt der Client abhängig davon, ob das longtime reconnect- Verfahren aktiviert wurde, in den Zustand
RECONNETCTING_LONGTIME
(bei aktiviertem longtime reconnect) beziehungsweiseFAILED
bei nicht aktiviertem longtime reconnect. Zudem werden die am TransportClient registrierten TransportClient.TransportListener darüber informiert. -
Standardwert: 10
-
-
setInitialBackoffValue
-
Setzt die initiale Zeit nach der ein neuer Verbindungsaufbau unternommen werden soll (in Millisekunden)
-
Standardwert: 50 ms
-
-
setMaxRetryDelay
-
Setzt die Obergrenze, bis zu der hin ein Wiederverbindungsversuch verzögert werden kann (in Millisekunden)
-
Standardwert: 30.000 ms = 30 s
-
-
setRequestAggregationWindow
-
Setzt das Zeitfenster für Longpoll, innerhalb dessen clientseitig auf Nachrichten gewartet werden soll, um sie in einem Batch zu schicken, statt jede einzeln. Das reduziert die Anzahl an Anfragen, die der Client senden muss. Typischerweise liegt dieses Zeitfenster im Millisekundenbereich, sodass der Nutzer keine Verzögerung spürt, aber trotzdem mehrere aufeinander folgende Nachrichten gebündelt an den Server verschickt werden.
-
Standardwert: 100 ms
-
-
setLongtimeReconnectEnabled
-
Definiert, ob nach dem Erreichen der maximalen Anzahl Versuche mit dem Backoff-Verfahren weiter versucht werden soll, die Serververbindung wiederherzustellen. Wenn dieser Wert auf
true
gesetzt ist wird in einem definierten Intervall versucht,die Verbindung wiederherzustellen. Die Länge dieses Intervalls kann in setLongTimeIntervalInMillis konfiguriert werden. -
Standardwert:
true
-
-
setLongtimeReconnectIntervalInMillis
-
Definiert das Intervall in Millisekunden, in dem beim longtime reconnect-Verfahren versucht werden soll, die Serververbindung nach Erreichen der maximalen Anzahl Versuche mit dem Backoff-Verfahren wiederherzustellen. Dieses Intervall wird nur beachtet, wenn das Verfahren mit setLongtimeReconnectEnabled(true) aktiviert wurde.
-
Standardwert: 60.000ms = 60 s
-
Nachdem eine Verbindung erstellt wurde, kann sie jederzeit statisch abgerufen werden:
ServerConnection.get()
Über die so erhaltene ServerConnection kann man auf den TransportClient zugreifen:
serverConnection.getTransportClient()
Dieser ermöglicht es:
-
Den Verbindungsstatus abzurufen
-
Das verwendete Protokoll zu erfragen
-
Einen TransportClient.TransportListener zu (de)registrieren. Dieser kann
-
über Statusänderungen der Verbindung informieren
-
über Änderungen des verwendeten Protokolls informieren
-
über Fehler der Transportschicht informieren
-
-
Die Verbindung zu trennen oder aufzubauen
Beispiele hierzu finden sich auch in der Enterprise Demo in der Klasse "EnterpriseDemoMain".
Serverseitig
Da das jadice web toolkit vor der Transportschicht erzeugt wird, benötigt man einen NetworkContext.NetworkInitializedListener der darüber informiert, dass die Transportschicht initialisiert wurde, um sie anzusprechen. Der Listener wird über den NetworkContext (de)registriert:
NetworkContext.addNetworkInitializedListener(...); NetworkContext.removeNetworkInitializedListener(...);
Nachdem die Transportschicht initialisiert wurde, kann man:
- sich mit Hilfe von ClientSessionListenern, über verschiedene Client-Events informieren lassen (Erzeugung einer Session, Schließen einer Session, Timeout und Protokolländerungen).
// Example how to register a listener that notifies when the network layer has been initialized
NetworkContext.addNetworkInitializedListener(networkContext -> {
// Example how to register a client session listener that notifies about client events
networkContext.addClientSessionListener(new ClientSessionListener() {
@Override
public void sessionTimedOut(Client client) {
LOGGER.info("Client timed out: [id=" + client.getId() + ", protocol=" + client.getTransportMethod() + "]");
}
@Override
public void sessionStarted(Client client) {
LOGGER.info(
"Client session started: [id=" + client.getId() + ", protocol=" + client.getTransportMethod() + "]");
}
@Override
public void sessionClosed(Client client) {
LOGGER.info(
"A client session was closed: [id=" + client.getId() + ", protocol=" + client.getTransportMethod() + "]");
}
@Override
public void methodChanged(Client client, Method oldMethod, Method newMethod) {
LOGGER.info("Client [id=" + client.getId() + "] changed the communication method from " + oldMethod + " to "
+ newMethod);
}
});
});
- Die verbundenen Clients ermitteln:
// Example how to register a listener that notifies when the network layer has been initialized
NetworkContext.addNetworkInitializedListener(networkContext -> {
networkContext.getClients();
};
Wurde die Transportschicht initialisiert, so kann der NetworkContext auch statisch abgefragt werden:
NetworkContext.get();
Konfiguration der serverseitigen Netzwerkparameter
Die Netzwerkparameter sind schon sinnvoll voreingestellt, können deklarativ über Spring Boot (siehe Konfiguration serverseitiger Einstellungen) oder aber programmatisch über den ConfigurationManager kundenindividuell angepasst werden:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration();
Hier kann man
-
Bei Verwendung des Longpoll-Protokolls das Zeitfenster konfigurieren, für das Serverantworten gesammelt und als Batch gesendet werden um die Anzahl an Antworten zu reduzieren.
-
Beispiel:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration().setResponseAggregationWindow(Duration.ofMillis(70));
-
Standardwert: 70 ms
-
-
Bei Verwendung des Longpoll-Protokolls die Zeitspanne konfigurieren, nach der ein Timeout ausgelöst wird. Diese gibt an, wann eine offene Longpollanfrage geschlossen werden soll, sodass der Client eine neue sendet. Sollte der Server eine Antwort haben, wird dieser Longpoll sofort beantwortet und geschlossen ohne dass ein Timeout eintritt. Um die Nachrichtenzahl niedrig zu halten, sollte dieser Wert nicht zu klein gewählt werden.
-
Beispiel:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration().setLongPollTimeout(Duration.ofMinutes(5));
-
Standardwert: 5 min
-
-
Bei Verwendung des Websocket-Protokolls die Zeitspanne konfigurieren, nach der ein Timeout ausgelöst wird. Diese gibt an, wann eine offene Weboscket-Verbindung geschlossen werden soll, sodass der Client eine neue aufbaut. Um die Nachrichtenzahl niedrig zu halten, sollte dieser Wert nicht zu klein gewählt werden.
-
Beispiel:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration().setWebSocketTimeout(Duration.ofHours(9));
-
Standardwert: 9 h
-
-
Bei Verwendung des Longpoll-Protokolls das Intervall konfigurieren, innerhalb dessen keep-alive-Nachrichten an den Client gesendet werden. Diese Nachrichten führen dazu, dass der initiale HTTP-Request auf Clientseite nicht in einen Timeout läuft.
-
Beispiel:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration().setKeepAliveInterval( Duration.ofSeconds(30));
-
Standardwert: 30 s
-
-
Die Zeit konfigurieren, nach der für inaktive Clients ein Sessiontimeout eintritt. Sollte sich ein Client über diese Zeit nicht gemeldet haben (z.B. weil das Browserfenster geschlossen wurde), werden alle Netzwerkresourcen aufgeräumt, die mit dem Client verknüpft waren.
-
Beispiel:
ConfigurationManager.getServerConfiguration().getNetworkConfiguration().setSessionTimeout(Duration.ofSeconds(30));
-
Standardwert: 30 s
-
Beispiele zu der serverseitigen Konfiguration finden sich in der Enterprise Demo in der Klasse "EnterpriseDemoSystem" oder für die deklarative Konfiguration in der "application.yml" der Simple Spring Demo.
Unterstützung von CORS (Cross-Origin Resource Sharing)
Öffnet man das jadice web toolkit im Browser, so versucht sich der Client zum Backend des jadice web toolkit zu verbinden. Falls das Backend unter der selben URL erreichbar ist, von der auch der Javascript-Client geladen wurde, sind keine weiteren Schritte notwendig. Falls sich die URLs jedoch unterscheiden, muss aufgrund der Same-Origin-Policy ein CORS-Filter angelegt werden.
Das folgende Kapitel beschreibt die Schritte, die für den Betrieb des jadice web toolkit über Domänengrenzen hinweg notwendig sind.
Zunächst muss ein CORS-Filter implementiert werden:
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebFilter(
filterName = "jwtCORSFilter",
description = "Allows Cross Origin Requests to the JWT",
displayName = "jadice web toolkit CORS Filter",
urlPatterns = {
"/*"
},
asyncSupported = true)
public class CORSFilter implements Filter {
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest) servletRequest;
final HttpServletResponse resp = (HttpServletResponse) servletResponse;
// set the allowed origin to http://127.0.0.1:8080.
resp.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:8080");
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Headers",
"content-type, x-gwt-permutation");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
private String getAllowedDomainsRegex() {
return "individual / customized Regex";
}
}
Folgende Konfiguration ist hierzu nötig:
-
Access-Control-Allow-Credentials
Unter der Verwendung von longpoll wurde der Aufruf von
com.google.gwt.http.client.RequestBuilder#setIncludeCredentials
hinzugefügt, um credentials in Cross-Origin Requests zu aktivieren. Deshalb muss nun auch im Filter das flagAccess-Control-Allow-Credentials
zwingend auftrue
gesetzt werden. -
Access-Control-Allow-Origin
Im obigen Beispiel wird der Eintrag
Access-Control-Allow-Origin
im Header auf "127.0.0.1:8080" gesetzt. Dies bedeutet, dass nur Anfragen, die von Adresse 127.0.0.1 und Port 8080 kommen erlaubt sind.
Im obigen Beispiel erfolgt die Registrierung des CORS-Filters über die WebFilter Annotation. Alternativ ist dies auch über die web.xml möglich:
<filter>
<filter-name>CORSFilter</filter-name>
<filter-class>com.levigo.jadice.web.demo.basicviewer.server.CORSFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CORSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>