WebSocket
Messages instantanés entre le serveur et le client.
Introduction
Avec WebSocket, il est possible d'établir une connexion permanente entre le serveur et le navigateur du client.
Cela signifie qu'à tout moment le navigateur pourra utiliser la connexion établie via WebSocket pour envoyer des données au serveur, sans qu'il soit obligatoire d'obtenir une réponse. Il en va de même pour le serveur qui pourra contacter le navigateur du client en envoyant des données de manière immédiate.
La communication est réalisée instantanément et à tout moment il est possible de faire transiter des données d'un côté à l'autre de manière indépendante et immédiate.
C'est très utile pour réaliser des opérations en temps réel avec des communications immédiates, c'est comme s'il s'agissait d'un chat, d'une discussion entre le navigateur et le serveur, à tout moment l'un ou l'autre peut envoyer un message et l'autre le reçoit.
Par exemple, il est très utilisé depuis des solutions de chats en général jusqu'aux jeux, nous l'utilisons aussi pour présenter des modifications de données en direct, c'est-à-dire lorsqu'il existe un besoin de réaliser une communication le plus immédiatement possible.
De manière simplifiée, la différence entre la communication HTTP classique et WebSocket :

Du côté du serveur, un endpoint est créé, une adresse qui permet d'établir des connexions avec WebSocket.
Le serveur a la capacité d'envoyer des données uniquement vers une connexion spécifique, c'est-à-dire qu'il est possible d'envoyer de nouveaux messages à un participant d'un chat en particulier.
De même, le serveur peut faire du broadcast, c'est-à-dire l'envoi de données à toutes les connexions actives sur une adresse déterminée, autrement dit lorsqu'un message est envoyé à tous les participants du chat qui sont dans une salle ou un groupe.
Activation et Configuration
Pour activer et configurer WebSocket dans votre application Netuno, il est nécessaire de modifier le fichier de configuration de l'application correspondant à l'environnement utilisé, comme :
📂 config/_development.json📂 config/_production.json
Insérez et ajustez les paramètres suivants :
...
"ws": {
"hosts": [...], "<-- OPTIONNEL": "plus d'informations dans la documentation ci-dessous.",
"endpoints": [
{
"name": "pool",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/pool",
"path": "/",
"service": "/services/ws/pool"
},
{
"name": "salle",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/salle",
"path": "/{uid}",
"service": "/services/ws/salle"
}
]
},
...
endpoints
C'est un tableau qui permet de définir que le serveur prendra en charge WebSocket à travers les multiples adresses publiques spécifiées (endpoints), donc chaque endpoint est une adresse publique qui prend en charge la réception de connexions via WebSockets depuis les navigateurs.
name
Nom d'identification de l'endpoint.
enabled
Permet d'activer et de désactiver l'endpoint, la valeur par défaut est true (actif).
sendTimeout
Limite de temps maximale pour envoyer un message aux clients, la valeur par défaut est 60000 millisecondes, soit l'équivalent de 1 minute.
idleTimeout
Limite de temps maximale pour l'inactivité de la connexion, la valeur par défaut est 300000 millisecondes, soit l'équivalent de 5 minutes.
maxText
Limite en octets pour la taille maximale de la longueur du message, la valeur par défaut est 1048576, équivalent à 1 mégaoctet.
public
Définit la base de l'adresse publique qui permettra de recevoir les connexions via WebSocket provenant des navigateurs.
L'adresse finale est constituée de la valeur de configuration public suivie de ce qui est défini dans path.
path
Définit la partie finale du chemin de l'adresse publique, peut être dynamique, ce qui permet de créer par exemple de multiples canaux séparés, par exemple c'est utile pour créer plusieurs salles.
Attention : le
pathdoit toujours commencer par/, et dans la configuration de connexion côté front-end, il est très important de contenir la barre exactement comme défini danspath.
Côté front-end, l'adresse finale sera public + path, voir l'exemple :
...
"public": "/ws/pool",
"path": "/",
...
Donc l'adresse finale côté front-end sera /ws/pool/, avec la barre à la fin.
service
Adresse de la base de service back-end de l'application qui traite les connexions et la communication via WebSocket.
hosts
Optionnellement, nous pouvons définir les hôtes sur lesquels nos endpoints sont disponibles, c'est-à-dire que dans la connexion arrive l'adresse de l'hôte dans l'en-tête HTTP, exactement cet hôte en considérant uniquement l'information du domaine ou de l'IP et en écartant l'indication du port, c'est ce qui est utilisé pour associer et trouver l'endpoint correspondant.
Par conséquent, l'endpoint pour fonctionner correctement doit combiner
hostetpublic+path, de la manière suivante :
ws://[host]:9000[public][path]En production avec NGINX, proxy inverse et SSL, cela doit ressembler à ceci :
wss://[host][public][path]
Netuno définit certains domaines locaux avec le préfixe du nom de l'app, qui s'associe automatiquement comme valeur par défaut pour les hôtes des endpoints, en plus pour l'app en développement il ajoute aussi localhost et 127.0.0.1 aux hôtes par défaut, c'est pourquoi la définition des hôtes est optionnelle, car il y a cette configuration automatique.
Les hôtes suivants sont définis automatiquement pour les endpoints, si le nom de l'app est monapp :
- Celui-ci pointe directement vers l'IP locale 127.0.0.1 :
monapp.local.netu.no
- Ceux-ci, pour fonctionner, il est nécessaire de les enregistrer dans le fichier hosts du système d'exploitation :
monapp.localhostmonapp.local
Si le nom de l'app est mon_app, alors les hôtes automatiques deviennent :
- Celui-ci pointe directement vers l'IP locale 127.0.0.1 :
mon-app.local.netu.no
- Ceux-ci, pour fonctionner, il est nécessaire de les enregistrer dans le fichier hosts du système d'exploitation :
mon-app.localhostmon-app.local
Dans le domaine, le
_(tiret bas) n'est pas autorisé et dans le nom des apps le-(tiret) n'est pas autorisé, c'est pourquoi en externe nous utilisons le-(tiret) dans l'adresse, mais Netuno transforme automatiquement le-(tiret) en_(tiret bas) pour trouver la bonne application.
L'application par défaut ou l'application définie à l'initialisation du serveur Netuno possède en plus les hôtes suivants :
localhost127.0.0.1
Pour définir des hôtes personnalisés, nous utilisons les configurations suivantes :
...
"ws": {
"endpoints": [
{
"hosts": ["mon-domaine1.com", "www.mon-domaine1.com"],
...
},
{
"hosts": ["mon-domaine2.com", "www.mon-domaine2.com"],
...
}
]
},
...
Si tous les endpoints fonctionnent pour les mêmes hôtes, alors nous pouvons définir globalement de cette façon :
...
"ws": {
"hosts": ["mon-domaine.com", "www.mon-domaine1.com"],
"endpoints": [
...
]
},
...
Assurez-vous que dans l'adresse finale du WebSocket vous utilisez toujours un domaine ou une IP qui soit un hôte valide, sinon cela ne fonctionnera pas car l'endpoint ne pourra pas être trouvé en interne.
Service pour les Endpoints
POST = nouvelle connexion
Le service configuré avec la méthode POST est exécuté lorsqu'une nouvelle connexion est établie, par exemple :
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
const uidSalle = _ws.path().getString("uid")
const dbSalle = _db.get("salle", uidSalle)
if (dbSalle == null) {
_log.warn(`Salle invalide ${uidSalle}.`)
_ws.close()
} else {
_log.info(`Nouvelle session WebSocket : ${_ws.sessionId()}`)
}
const uidSalle = _ws.path().getString("uid")
const dbSalle = _db.get("salle", uidSalle)
if (dbSalle == null) {
_log.warn(`Salle invalide ${uidSalle}.`)
_ws.close()
} else {
_log.info(`Nouvelle session WebSocket : ${_ws.sessionId()}`)
}
uidSalle = _ws.path().getString("uid")
dbSalle = _db.get("salle", uidSalle)
if dbSalle == null:
_log.warn(f"Salle invalide {uidSalle}.")
_ws.close()
else:
_log.info(f"Nouvelle session WebSocket : {_ws.sessionId()}")
uidSalle = _ws.path().getString("uid")
dbSalle = _db.get("salle", uidSalle)
if dbSalle == null
_log.warn("Salle invalide #{uidSalle}.")
_ws.close()
else
_log.info("Nouvelle session WebSocket : #{_ws.sessionId()}")
end
val uidSalle = _ws.path().getString("uid")
val dbSalle = _db.get("salle", uidSalle)
if (dbSalle == null) {
_log.warn("Salle invalide ${uidSalle}.")
_ws.close()
} else {
_log.info("Nouvelle session WebSocket : ${_ws.sessionId()}")
}
def uidSalle = _ws.path().getString("uid")
def dbSalle = _db.get("salle", uidSalle)
if (dbSalle == null) {
_log.warn("Salle invalide ${uidSalle}.")
_ws.close()
} else {
_log.info("Nouvelle session WebSocket : ${_ws.sessionId()}")
}
Normalement, le
_ws.sessionId()est enregistré dans la base de données pour être utilisé dans de futures communications.
PUT = nouveau message
Si le message reçu n'est pas au format JSON, ou s'il s'agit d'un JSON qui n'est pas au format pris en charge pour l'exécution d'un service spécifique (service), alors ce service générique sera exécuté :
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`La session ${_ws.sessionId()} a envoyé ce message : ${_ws.message()}`)
_log.info(`La session ${_ws.sessionId()} a envoyé ce message : ${_ws.message()}`)
_log.info(f"La session {_ws.sessionId()} a envoyé ce message : {_ws.message()}")
_log.info("La session #{_ws.sessionId()} a envoyé ce message : #{_ws.message()}")
_log.info("La session ${_ws.sessionId()} a envoyé ce message : ${_ws.message()}")
_log.info("La session ${_ws.sessionId()} a envoyé ce message : ${_ws.message()}")
DELETE = fermeture de la connexion
Est exécuté lorsqu'une déconnexion du navigateur du client se produit.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`La session ${_ws.sessionId()} s'est déconnectée.`)
_log.info(`La session ${_ws.sessionId()} s'est déconnectée.`)
_log.info(f"La session {_ws.sessionId()} s'est déconnectée.")
_log.info("La session #{_ws.sessionId()} s'est déconnectée.")
_log.info("La session ${_ws.sessionId()} s'est déconnectée.")
_log.info("La session ${_ws.sessionId()} s'est déconnectée.")
GET = flux continu de messages
Démarre l'exécution lorsqu'une nouvelle connexion du navigateur du client se produit, et l'exécution est maintenue jusqu'à la fin de la connexion.
Autrement dit, le service pour la méthode HTTP GET reçoit continuellement les messages provenant du client en stream, c'est-à-dire qu'il s'agit d'un flux ouvert et continu de messages qui peut à la fois recevoir et envoyer.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
}
_ws.sendText("Ok ! Le message a été reçu avec succès.")
}
}
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
}
_ws.sendText("Ok ! Le message a été reçu avec succès.")
}
}
if _ws.isStream():
while _ws.awaitStream():
if _ws.isBinaryStreamed():
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
elif _ws.isTextStreamed():
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
_ws.sendText("Ok ! Le message a été reçu avec succès.")
if _ws.isStream()
while _ws.awaitStream()
if _ws.isBinaryStreamed()
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
elsif _ws.isTextStreamed()
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
end
_ws.sendText("Ok ! Le message a été reçu avec succès.")
end
end
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
}
_ws.sendText("Ok ! Le message a été reçu avec succès.")
}
}
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nouveau message binaire reçu : "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nouveau message texte reçu : "+ _ws.textStreamed())
}
_ws.sendText("Ok ! Le message a été reçu avec succès.")
}
}
Le if avec _ws.isStream() fait en sorte de vérifier s'il s'agit bien d'une requête interne gérée par WebSocket pour réaliser le stream.
Dans le while avec _ws.awaitStream(), il attend qu'un nouveau flux de message arrive, c'est-à-dire qu'il attend l'arrivée d'un nouveau message. Dès qu'il y a un nouveau message, le code interne du while est exécuté.
Le
_ws.awaitStream()fonctionne comme une boucle infinie en attente d'un nouveau message.
Ensuite, dans les ifs internes du while, le type de message est vérifié, si le message est binaire ou texte, dans les deux cas le contenu de ces messages est présenté dans les logs. Remarquez que n'importe quel format de message peut être utilisé, pouvant être traité de n'importe quelle manière qui soit convenable.
Enfin, dans _ws.sendText, un message de réponse est envoyé au client, confirmant la réception.
Chaque exécution du service
GETsera une connexion active.
Lorsque la connexion avec l'endpoint WebSocket est terminée, alors l'exécution de ce service est terminée en interne.
Communication
sendService
Exécute le service et envoie la sortie générée à un client spécifique connecté via WebSocket, utilise l'ID de connexion du client (sessionId).
Exemple d'appel de service
Le code ci-dessous peut être utilisé dans le service de méthode PUT de l'endpoint (service), remarquez que _ws.sessionId() est utilisé pour obtenir l'ID de la session de connexion du client ; si l'identifiant de session est enregistré dans la base de données, sendService pourrait être exécuté dans n'importe quel autre service à n'importe quel moment.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identification de la connexion WebSocket actuelle.
_val.map() // Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", // Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identification de la connexion WebSocket actuelle.
_val.map() // Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", // Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Identification de la connexion WebSocket actuelle.
_val.map() # Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", # Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Identification de la connexion WebSocket actuelle.
_val.map() # Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", # Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
val uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identification de la connexion WebSocket actuelle.
_val.map() // Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", // Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
final uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identification de la connexion WebSocket actuelle.
_val.map() // Paramétrage du service qui sera exécuté.
.set("method", "POST")
.set("service", "/services/salle/participant/nouveau")
.set(
"data", // Paramètres qui seront transmis au service et reçus via _req.
_val.map()
.set("uid", uid)
)
)
Autrement dit, dans la configuration, le path configuré dans l'endpoint a la valeur /{uid}, ce qui permet au client de se connecter via l'adresse publique :
/ws/salle/d641f095-30eb-4025-a39c-b2e3e497eab7
Donc la valeur de uid sera d641f095-30eb-4025-a39c-b2e3e497eab7.
Ensuite, le service situé à l'URL /services/salle/participant/nouveau sera exécuté et la valeur de uid est transmise.
Le uid peut être obtenu dans le code du service exécuté, comme un paramètre de la requête HTTP, voir un exemple :
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
// L'UID envoyé est obtenu directement dans la requête.
const uid = _req.getString('uid')
// L'UID envoyé est obtenu directement dans la requête.
const uid = _req.getString('uid')
# L'UID envoyé est obtenu directement dans la requête.
uid = _req.getString('uid')
# L'UID envoyé est obtenu directement dans la requête.
uid = _req.getString('uid')
// L'UID envoyé est obtenu directement dans la requête.
val uid = _req.getString('uid')
// L'UID envoyé est obtenu directement dans la requête.
final uid = _req.getString('uid')
broadcastService
Pour réaliser le broadcast, il faut le nom de l'endpoint WebSocket, définir le chemin (path) si applicable, et le service qui sera exécuté.
La sortie du service sera envoyée à toutes les connexions actives sur l'
endpointspécifié.
Exemple de broadcast
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_ws.broadcastService(
"admin", // Nom de l'endpoint WebSocket.
"/", // Chemin configuré dans l'endpoint (path).
_val.map() // Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
_ws.broadcastService(
"admin", // Nom de l'endpoint WebSocket.
"/", // Chemin configuré dans l'endpoint (path).
_val.map() // Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
_ws.broadcastService(
"admin", # Nom de l'endpoint WebSocket.
"/", # Chemin configuré dans l'endpoint (path).
_val.map() # Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
_ws.broadcastService(
"admin", # Nom de l'endpoint WebSocket.
"/", # Chemin configuré dans l'endpoint (path).
_val.map() # Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
_ws.broadcastService(
"admin", // Nom de l'endpoint WebSocket.
"/", // Chemin configuré dans l'endpoint (path).
_val.map() // Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
_ws.broadcastService(
"admin", // Nom de l'endpoint WebSocket.
"/", // Chemin configuré dans l'endpoint (path).
_val.map() // Paramétrage du service qui sera exécuté.
.set("service", "/services/salle/participant/liste")
)
Dans cet exemple, le contenu de la sortie générée par le service salle/participant/liste sera envoyé à tous ceux qui sont connectés à l'endpoint admin.
WS Client - NPM
Pour réaliser facilement l'intégration avec le frontend, le module NPM est mis à disposition :
Commande d'installation :
bun install @netuno/ws-client
import _ws from '@netuno/ws-client';
_ws.config({
url: 'ws://localhost:9000/ws/exemple/',
servicesPrefix: '/services',
autoReconnect: true,
connect: (event) => {
console.info('WebSocket connecté', event);
},
close: (event) => {
console.warn('WebSocket fermé', event);
},
error: (error) => {
console.error('Erreur WebSocket', error);
},
message: (data, event) => {
console.debug('Message WebSocket', data);
}
});
Assurez-vous que l'URL combine correctement les valeurs configurées dans l'endpoint pour
hostetpublic+path, de la manière suivante :
ws://[host]:9000[public][path]Quant à l'URL de production avec l'intégration NGINX, proxy inverse et SSL, elle sera probablement ainsi :
wss://[host][public][path]
Ensuite, vous pouvez initier la connexion :
_ws.connect();
Exécuter un service via WebSocket
Nous pouvons exécuter des services de l'API REST via la connexion WebSocket, cela signifie qu'aucune requête HTTP ne sera réalisée, donc les données de la requête sont envoyées comme message, et la réponse du service est obtenue comme un message reçu en retour.
Avant de commencer, nous devons créer un observateur de la réponse du service, par exemple :
const refEcouteur = _ws.addListener({
method: 'GET', // Optionnel ; omettez pour écouter toutes les méthodes.
service: 'chemin/de/mon/service/ici',
success: (data) => {
console.log('Réponse du service exécuté via WebSocket :', data);
},
fail: (error)=> {
console.log('Le service exécuté via WebSocket a échoué :', error);
}
});
Maintenant, nous pouvons envoyer une demande d'exécution du service, sachant que la réponse du service sera obtenue dans l'observateur créé ci-dessus, nous exécutons le service ainsi :
_ws.sendService({
method: 'GET', // Optionnel ; les requêtes utilisent GET lorsqu'il est omis.
service: 'chemin/de/mon/service/ici',
data: {
message: 'Bonjour...'
}
});
Pour supprimer l'observateur ajouté, utilisez :
_ws.removeListener(refEcouteur);
Et pour fermer la connexion WebSocket :
_ws.close();
NGINX Proxy inverse
Dans NGINX, le proxy inverse peut être configuré de cette façon :
location /ws {
proxy_pass http://localhost:9000;
proxy_set_header Host mon-app.local.netu.no;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
add_header X-Frame-Options "";
}
Sur la ligne 3 où est défini le
Host, il faut ajuster la partiemon-apppour le nom correspondant de l'application Netuno. Si le nom de l'application contient_, alors dans l'adresse duHostil sera remplacé par-, par exemple, l'applicationmon_appaura la valeur duHostainsi :mon-app.local.netu.no.
Redémarrez NGINX pour que la configuration prenne effet.
Les domaines configurés dans NGINX, c'est-à-dire le paramétrage du server_name, n'ont pas besoin d'être dans les hôtes de l'endpoint dans l'application Netuno, car dans la configuration de proxy ci-dessus, le Host dans l'en-tête HTTP est modifié pour une adresse standard qui est déjà configurée automatiquement par Netuno toujours, tant que le préfixe avec le nom de l'application est correct.
Si le serveur NGINX utilise le port 80, alors l'URL finale sera :
ws://[host][public][path]
Et avec SSL/HTTPS, si le serveur NGINX utilise le port 443, alors l'URL finale sera :
wss://[host][public][path]