Ecriture de serveurs WebSocket

Avant de débuter, vous devez connaître précisément le fonctionnement du protocole HTTP et disposer d'une certaine expérience sur celui-ci. Des connaissances sur les sockets TCP dans votre langage de développement est également précieux. Ce guide ne présente ainsi que le minimum des connaissances requises et non un guide ultime.

Lire la dernière spécification officielle sur les WebSockets RFC 6455. Les sections 1 et 4-7 sont particulièrement intéressant pour ce qui nous occupe. La section 10 évoque la sécurité et doit être connu et mis en oeuvre avant d'exposer votre serveur au-delà du réseau local / lors de la mise en production. 

Un serveur WebSocket est compris ici en "bas niveau" (c'est-à-dire plus proche du langage machine que du langage humaine ; "low level" n'a pas de traduction directe en français). Les WebSockets sont souvent séparés et spécialisés vis-à-vis de leurs homologues serveurs (pour des questions de montées en charge ou d'autres raisons), donc vous devez souvent utilisé un proxy inverse (c'est-à-dire de l'extérieur vers l'intérieur du réseau local, comme pour un serveur HTTP classique) pour détecter les "poignées de mains" spéficiques au WebSocket, qui précédent l'échange et permettent d'aiguiller les clients vers le bon logiciel. Dans ce cas, vous ne devez pas ajouter à votre serveur des cookies et d'autres méthodes d'authentification. 

La "poignée de mains" du WebSocket

En tout premier lieu, le serveur doit écouter les connections sockets entrant utilisant le protocole TCP standard. Suivant votre plateforme, celui-ci peut déjà le faire pour vous. Pour l'exemple qui suit, nous prenons pour acquis que votre serveur écoute le domaine exemple.com sur le port 8000 and votre serveur socket répond aux requêtes de type GET sur le chemin /chat

Attention: Si le serveut peut écouter n'importe quel port, mais que vous décidez de ne pas utiliser un port standard (80 ou 443 pour SSL), cela peut créer en avant des problèmes avec les parefeux et/ou les proxies. De plus, gardez en mémoire que certains navigateur Web (notablement Firefox 8+), n'autorise pas les connexions Webscoket non-SSL sur une page SSL. 

La poignée de mains est la partie "Web" dans les WebSockets : c'est le pont entre le protocole HTTP et celui WS. Durant cette poignée, les détails (les paramètres) de la connexion sont négociés et l'une des parties peut interromptre la transaction avant la fin si l'un des termes ne lui est pas autorisé / ne lui est pas possible. Le serveur doit donc être attentif à comprendre parfaitement les demandes et attentes du client, sinon un terme de la transaction pourrait être déclenché. 

La requête de poignée de mains côté client 

Même si vous construisez votre serveur au profit des WebSockets, votre client doit tout de même démarrer un processus dit de poignée de main. Vous devez donc savoir comment interprêter cette requête. En premier, le client enverra tout d'abord une requête HTTP correctement formée. La requête doit être à la version 1.1 ou supérieure et la méthode doit être de type GET : 

GET /chat HTTP/1.1
Host: exemple.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Le client peut soliciter des extensions de protocoles ou des sous-protocoles à cet instant ; voir Miscellaneous pour les détails. En outre, des en-têtes communs tel que User-Agent, Referer, Cookie ou des en-têtes d'authentification peuvent être envoyée par la même requête : leur usage est laissé libre car ils ne se rapportent pas directement à la WebSocket et au processus de poignée de main. A ce titre il semble préférable de les ignorer : d'ailleurs dans de nombreuses configurations communes, un proxy inverse les aura finalement déjà traitées. 

Si un des entêtes n'est pas compris ou sa valeur n'est pas correcte, le serveur devrait envoyer une réponse  "400 Bad Request" (erreur 400 : la requête est incorrecte) et clore immédiatement la connexion. Il peut par ailleurs indiquer la raison pour laquelle la poignée de mains a échoué dans le corps de réponse HTTP, mais le message peut ne jamais être affiché par la navigateur (en somme, tout dépend du comportement du client). Si le serveur ne comprend pas la version de WebSockets présentée, il doit envoyer dans la réponse un entête Sec-WebSocket-Version correspondant à la ou les version-s supportée-s. Ici le guide explique la version 13, la plus récente à l'heure de l'écriture du tutoriel (voir le tutoriel en version anglaise pour la date exacte ; il s'agit là d'une traduction). Maintenant, nous allons passer à l'entête attendu : Sec-WebSocket-Key.

Astuce:  Un grand nombre de navigateurs enverront un  Entête d'origine. Vous pouvez alors l'utiliser pour vérifier la sécurité de la transaction (par exemple vérifier la similitude des domaines, listes blanches ou noires, etc.) et éventuellement retourner une réponse 403 Forbidden si l'origine ne vous plaît pas. Toutefois garder à l'esprit que cet entête peut être simulé ou trompeur (il peut être ajouté manuellement ou lors du transfert). De nombreuses applications refusent les transactions sans celui-ci. 

Astuce: L'URI de la requête (/chat dans notre cas), n'a pas de signification particulièrement dans les spécifications en usage : elle permet simplement, par convention, de disposer d'une multitude d'applications en parallèle grâce à WebSocket. Par exemple exemple.com/chat peut être associée à une API/une application de dialogue multiutilisateurs lorsque /game invoquera son homologue pour un jeu. 

Note: Les codes réguliers (c-à-d défini par le protocole standard) HTTP ne peuvent être utilisés qu'avant la poignée : ceux après la poignée, sont définis d'une manière spécifique dans la section 7.4 de la documentation sus-nommée. 

La réponse du serveur lors de la poignée de mains 

Lorsqu'il reçoit la requête du client, le serveur doit envoyer une réponse correctement formée dans un format non-standard HTTP et qui ressemble au code ci-dessous. Gardez à l'esprit que chaque entête se termine par un saut de ligne : \r\n ; un saut de ligne doublé lors de l'envoi du dernier entête pour séparer du reste du corps (même si celui-ci est vide). 

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

En sus, le serveur peut décider de proposer des extensions de protocoles ou des sous-protocoles à cet instant ; voir Miscellaneous pour les détails. L'entête Sec-WebSocket-Accept qui nous intéresse : le serveur doit la former depuis l'entête Sec-WebSocket-Key que le client a envoyé précédemment. Pour l'obtenir, vous devez concaténater (rassembler) la valeur de Sec-WebSocket-Key and "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (valeur fixée par défaut : c'est une "magic string") puis procéder au hash par la méthode SHA-1 du résultat et retourner le format au format base64

Pour information / pour mémoire : Ce processus qui peut paraître inutilement complexe, permet de certifier que le serveur et le client sont bien sur une base WebSocket et non une requête HTTP  (qui serait alors mal interprétée). 

Ainsi si la clé (la valeur de l'entête du client) était "dGhlIHNhbXBsZSBub25jZQ==", le retour (Accept * dans la version d'origine du tutoriel) sera : "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=". Une fois que le serveur a envoyé les entêtes attendues, alors la poignée de mains est considérée comme effectuée et vous pouvez débuter l'échange de données ! 

Le serveur peut envoyer à ce moment, d'autres entêtes comme par exemple Set-Cookie, ou demander une authenficiation ou encore une redirection via les codes standards HTTP et ce avant la fin du processus de poignée de main. 

Suivre les clients confirmés 

Cela ne concerne pas directement le protocole WebSocket, mais mérite d'être mentionné à cet instant : votre serveur pourra suivre le socket client : il ne faut donc pas tenter une poignée de mains supplémentaire avec un client déjà confirmé. Un même client avec la même IP pourrait alors se connecter à de multiples reprises, mais être finalement rejeté et dénié par le serveur si les tentatives sont trop nombreuses selon les régles pouvant être édictées pour éviter les attaques dite de déni de service

L'échange de trames de données 

Le client ou le serveur peuvent choisir d'envoyer un message à n'importe quel moment à partir de la fin du processus de poignée de mains : c'est la magie des WebSockets (une connexion permanente). Cependant, l'extraction d'informations à partir des trames de données n'est pas une expérience si... magique. Bien que toutes les trames suivent un même format spécifique, les données allant du client vers le serveur sont masquées en utilisant le cryptage XOR (avec une clé de 32 bits). L'article 5 de la spécification décrit en détail ce processus. 

Format

Note du traducteur : Dans cette partie, payload équivaut en bon français à charge utile. C'est-à-dire les données qui ne font pas partie du fonctionnement de la trame mais de l'échange entre le serveur et le client. Ainsi j'ai traduit payload data comme des données utiles. 

Chaque trame (dans un sens ou dans un autre) suit le schéma suivant : 

 0               1               2               3              
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 4               5               6               7              
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
 8               9               10              11             
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
 12              13              14              15
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

RSV1-3 peuvent être ignorés, ils sont pour les extensions. 

Le masquage de bits indique simplement si le message a été codé. Les messages du client doivent être masquées, de sorte que votre serveur doit attendre qu'il soit à 1. (l'article 5.1 de la spécification prévoit que votre serveur doit se déconnecter d'un client si celui-ci envoie un message non masqué). Lors de l'envoi d'une trame au client, ne masquez pas et ne réglez pas le bit de masque - cela sera expliqué plus tard.

Note: Vous devez masquer les messages [NDT : seulement ? even peut avoir plusieurs sens dans ce cas] lorsque vous utilisez un socket sécurisé.

Les champs opcode définissent comme est interpêtée la charge utile (payload data) : ainsi 0x0 indique la consigne "continuer", 0x1 indique du texte (qui est systématiquement encodé en UTF-8), 0x2 pour des données binaires, et d'autres "codes de contrôle" qui seront évoqués par ailleurs. Dans cette version des WebSockets, 0x3 à 0x7 et 0xB à 0xF n'ont pas de significations particulières. 

Le bit FIN indique si c'est le dernier message de la série [NDT : pour la concaténation, pas la fin de la connexion elle-même]. S'il est à 0, alors le serveur doit attendre encore une ou plusieurs parties. Sinon le message est considéré comme complet. 

Connaître la taille des données utiles 

Pour (pouvoir) lire les données utiles, vous devez savoir quand arrêter la lecture dans le flux des trames entrantes vers le serveur. C'est pourquoi il est important de connaître la taille des données utiles. Et malheureusement ce n'est pas toujours simple. Voici quelques étapes essentielles à connaître : 

  1. (étape 1) Lire tout d'abord les bits 9 à 15 (inclu) et les interprêter comme un entier non-signé. S'il équivaut à 125 ou moins, alors il correspond à la taille totale de la charge utile.
    S'il vaut à 126, allez à l'étape 2 ou sinon, s'il vaut 127, allez à l'étape 3. 
  2. (étape 2) Lire les 16 bits supplémentaires et les interprêter comme précédent (entier non-signé). Vous avez la taille des données utiles. 
  3. (étape 3) Lire les 64 bits supplémentaires et les interprêter comme précédent (entier non-signé). Vous avez la taille des données utiles. Attention, le bit le plus significatif doit rester à 0. Vous avez la taille des données utiles. 

Lire et démasquer les données 

Si le bit MASK a été fixé (et il devrait l'être, pour les messages client-serveur), vous devez lire les 4 prochains octets (32 bits) :ils sont la clé de masquage. Une fois la clé de longueur de charge utile connue et de masquage décodée, vous pouvez poursuivre la lecture des autres bits comme étant les données utiles masquées. Par convention pour le reste du paragraphe, appelons-les données encodées, et la clé masque. Pour décoder les données, bouclez les octets du texte reçu en XOR avec l'octet du (i modulo 4) ième octet du masque. En voici le pseudo-code (JavaScript valide) : 

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

La variable DECODED est ici les données utilees à votre application - en fonction de l'utilisation ou non d'un sous-protocole (si c'est json, vous devez encore décoder les données utiles reçues avec le parseur JSON). 

La fragmentation des messages 

Les champs FIN et opcodes fonctionnent ensemble pour envoyer un message découpé en une multitutde de trames. C'est ce que l'on appele la fragmentation des messages. La fragmentation est seulement possible avec les opcodes de 0x0 à 0x2

Souvenez-vous de l'intérêt des l'opcode et ce qu'il implique dans l'échange des trames. Pour 0x1 c'est du texte, pour 0x2 des données binaires, etc. Toutefois pour 0x0, la frame est dite "continue" (elle s'ajoute à la précédente). En voici un exemple plus clair, où il y a en réalité deux textes de message (sur 4 trames différentes) : 

Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!

La première trame dispose d'un message en entier (FIN = 1 et optcode est différent de 0x0) : le serveur peut oeuvre à la requête reçue et y répondre. A partir de la seconde trame et pour les deux suivantes (soit trois trames), l'opcode à 0x1 puis 0x0 signifie qu'il s'agit d'un texte suivi du reste de contenu (0x1 = texte ; 0x0 = la suite). La 3e trame à FIN = 1 ce qui indique la fin de la requête. 
Voir la section 5.4 de la spécification pour les détails de cette partie. 

Pings-Pongs : le "coeur" des WebSockets

A n'importe quel moment après le processus de poignée de mains, le client ou le serveur peut choisir d'envoyer un ping à l'autre partie. Lorsqu'il est reçu, l'autre partie doit renvoyer dès possible un pong. Cette pratique permet de vérifier et de maintenir la connexion avec le client par exemple. 

Le ping ou le pong sont des trames classiques dites de contrôle. Les pings disposent d'un opcode à 0x9 et les pongs à 0xA. Lorsqu'un ping est envoyé, le pong doit disposer de la même donnée utile en réponse que le ping (et d'une taille maximum autorisé de 125). Le pong seul (c-à-d sans ping) est ignoré. 

Lorsque plusieurs pings sont envoyés à la suite, un seul pong suffit en réponse (le plus récent pour la donnée utile renvoyée). 

Clore la connexion 

La connexion peut être close à l'initiative du client ou du serveur grâce à l'envoi d'une trame de contrôle contenant des données spécifiques permettant d'interrompre la poignée de main (de lever définitivement le masque pour être plus précis ; voir la section 5.5.1). Dès la réception de la trame, le récepteur envoit une trame spécifique de fermeture en retour (pour signifier la bonne compréhension de la fin de connexion). C'est l'émetteur à l'origine de la fermeture qui doit clore la connexion ; toutes les données supplémentaires sont éliminés / ignorés. 

Diverses informations utiles

L'ensemble des codes, extensions et sous-protocoles liés aux WebSocket sont enregistrés dans le (regristre) IANA WebSocket Protocol Registry.

Les extensions et sous-protocoles des WebSockets sont négociés durant l'échange des entêtes de la poignée de mains. Si l'on pourrait croire qu'extensions et sous-protocles sont finalement la même chose, il n'en est rien : le contrôle des extensions agit sur les trames ce qui modifie la charge utile ; lorsque les sous-protocoles modifie seulement la charge utile et rien d'autre. Les extensions sont optionnelles et généralisées (par exemple pour la compression des données) ; les sous-protocoles sont souvent obligatoires et ciblés (par exemple dans le cadre d'une application de chat ou d'un jeu MMORPG). 

NDT : Les sous-extensions ou les sous-protocoles ne sont pas obligatoires pour l'échange de données par WebSockets ; mais l'esprit développé ici est de rendre soit plus efficace ou sécurisée la transmission (l'esprit d'une extension) ; soit de délimiter et de normaliser le contenu de l'échange (l'esprit d'un sous-protocole ; qui étend donc le protocole par défaut des WebSockets qu'est l'échange de texte simple au format UTF-8). 

Les extensions

Cette section mériterait d'être étendue et complétée. Merci de le faire si vous disposez des moyens pour le faire. 

L'idée des extensions pourrait être, par exemple, la compression d'un fichier avant de l'envoyer par courriel / email à quelqu'un : les données transférées ne changent pas de contenu, mais leur format oui (et leur taille aussi...). Ce n'est donc pas le format du contenu qui change que le mode transmission - c'est le principe des extensions en WebSockets, dont le principe de base est d'être un protocole simple d'échange de données. 

Les extensions sont présentés et expliqués dans les sections 5.8, 9, 11.3.2, and 11.4 de la documentation sus-nommées. 

Les sous-protocoles 

Les sous-protocoles sont à comparer à un schéma XML ou une déclaration de DocType. Ainsi vous pouvez utiliser seulement du XML et sa syntaxe et, imposer par le biais des sous-protocoles, son utilisation durant l'échange WebSocket. C'est l'intérêt de ces sous-protocoles : établir une structure définie (et intangible : le client se voit imposer la mise en oeuvre par le serveur), bien que les deux doivent l'accepter pour communiquer ensemble. 

Les sous-protocoles sont expliqués dans les sections 1.9, 4.2, 11.3.4, and 11.5 de la documentation sus-nommés. 

Exemple : un client souhaite demander un sous-protocole spécifique. Pour se faire, il envoie dans les entêtes d'origine du processus de poignées de mains : 

GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

Ou son équivalent : 

...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp

Le serveur doit désormais choisir l'un des protocoles suggérés par le client et qu'il peut prendre en charge. S'il peut en prendre plus d'un, le premier envoyé par le client sera privilégié. Dans notre exemple, le client envoit soap et wamp, le serveur qui supporte les deux enverra donc : 

Sec-WebSocket-Protocol: soap

Le serveur ne peut (ne doit) envoyer plus d'un entête Sec-Websocket-Protocol. S'il n'en supporte aucun, il ne doit pas renvoyer l'entête Sec-WebSocket-Protocol (l'entête vide n'est pas correct). Le client peut alors interrompre la connexion s'il n'a pas le sous-protocole qu'il souhaite (ou qu'il supporte). 

Si vous souhaitez que votre serveur puisse supporter certains sous-protocoles, vous pourriez avoir besoin d'une application ou de scripts supplémentaires sur le serveur. Imaginons par exemple que vous utilisiez le sous-protocole json - où toutes les données échangées par WebSockets sont donc formatés suivant le format JSON. Si le client sollicite ce sous-protocole et que le serveur souhaite l'accepter, vous devez disposer d'un parseur (d'un décodeur) JSON et décoder les données par celui-ci. 

Astuce: Pour éviter des conflits d'espaces de noms, il est recommandé d'utiliser le sous-protocole comme un sous-domaine de celui utilisé. Par exemple si vous utilisez un sous-protocole propriétaire qui utilise un format d'échange de données non-standard pour une application de chat sur le domaine exemple.com, vous devrirez utiliser : Sec-WebSocket-Protocol: chat.exemple.com. S'il y a différentes versions possibles, modifiez le chemin pour faire correspondre le path à votre version comme ceci : chat.exemple.com/2.0. Notez que ce n'est pas obligatoire, c'est une convention d'écriture optionnel et qui peut être utilisée d'une toute autre façon. 

Contenus associés

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : ynno, Nothus
 Dernière mise à jour par : ynno,