Le protocole WebSocket, décrit dans la spécification RFC 6455 fournit un moyen dâéchanger des données entre le navigateur et le serveur via une connexion persistante. Les données peuvent être transmises dans les deux sens sous forme de âpaquetsâ, sans interrompre la connexion et de requêtes HTTP supplémentaires.
WebSocket est particulièrement adapté aux services qui nécessitent un échange de données continu, par exemple jeux en ligne, systèmes de trading en temps réel, etc.
Un exemple simple
Pour ouvrir une connexion websocket, nous devons créer new WebSocket en utilisant le protocole spécial ws dans lâurl :
let socket = new WebSocket("ws://javascript.info");
Il existe également un protocole chiffré wss://. Câest comme HTTPS mais pour les websockets.
wss://Le protocole wss:// est non seulement chiffré, mais également plus fiable.
Câest parce que les données ws:// ne sont pas chiffrées, par conséquent visibles pour tout intermédiaire. Les anciens serveurs proxy ne connaissent pas WebSocket, ils peuvent voir des en-têtes âétrangesâ et abandonner la connexion.
Dâun autre côté, wss:// est WebSocket sur TLS, (comme HTTPS est HTTP sur TLS), la couche de sécurité de transport chiffre les données à lâexpéditeur et déchiffre au récepteur. Les paquets de données sont donc transmis chiffrés via des proxys. Ils ne peuvent pas voir ce quâil y a à lâintérieur et les laisser passer.
Une fois le socket créé, nous devons écouter les événements quâil contient. Il y a au total 4 événements :
openâ Connection établie,messageâ Donnée reçue,errorâ erreur websocket,closeâ connexion fermée.
⦠Et si nous souhaitons envoyer quelque chose, alors socket.send(data) fera cela.
Voici un exemple :
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Connection established");
alert("Sending to server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// par exemple : processus serveur arrêté ou réseau en panne
// event.code est généralement 1006 dans ce cas
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error]`);
};
à des fins de démonstration, il y a un petit serveur server.js écrit en Node.js, pour lâexemple ci-dessus, en cours dâexécution. Il répond par âBonjour du serveur, Johnâ, puis attend 5 secondes et ferme la connexion.
Vous verrez donc les événements open â message â close.
On peut déjà parler de WebSocket. Câest assez simple, non ?
Approfondissont maintenant.
Ouvrir un websocket
Lorsque new WebSocket(url) est créé, il se connecte immédiatement.
Lors de la connexion, le navigateur (à lâaide des en-têtes) demande au serveur: âPrenez-vous en charge Websocket ?â Et si le serveur répond âouiâ, alors la conversation se poursuit dans le protocole WebSocket, qui nâest pas du tout HTTP.
Voici un exemple dâen-têtes de navigateur pour une demande faite par new WebSocket("wss://javascript.info/chat").
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Originâ lâorigine de la page client, par exemplehttps://javascript.info. Les objets WebSocket sont cross-origin par nature. Il nây a pas dâen-têtes spéciaux ou dâautres limitations. Les anciens serveurs ne sont pas en mesure de gérer WebSocket de toute façon, il nây a donc pas de problème de compatibilité. Mais lâen-têteOriginest important, car il permet au serveur de décider de discuter ou non en WebSocket avec ce site Web.Connection: Upgradeâ indique que le client souhaite modifier le protocole.Upgrade: websocketâ le protocole demandé est âwebsocketâ.Sec-WebSocket-Keyâ une clé générée aléatoirement par le navigateur pour la sécurité.Sec-WebSocket-Versionâ Version du protocole WebSocket, 13 est la version actuelle.
Nous ne pouvons pas utiliser XMLHttpRequest ou fetch pour effectuer ce type de requête HTTP, car JavaScript nâest pas autorisé à définir ces en-têtes.
Si le serveur accepte de passer à WebSocket, il doit envoyer le code de réponse 101 :
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Ici Sec-WebSocket-Accept est Sec-WebSocket-Key, recodé à lâaide dâun algorithme spécial. En le voyant, le navigateur comprend que le serveur prend réellement en charge le protocole WebSocket.
Ensuite, les données sont transférées en utilisant le protocole WebSocket, nous verrons bientôt sa structure (âframesâ). Et ce nâest pas du tout HTTP.
Extensions et sous-protocoles
Il peut y avoir des en-têtes supplémentaires Sec-WebSocket-Extensions et Sec-WebSocket-Protocol qui décrivent les extensions et les sous-protocoles.
Par exemple :
-
Sec-WebSocket-Extensions: deflate-framesignifie que le navigateur prend en charge la compression des données. Une extension est liée au transfert des données, câest une fonctionnalité qui étend le protocole WebSocket. Lâen-têteSec-WebSocket-Extensionsest envoyé automatiquement par le navigateur, avec la liste de toutes les extensions quâil prend en charge. -
Sec-WebSocket-Protocol: soap, wampsignifie que nous aimerions transférer non seulement toutes les données, mais les données SOAP ou les protocoles WAMP (âThe WebSocket Application Messaging Protocolâ). Les sous-protocoles WebSocket sont enregistrés dans le catalogue IANA. Donc, cet en-tête décrit les formats de données que nous allons utiliser.Cet en-tête facultatif est défini à lâaide du deuxième paramètre de
new WebSocket. Câest le tableau des sous-protocoles, par exemple si nous souhaitons utiliser SOAP ou WAMP :let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
Le serveur doit répondre avec une liste de protocoles et dâextensions quâil accepte dâutiliser.
Par exemple, la requête :
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
Réponse :
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
Ici, le serveur répond quâil prend en charge lâextension âdeflate-frameâ, et uniquement SOAP des sous-protocoles demandés.
Transfert de données
La communication WebSocket se compose de âframesâ â des fragments de données, qui peuvent être envoyés de chaque côté et peuvent être de plusieurs types :
- âtext framesâ â contiennent des données textuelles que les parties sâenvoient mutuellement.
- âbinary data framesâ â contiennent des données binaires que les parties sâenvoient mutuellement.
- âping/pong framesâ sont utilisés pour vérifier la connexion, envoyée par le serveur, le navigateur y répond automatiquement.
- il y a aussi la âconnection close frameâ et quelques autres frames de service.
Dans le navigateur, nous travaillons directement uniquement avec du texte ou des frames binaires.
La méthode WebSocket .send() peut envoyer du texte ou des données binaires.
Un appel socket.send(body) autorise body dans une chaîne de caractères ou un format binaire, y compris Blob, ArrayBuffer, etc. Aucun paramètre requis : il suffit de lâenvoyer dans nâimporte quel format.
Lorsque nous recevons les données, le texte vient toujours sous forme de chaîne de caractères. Et pour les données binaires, nous pouvons choisir entre les formats Blob et ArrayBuffer.
Cela est défini par la propriété socket.binaryType, câest "blob" par défaut, donc les données binaires viennent en tant quâobjets Blob.
Blob est un objet binaire de haut niveau, il sâintègre directement avec <a>, <img> et dâautres balises, câest donc un défaut sain. Mais pour le traitement binaire, pour accéder aux octets de données individuels, nous pouvons le changer en "arraybuffer" :
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data est soit une chaîne de caractères (si du texte), soit un arraybuffer (si binaire)
};
Limitation de débit
Imaginez, notre application génère beaucoup de données à envoyer. Mais lâutilisateur a une connexion réseau lente, peut-être sur un Internet mobile, en dehors dâune ville.
Nous pouvons appeler socket.send (data) encore et encore. Mais les données seront mises en mémoire tampon (stockées) en mémoire et envoyées uniquement aussi rapidement que la vitesse du réseau le permet.
La propriété socket.bufferedAmount stocke le nombre dâoctets qui sont mis en mémoire tampon à ce moment, en attente dâêtre envoyés sur le réseau.
Nous pouvons lâexaminer pour voir si le socket est réellement disponible pour la transmission.
// toutes les 100 ms examine le socket et envoi plus de données
// uniquement si toutes les données existantes ont été envoyées
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
Connexion fermée
Normalement, lorsquâune partie souhaite fermer la connexion (le navigateur et le serveur ont les mêmes droits), ils envoient un âconnection close frameâ avec un code numérique et une raison textuelle.
La méthode pour cela est :
socket.close([code], [reason]);
codeest un code de fermeture WebSocket spécial (facultatif)reasonest une chaîne de caractères qui décrit la raison de la fermeture (facultatif)
Ensuite, lâautre partie du gestionnaire dâévénements close obtient le code et la raison, par exemple :
// partie fermante :
socket.close(1000, "Work complete");
// l'autre partie
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
Valeurs de code les plus courantes :
1000â la closure normale par défaut (utilisée si aucuncodenâest fourni),1006â aucun moyen pour utiliser ce code manuellement, indique que la connexion a été perdue (pas de frame de fermeture).
Il existe dâautres codes comme :
1001â la partie sâen va, par exemple le serveur sâarrête ou un navigateur quitte la page,1009â le message est trop gros pour être traité,1011â erreur inattendue sur le serveur,- ⦠etc.
La liste complète se trouve dans le RFC6455, §7.4.1.
Les codes WebSocket sont un peu comme les codes HTTP, mais différents. En particulier, tous les codes inférieurs à 1000 sont réservés, il y aura une erreur si nous essayons de définir un tel code.
// en cas de rupture de connexion
socket.onclose = event => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};
Ãtat de connexion
Pour obtenir lâétat de la connexion, il existe en outre la propriété socket.readyState avec des valeurs :
0â âCONNECTINGâ: la connexion nâa pas encore été établie,1â âOPENâ: communicante,2â âCLOSINGâ: la connexion se ferme,3â âCLOSEDâ: la connexion est fermée.
Exemple de tchat
Passons en revue un exemple de discussion à lâaide de lâAPI WebSocket du navigateur et du module Node.js WebSocket https://github.com/websockets/ws. Nous porterons notre attention principalement sur le côté client, mais le serveur est également simple.
HTML: nous avons besoin dâun <form> pour envoyer des messages et dâun <div> pour les messages entrants :
<!-- message form -->
<form name="publish">
<input type="text" name="message">
<input type="submit" value="Send">
</form>
<!-- div with messages -->
<div id="messages"></div>
De JavaScript, nous voulons trois choses :
- Ouvrir la connexion.
- Lors de la soumission du formulaire â
socket.send(message)pour le message. - Sur le message entrant â lâajouter Ã
div#messages.
Voici le code :
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");
// envoyer un message depuis le formulaire
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// message reçu - affiche le message dans div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
Le code côté serveur dépasse un peu notre portée. Ici, nous utiliserons Node.js, mais ce nâest pas obligatoire. Dâautres plateformes ont également leurs moyens de travailler avec WebSocket.
Lâalgorithme côté serveur sera :
- Créer
clients = new Set()â un ensemble de sockets. - Pour chaque websocket accepté, lâajouter à lâensemble
clients.add (socket)et configurer lâécouteur dâévénementsmessagepour obtenir ses messages. - Lorsquâun message est reçu : parcourir les clients et lâenvoyer à tout le monde.
- Lorsquâune connexion est fermée :
clients.delete(socket).
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const clients = new Set();
http.createServer((req, res) => {
// ici, nous ne gérons que les connexions websocket
// dans un projet réel, nous aurions un autre code ici pour gérer les demandes non Websocket
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // la longueur maximale des messages sera de 50
for(let client of clients) {
client.send(message);
}
});
ws.on('close', function() {
clients.delete(ws);
});
}
Voici lâexemple fonctionnel :
Vous pouvez également le télécharger (bouton en haut à droite dans lâiframe) et lâexécuter localement. Nâoubliez pas dâinstaller Node.js et npm install ws avant lâexécution.
Résumé
WebSocket est un moyen moderne dâavoir des connexions navigateur-serveur persistantes.
- WebSockets nâont pas de limites cross-origin.
- Ils sont bien pris en charge dans les navigateurs.
- Peut envoyer/recevoir des chaînes de caractères et des données binaires.
LâAPI est simple.
Les méthodes :
socket.send(data),socket.close([code], [reason]).
Les événements :
open,message,error,close.
WebSocket ne comprend pas à lui seul la reconnexion, lâauthentification et de nombreux autres mécanismes de haut niveau. Il existe donc des bibliothèques client / serveur pour cela, et il est également possible dâimplémenter ces capacités manuellement.
Parfois, pour intégrer WebSocket dans un projet existant, les gens exécutent le serveur WebSocket en parallèle avec le serveur HTTP principal et partagent une seule base de données. Les requêtes à WebSocket utilisent wss://ws.site.com, un sous-domaine qui mène au serveur WebSocket, tandis que https://site.com va au serveur HTTP principal.
Certes, dâautres modes dâintégration sont également possibles.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)