Approches pour le développement d'applications web côté serveur
-
Types de frameworks pour le développement web :
-
Réalisations de pages web dynamiques mixant code exécuté côté serveur et code HTML statique
- Approche popularisée par PHP dès 1994
- Concept repris en Java avec Java Server Pages (JSP)
- Avantage : abord assez aisé pour des applications simples
- Inconvénients : organisation difficile pour des gros projets, pas de séparation propre entre la partie présentation (code HTML) et le code serveur
-
Frameworks Modèle-Vue-Contrôleur de type pull
- Eléments centraux : composants visuels à afficher
- Récupération par les composants visuels des données à afficher calculées par du code executé sur le serveur (contrôleurs)
- Approche utilisée par Java Server Faces (framework disponible avec JavaEE) ainsi que Lift, Tapestry...
-
Framework Modèle-Vue-Contrôleur de type push
- Éléments centraux : actions (contrôleurs) déclenchées par les requêtes HTTP
-
Consommation de données communiquées dans la requête HTTP et production d'une réponse HTTP par le contrôleur
- Données échangeables dans un format structuré : JSON, XML...
- Utilisation possible de moteurs de templates (Velocity...) pour renvoyer le code dans un format de type HTML
-
Réalisations de pages web dynamiques mixant code exécuté côté serveur et code HTML statique
-
Modèle de concurrence et types d'entrées/sorties
-
Utilisation de threads
- Une thread = une pile d'appel ; partage du tas entre toutes les threads du processus
- Entrées/sorties bloquantes : l'exécution d'une thread s'arrête pour attendre la possibilité de recevoir/envoyer des données sur le réseau
- Organisation par le système d'exploitation de l'ordonnancement des threads en fonction de la disponibilité des entrées/sorties
- Avantage : paradigme de programmation séquentiel assez aisé à appréhender
-
Inconvénients
- Le partage des objets en mémoire sur le tas est assez délicat (nécessité d'utiliser des moniteurs et verrous pour protéger en lecture/écriture des objets)
- Le passage à l'échelle (nombre connexions simultanées) des serveurs webs threadés est problématique
-
Utilisation d'une thread d'événements unique avec entrées/sorties non bloquantes
- Gestion par une seule thread d'un file d'événements traités les uns a la suite des autres
- Evénement = disponiblité en lecture (ou possibilité d'écrire des données) sur une socket (connexion avec un client web)
- Utilisation d'un mécanisme de sélecteur pour déterminer les connexions disponibles pour la lecture et/ou l'écriture
-
Avantages :
- Meilleur passage à l'échelle
- Structures de données lues/écrites que par une unique thread (plus de problématiques de verrous à gérer)
- Inconvénient : nécessite un style de programmation asynchrone moins naturel que le style séquentiel des threads (inconvénient en partie évitable avec l'utilisation de co-routines)
-
Utilisation de threads
-
Modèle de déploiement
- Utilisation d'un serveur applicatif web lourd Java EE : Wildfly, Glassfish...
- Utilisation de biblithèques Java (jar) fournissant les classes de base pour le serveur web : Spring Boot, Vertx...
Tendance actuelle :
- Utilisation de rameworks MVC de type push se concentrant sur l'écriture de contrôleurs répondant aux requêtes HTTP
-
Gestion de l'aspect vue par des frameworks JavaScript côté client
- Le framwork web côté serveur reçoit et retourne des données en JSON qui seront mises en formes par le navigateur web
-
Utilisation de frameworks basées sur une boucle d'événements mono-threadée et E/S non-bloquantes
- Approche popularisée par NodeJS (framework web serveur utilisant le langage JavaScript)
- Support par les versions récentes de Java EE des E/S non-blouquantes
- Autres framworks Java de type push avec possibilité d'utiliser des E/S non-bloquantes : Play (basé sur Netty), Vert.x...
- Evitement des serveurs applicatifs lourds Java EE (configuration et déploiement plus difficile)
A propos de Vert.x
- Framework web développé par Tim Fox (employé chez VMWare) à partir de 2011
- Support de plusieurs langages de programmation : JavaScript, Java, Kotlin, Scala, Groovy, Ceylon (virtuellementtout langage exécutable sur la JVM)
- Projet actuellement géré par la fondation Eclipse
- Utilisation d'une thread pour gérer une file d'événements avec des E?S non-bloquantes
- Mise en œuvre d'un bus de communication pour convoyer des événements entre différentes composantes
- Module élémentaire d'une application Vert.x : le Verticle
- Possibilité de développer plusieurs serveurs dans une application avec plusieurs Verticle (serveurs webs HTTP ou serveur TCP ou UDP)
- Une page intéressante d'introduction à Vert.x
Un premier Verticle saluant le monde
Ecrivons un premier Verticle saluant le monde (comme la tradition l'exige). Pour cela on pourra utiliser l'exemple de projet hébergé à l'adresse suivante :
https://github.com/vert-x3/vertx-maven-starter
On suit les instructions de la page pour récupérer une copie du dépôt Git avec git clone puis en exécutant mvn exec:java pour lancer le serveur avec Maven. On peut aussi lancer le script redeploy.sh qui permet de recompiler et lancer automatiquement le serveur lorsque l'on modifie le code-source.
Pour des besoins personnalisés (dépendances spécifiques), on peut utiliser le générateur de projet disponible à cette adresse : https://start.vertx.io/
Voici le code-source du Verticle de salutation :
package io.vertx.starter; import io.vertx.core.AbstractVerticle; public class MainVerticle extends AbstractVerticle { @Override public void start() { vertx.createHttpServer() .requestHandler(req -> req.response().end("Hello Vert.x!")) .listen(8080); } }
Le RequestHandler installé traite chaque requête et répond invariablement un message de bienvenue. Si nous souhaitons répondre uniquement pour un chemin déterminé (par exemple /hello), nous pouvons utiliser un Router comme RequestHandler qui lui-même peut accueillir des RequestHandler pour différents chemins :
HttpServer server = vertx.createHttpServer(); Router router = Router.router(vertx); router.route("/hello").method(HttpMethod.GET).handler(req -> req.response().end("Hello with router")); server.requestHandler(router).listen(8080);
Le RequestHandler Router est fourni par la bibliothèque additionnelle vertx-web (page de référence), nous devons l'ajouter comme dépendance dans le fichier pom.xml utilisé par Maven pour la construction de projet :
<dependencies> ... <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web</artifactId> </dependency> ... </dependencies>
En cas de souci avec le fonctionnement de notre application, nous pouvons activer un affichage des entrées loguées comme indiqué ici.
Il est possible d'utiliser des routes paramétrées pour personnaliser le contenu affiché :
router.route("/hello/:name").method(HttpMethod.GET) .handler(req -> req.response().end("Hello " + req.pathParam("name") + " with router"));
Il est possible d'ajouter plusieurs routes sur un Router ; lorsqu'une requête arrive, le serveur les teste par ordre de déclaration jusqu'à trouver une route validant la requête : la requête est envoyée au RequestHandler associé. Certains RequestHandler peuvent demander que la requête soit transmise à un éventuel RequestHandler suivant.
Nous pouvons par exemple ajouter en tant que première route validant toutes les requêtes un RequestHandler ajoutant un en-tête dans la réponse et demandant que la requête soit transmise au RequestHandler suivant (validant la requête) :
router.route().handler(req -> { req.response().putHeader("X-MyCustomizedHeader", "Here is the date " + new Date().toString()); req.next(); });
Un serveur web délivrant du contenu statique
Un serveur web peut délivrer :
- du contenu dynamique (calculé à l'exécution en fonction de paramètres fournis dans la requête)
- du contenu statique (pages HTML, scripts JavaScript, image ou tout autre type de contenu prédéfini)
vertx. fournit un RequestHandler destiné à la fourniture de contenu statique que nous pouvons installé sur le Router :
router.route("/static/*").handler(StaticHandler.create());
Pour cet exemple toutes les requêtes dont le chemin commence par /static/ sont redirigées vers le StaticHandler. Par défaut le StaticHandler recherche le contenu statique dans le répertoire src/main/resources/webroot. Ainsi si nous appelons l'URL http://localhost:8080/static/image.png, le StaticHandler retournera le fichier présent dans src/main/resources/webroot/image.png (s'il est présent, autrement une erreur 404 sera renvoyée).
Gestion des sessions
Le protocole HTTP est sans état ; on peut néanmoins gérer des sessions à l'aide de cookie. A la première requête d'un client web, un identifiant aléatoire est installé dans un cookie par le serveur web ; pour toutes les requêtes suivantes, le client web renvoit cet identifiant.
Une session est utile pour stocker transitoirement des données associées à un visiteur : nombre de pages visitées, panier d'achat... L'objet Session se manipule comme une Map.
Ecrivons une page affichant un compteur de requêtes réalisé par un visiteur (ainsi qu'une page permettant de réinitialiser ce compteur) :
SessionStore sessionStore = LocalSessionStore.create(vertx); router.route().handler(SessionHandler.create(sessionStore)); router.route().handler(req -> { Integer requestNumber = req.session().get("requestNumber"); if (requestNumber == null) requestNumber = 0; requestNumber++; req.session().put("requestNumber", requestNumber); req.next(); }); router.route("/getRequestNumber").method(HttpMethod.GET).handler(req -> { Integer requestNumber = req.session().get("requestNumber"); req.response().end("RequestNumber=" + requestNumber); }); router.route("/resetRequestNumber").method(HttpMethod.GET).handler(req -> { req.session().remove("requestNumber"); req.response().end("resetted"); });
Utilisation d'un moteur de template
Il est possible de générer directement une chaîne de caractères représentant une page HTML dynamique à renvoyer par le serveur. La génération de la page depuis le code est fastidieuse (concaténation de chaînes) et s'oppose au principe de séparation entre la définition de la vue graphique (page HTML) et de la programmation des actions (récupération, stockage, traitement de données).
Pour générer une page HTML dynamique, nous pouvons utiliser un moteur de template. Nous écrivons alors la page HTML à retourner sous la forme d'un template, i.e. un fichier contenant des balises HTML avec des directives spécifiques permettant d'incorporer des variables, d'afficher certaines zones de la page selon certaines conditions, d'exécuter des boucles pour afficher des listes d'éléments... La syntaxe des directives de template est propre au moteur de template choisi.
Nous prenons l'exemple du moteur de templates Pebble (dont la syntaxe présente des similarités avec le moteur Jinja2 disponible pour Python) ; il existe également d'autres moteurs de template utilisant des syntaxes différentes compatibles avec Vert.x (par exemple Handlebars, Rocker, FreeMarker, ...). Nous pouvons créer un template simple base.peb pour l'affichage d'une page :
<html> <head> <title>{{title}}</title> </head> <body> <h1>{{title}}</title> {% block content %} to be replaced by template inheritance {% endblock %} {% block footer %} <hr> (C) Copyright SuperTemplater {% endblock %} </body> </html>
Cette page intègre la variable title et définit des zones qui peuvent être modifiées par héritage : content et footer. Nous pouvons ainsi hériter de ce template pour une page dynamique de salutation affichant le nom de l'utilisateur ainsi que son adresse IP avec le template hello.peb :
{% extends "base.peb" %} {% block content %} <p> {% if name is defined %} Hello {{ name }} {% else %} Hello anonymous person {% endif %} , here is your IP address: {{ ip }}! </p> <p> Now we try to display a list of elements: <ul> {% for element in basket %} <li>{{element}}</li> {% endfor %} </ul> </p> {% endblock %}
N'oublions pas de déclarer dans le pom.xml la dépendance nécessaire afin de permettre l'utilisation du moteur de template Pebble :
<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web-templ-pebble</artifactId> </dependency>
Ces templates peuvent être par exemple placés dans le répertoire src/main/resources/templates. Ecrivons maintenant le code du handler utilisant le template hello.peb :
final TemplateEngine engine = PebbleTemplateEngine.create(vertx); router.get("/hello/:name").handler(req -> { Map<String, Object> vars = new HashMap<>(); vars.put("title", "Hello world"); vars.put("name", req.pathParam("name")); vars.put("ip", req.request().remoteAddress().host()); vars.put("basket", Arrays.asList("apple", "pear", "grapefruit", "banana")); System.out.println(vars); engine.render(vars, "templates/hello", res -> { if (res.succeeded()) { req.response().end(res.result()); } else { req.fail(res.cause()); } }); });
Requêtes longues à traiter
Une requête longue à traiter exécutant des actions synchrones bloque la thread gérant la file d'événements. Prenons l'exemple d'une requête calculant le nombre d'entiers pairs de 0 à Integer.MAX_VALUE :
router.route("/getEvenNumber").method(HttpMethod.GET).handler(req -> { int even = 0; for (int i = 0; i < Integer.MAX_VALUE; i++) if (i % 2 == 0) even++; req.response().end("even: " + even); });
Cette requête peut nécessiter plusieurs secondes de calcul durant lesquelles aucune autre requête arrivant au serveur ne peut être gérée. Pour éviter ce problème, on demande à vert.x d'exécuter le code bloquant sur une thread annexe pour ne pas bloquer la thread principale. On utilise pour cela vertx.runBlocking :
router.route("/getEvenNumber2").method(HttpMethod.GET).handler(req -> { vertx.executeBlocking(event -> { int even = 0; for (int i = 0; i < Integer.MAX_VALUE; i++) if (i % 2 == 0) even++; event.complete(even); }, event -> { req.response().end("even: " + event.result()); }); });
executeBlocking requiert deux paramètres passés sous la forme d'expressions lambda à exécuter :
- Le premier est le code bloquant à exécuter ; on appelle event.complete(result) pour signaler le résultat du calcul (on peut aussi appeler event.fail(ex) avec une exception survenue en cas de problème)
- Le deuxième est du code appelé sur la thread principale lorsque le résultat est calculé. Il est prudent avant d'appeler event.result() de vérifier si le calcul n'a pas échoué avec event.failed() (event.cause() permet de récupérer l'éventuelle exception)
Par défaut vert.x tolère un blocage de 2 secondes de la thread principale et d'une minute pour une thread secondaire. On peut modifier en cas de besoin ces valeurs :
VertxOptions options = new VertxOptions(); options.setBlockedThreadCheckInterval(120_000L); // en millisecondes pour la thread annexe (ici 2 minutes) options.setMaxEventLoopExecuteTime(1_000_000_000L); // en nanosecondes pour la thread principale d'événements (ici 1 seconde) Vertx vertx = Vertx.vertx(options);
Un service REST JSON
Réalisons un service web de type REST acceptant une requête HTTP avec un contenu en JSON et retournant un réponse JSON. Ce service REST a pour objectif de compter le nombre d'occurrences de mot dans un texte fourni dans la requête.
Nous écrivons le RequestHandler que nous installons sur le Router (sans avoir oublié auparavant d'activer le BodyHandler nous permettant de récupérer le corps de la requête) :
router.route().handler(BodyHandler.create()); router.post("/countOccurrences").consumes("application/json").handler(req -> { JsonObject obj = req.getBodyAsJson(); String s = obj.getString("text"); vertx.executeBlocking(event -> { event.complete(countOccurrences(s)); }, event -> { JsonObject result = new JsonObject(); Map<String, Integer> occurrMap = (Map<String, Integer>)event.result(); for (Map.Entry<String, Integer> entry: occurrMap.entrySet()) result.put(entry.getKey(), entry.getValue()); req.response().putHeader("Content-Type", "application/json") .end(result.encodePrettily()); } ); });
Nous écrivons également une méthode permettant de compter le nombre d'occurrences de mots dans un texte et retournant une Map :
private Map<String, Integer> countOccurrences(String s) { Map<String, Integer> occurrMap = new HashMap<>(); for (String word : s.split("[\\p{Punct}\\s]+")) occurrMap.merge(word, 1, (x, y) -> x + y); return occurrMap; }
Nous pouvons ensuite tester le service web avec un client HTTP tel que Curl (avec une interface en ligne de commande) :
curl -H "Content-Type: application/json" -d '{"text":"hello helo world test"}' http://localhost:8080/countOccurrences
Si le corps envoyé dans la requête n'est pas une chaîne JSON valide, la méthode req.getBodyAsJson() lève une exception et le serveur répond au client par une erreur 500. En capturant l'exception, nous pourrions renvoyer un message plus explicite :
JsonObject obj = null; try { obj = req.getBodyAsJson(); } catch (DecodeException e) { req.response().setStatusCode(400).end("invalid input JSON"); return; }
Gestion de la configuration
Vert.x propose un plugin pour gérer la configuration d'une application. On peut l'activer en ajoutant dans le fichier pom.xml de Maven la dépendance suivante :
<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-config</artifactId> <version>3.8.3</version> </dependency>
Le plugin supporte des fichiers de configuration au format Java properties ou en JSON par exemple. Il est possible de récupérer la configuration à distance sur un serveur HTTP ou vi le bus de communication intégré à Vert.x.
Nous pouvons utiliser le ConfigRetriever afin de récupérer des données de configuration pour initialiser le serveur. On récupère la configuration sous la forme d'un objet JSON dont on peut explorer les clés pour récupérer les informations souhaitées. On peut par exemple créer un serveur web qui écoute sur un port personnalisé et retourne également une réponse différente selon le chemin interrogé.
public class SimpleVerticleWithCustomConfig extends AbstractVerticle { @Override public void start() throws Exception { super.start(); ConfigRetriever retriever = ConfigRetriever.create(vertx); retriever.getConfig(json -> { JsonObject result = json.result(); // messages is a JSON object (can be viewed as a map) JsonObject messages = result.getJsonObject("messages"); Router router = Router.router(vertx); for (String key: messages.fieldNames()) router.route(key).handler(req -> req.response().end(messages.getString(key))); vertx.createHttpServer() .requestHandler(router) .listen(result.getInteger("port")); }); } }
Il est nécessaire d'indiquer la configuration à utiliser. Par défaut vert.x interroge les sources suivantes avec cet ordre de priorité :
- Le retour de la méthode JsonObject config() du verticle
- Les propriétés système de la JVM
- Les variables d'environnement
- Le fichier conf/config.json (par défaut dans le répertoire src/main/resources) dont le chemin peut être modifié par la propriété système vertx-config-path ou la variable d'environnement VERTX_CONFIG_PATH.
Ecrivons un fichier JSON pour la configuration :
{ "messages": { "/hello": "Hello world!", "/bye": "Good bye world!" }, "port": 80802 }
Déploiement de plusieurs verticles et communication entre-eux
Il est possible de déployer plusieurs verticles au sein d'une même application et de communiquer entre ces verticles en utilisant le bus intégré de communication. Ces verticles peuvent même être exécutés par des JVMs différentes que ce soit localement ou sur des machines différentes.
Le déploiement d'un verticle est réalisé en utilisant l'appel asynchrone deployVertice(Verticle v, Handler<AsyncResult<String>> handler), le handler étant appelé lorsque le verticle est déployé.
Un verticle peut envoyer un message sur le bus de communication que l'on peut obtenir avec vertx.eventBus(). Un message est toujours associé avec une adresse de destination. Un vertice peut écouter tous les messagesqui arrivent sur une adresse du bus en enregistrant un MessageConsumer :
MessageConsumer<String> consumer = vertx.eventBus().consumer("importantEvents"); consumer.handler(message -> { System.out.println("New important event received: " + message.body()); message.reply("Receival of event " + message.body() + " acknowledged"); });
Un MessageConsumer peut répondre à un message avec la méthode reply(...). Il est possible d'échanger des messages de type String ou alors n'importe quel objet Java. Dans ce cas, il faut indiquer comment l'objet doit être sérialisé et désérialiser pour passer sur le bus de communication. On créé pour cela un MessageCodec capable de réaliser ces opérations de conversion et on indique au bus de l'utiliser pour la classe en question :
vertx.eventBus().registerDefaultCodec(Person.class, new PersonMessageCodec());
Pour envoyer un message vers une adresse sur le bus, trois possibilités sont proposées :
- L'envoi du message en broadcast à tous les MessageConsumer écoutant l'adresse avec eventBus.publish(message)
- L'envoi du message à un seul MessageConsumer (sélectionné par Vert.x s'il en existe plusieurs) avec eventBus.send(message)
- L'envoi d'un message à un MessageConsumer en attendant une réponse de celui-ci avec eventBus.request(message, response -> { if response.succeeded() { ... }})
Réalisons comme exemple une application retournant le mot qui suit un mot du dictionnaire. Tout d'abord, nous crééons un verticle chargé de gérer le dictionnaire avec un MessaceConsumer retournant le mot suivant pour une demande envoyée sur le bus :
public class DictionaryManagerVerticle extends AbstractVerticle { private final SortedSet<String> dictionary; public DictionaryManagerVerticle(SortedSet<String> dictionary) { this.dictionary = dictionary; } @Override public void start() throws Exception { super.start(); MessageConsumer<String> consumer = vertx.eventBus().consumer("dictionary.nextWord"); consumer.handler(message -> { String word = message.body(); Set<String> tailSet = dictionary.tailSet(word); String nextWord = null; for (String candidate: tailSet) { if (candidate.equals(word)) continue; // next word nextWord = candidate; break; } System.err.println("Receiving " + message + " and replying " + nextWord); if (nextWord != null) message.reply(nextWord); else message.fail(0, "next word not found"); message.reply(null); }); } }
Ensuite nous crééons le verticle principal qui déploiera le précédent verticle pour trouver les prochains mots pour chaque requête reçue :
public class DictionaryServerVerticle extends AbstractVerticle { @Override public void start(Future<Void> startFuture) throws Exception { super.start(); ConfigRetriever retriever = ConfigRetriever.create(vertx); retriever.getConfig(json -> { // retrieve the configuration to get the path of the dictionary JsonObject config = json.result(); String dictionaryPath = config.getString("dictionaryPath"); SortedSet<String> dictionary = null; try { dictionary = DictionaryUtils.loadDictionary(dictionaryPath); } catch (IOException e) { startFuture.fail(e); } DictionaryManagerVerticle dmv = new DictionaryManagerVerticle(dictionary); vertx.deployVerticle(dmv, stringAsyncResult -> { // the verticle is deployed, we can start the server Router router = Router.router(vertx); router.route("/nextWord/:word").handler( req -> { String word = req.request().getParam("word"); DeliveryOptions options = new DeliveryOptions(); options.setSendTimeout(500); vertx.eventBus().request("dictionary.nextWord", word, options, answer -> { if (answer.succeeded()) { Object nextWord = answer.result().body(); if (nextWord != null) req.response().end(nextWord.toString()); return; } req.response().setStatusCode(404).end("not found"); }); }); vertx.createHttpServer() .requestHandler(router) .listen(8081); }); }); } }
Communication avec une WebSocket
Une communication WebSocket peut être initialisée à partir d'une connection HTTP classique. Il est possible d'échanger des messages binaires ou textuels bi-directionnellement entre le client et le serveur.
On peut ajouter un handler sur un serveur HTTP Vert.x afin de pouvoir traiter les communications WebSocket. A partir de la connexion WebSocket, nous pouvons enregister une fonction qui sera exécutée lorsqu'un message binaire ou textuel provient du client. Nous pouvons également envoyer des messages binaires et textuels vers le client.
Nous présentons ici comme exemple un Verticle gérant un salon de discussion recevant des messages et les relayant aux utilisateurs connectés.
public class WebSocketVerticle extends AbstractVerticle { @Override public void start() throws Exception { super.start(); final Set<ServerWebSocket> sockets = new HashSet<>(); vertx.createHttpServer() // install the websocket handler .websocketHandler(socket -> { sockets.add(socket); // inform all the websockets that a new person has arrived sockets.forEach(s -> s.writeTextMessage(socket.remoteAddress() + " entered the chat")); // action to do when a message arrives socket.textMessageHandler(msg -> { // relay the message to all the sockets (including the sending socket) sockets.forEach(s -> s.writeTextMessage(socket.remoteAddress() + ": " + msg)); }); // handle the closure of the socket socket.closeHandler(a -> { sockets.remove(socket); sockets.forEach(c -> c.writeTextMessage(socket.remoteAddress() + " leaved the chat")); }); }) // install an handler to send a HTML file with JavaScript code to contact the websocket .requestHandler(req -> { if (req.path().equals("/")) req.response().sendFile("websocket.html").end(); }) .listen(8081); } }
L'appel à requestHandler nous permet d'envoyer une page HTML (websocket.html) contenant du code JavaScript nous permettant d'ouvrir une websocket vers le serveur et de recevoir et envoyer des messages avec celle-ci :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocket chat</title> </head> <body> <h1>WebSocket chat</h1> <div> Received messages: <ol id="receivedMessages"> </ol> </div> <p> <textarea id="messageToSend" cols="70" rows="2">test message</textarea> <br> <input type="submit" id="submitMessage"> </p> <script> var openWebSocket = () => { console.log("Opening WebSocket"); // take the current URL and replace the http protocol by ws at the head of the URL let socket = new WebSocket("ws" + window.location.href.substring(4)); // open the websocket let displayMessage = msg => { let a1 = document.createElement("li"); a1.textContent = msg; document.getElementById("receivedMessages").appendChild(a1); }; socket.onopen = event => { displayMessage("Connection opened"); } socket.onmessage = event => { displayMessage(event.data); } document.getElementById("submitMessage").addEventListener("click", e => { let msg = document.getElementById("messageToSend").value; socket.send(msg); }); }; window.onload = e => { openWebSocket(); }; </script> </body> </html>