Websockets

De Justine's wiki
Aller à la navigation Aller à la recherche

Présentation

Qu'est-ce qu'un websocket ?

Un websocket est une connexion persistante entre un client et un serveur. Les websockets fournissent des communications full-duplex qui fonctionnent sur du HTTP, à travers une seule connection TCP.

Pourquoi des websockets ?

HTTP est, à la base, strictement unidirectionnel : un client demande une ressource, et le serveur la lui envoie. Le serveur ne peut pas envoyer de ressource sans que le client ne la lui demande. Auparavant, on utilisait une technique appellée "long polling" : le client demande une ressource avec un long timeout, et le serveur profite de ce timeout pour envoyer des données. Seulement, cela contraint le serveur à garder des ressources occupées sans raison.

Les websockets permettent d'envoyer des données sous la forme de messages, comme UDP (mais avec la fiabilité de TCP). Websocket utilise HTTP comme mécanisme de transport initial, mais garde la connexion TCP *alive* après réception de la réponse HTTP pour qu'elle puisse être utilisée pour envoyer des messages entre le client et le serveur. On peut ainsi construire des applications en temps réel.

Survol du protocole

La RFC 6455 définit le protocole WebSocket, et nous dit que "Le protocole consiste en un handshake d'ouverture suivi par un envoi de messages basique, mis sur du TCP" (traduction approx.).

WebSocket commence en tant requête / réponse HTTP standard; dans la chaîne de la réponse, le client demande à ouvrir une connexion WebSocket et le serveur lui répond si il le peut. Si ce handshake fonctionne, cela signifie que le client et le serveur se sont mis d'accord pour utiliser la connexion tcp qui avait été établie pour la requête HTTP, en tant que connexion WebSocket. Les données peuvent désormais circuler via un protocole d'envoi de messages basique. La connexion est fermée quand les deux parties se mettent d'accord.

Présentation du handshake

Les websockets n'utilisent pas le schéma http:// ou https:// (parce qu'ils ne suivent pas le protocole HTTP); ils utilisent le schéma ws:// ou wss:// si le flux est chiffré. Le reste de l'URI est similaire à une URI HTTP : un hôte, un port, un chemin et des paramètres de requête.

"ws:" "//" host [ ":" port ] path [ "?" query ]

Une connexion WebSocket ne peut être établie que vers une URI qui suit ce schéma; de la même façon, avec une URI ws:// ou wss://, client et serveur DOIVENT utiliser le protocole WebSocket.

Les connexions WS sont établies en "upgradant" une requête/réponse HTTP. Un client qui veut établir une connexion WS va envoyer une requête HTTP qui contient quelques headers obligatoires:

  • Connection : Upgrade -> Le header Connection définit si la connexion établie reste ouverte après que la transaction actuelle se termine (ou pas). Une valeur commune pour ce header est "keep-alive" pour s'assurer de la persistence de la connexion lors des requêtes suivantes envers le serveur. Durant le handshake d'ouvertue, Upgrade signale que l'on veut garder la connexion ouverte afin de l'utiliser pour des requêtes qui ne sont pas du HTTP.
  • Upgrade: websocket -> Ce HEader est utilisé par les clients pour demander au serveur de basculer sur un des protocoles listés, par ordre de préférence descendante. Ici, on spécifie websocket.
  • Sec-WebSocket-Key: q4xkcO32u266gldTuKaSOw== -> Cette valeur aléatoire unique (nonce, number used once) est générée par le client en prenant une valeur à 16 octets en base64.
  • Sec-WebSocket-Version: 13 -> La seule version acceptée est la 13e. Les autres sont invalides.

Le tout formerait une requête GET sur une URI ws:// qui ressemblerait à quelque chose comme cela: <source> GET ws://example.com:8181/ HTTP/1.1 Host: localhost:8181 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: q4xkcO32u266gldTuKaSOw== </source>

Quand le client a envoyé une requête initiale, il attend la réponse du serveur. La réponse doit contenir le code de réponse "HTTP 101 Switching Protocols", qui indique que le serveur change de protocole pour passer sur celui demandé par le client dans son champ Upgrade. Le serveur doit inclure des headers HTTP qui valide l'upgrade de la connexion: <source> HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: fA9dggdnMPU79lJgAE3W4TRnyDM= </source>

  • Connection: Upgrade -> La connexion a bien été upgradée
  • Upgrade: websocket -> La connexion a bien été upgradée en websocket
  • Sec-WebSocket-Accept : fA9dggdnMPU79lJgAE3W4TRnyDM= -> Il s'agit d'une valeur en base64, hashée en SHA-1. On obtient cette valeur en concaténant la valeur envoyée par le client et la valeur fixe 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 définie dans la RFC 6455. Ce système existe afin de s'assurer que les deux parties fassent bien du WebSocket; vu que WS réutilise des connexion HTTP, il existe en effet un risque que la requête soit interprétée comme du HTTP. (?)

Après cela, la connexion WebSocket est ouverte.

Le protocole WebSocket

WebSocket est un protocole framed (encadré ?), ce qui signifie que chaque chunk de données (les messages sont divisés en chunks) est divisé en un certain nombre de chunks, et la taille du chunk est encodée dans la trame. La trame contient un champ frame type, une longueur de payload, une portion données. Une vue de la trame est donnée dans la RFC 6455: <source>

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+ |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| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - +

Payload Data continued ...  :

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ </source> Nous n'allons voir ici que les parties importantes de cette trame.

  • Bit FIN -> Le premier bit de la trame. Ce bit est à 1 si la trame présente est le dernier chunk d'un message.
  • Bits RSV1, 2 et 3 -> Réservés pour utilisation future.
  • OPCODE sur 4 bits -> Chaque trame a un opcode qui détermine comment interpréter le payload:

- 0x00 : Cette trame continue le payload de la précédente - 0x01 : Trame de texte. Ces trames sont de l'UTF-8 encodées par le serveur - 0x02 : Trame binaire. Sont livrées telles quelles par le serveur - Ox03 à 0x07 : Réservé - 0x08 : Le client veut fermer la connexion - 0x09 : Trame de ping, utilisé comme "battement de coeur" pour s'assurer de la connexion; le receveur doit renvoyer un pong. - 0x0a : Trame de pong. - 0x0b à 0x0f : Réservé.

  • MASK -> Activer ce bit active le masquage. WebSocket requiert que toutes les données soient obfusquées en utilisant une clef random choisie par le client. Cette clef de masquage est combinée avec le payload en utilisant un XOR avant d'envoyer les données. Cela permet d'éviter que les caches n'interpètent les payload comme des données cachables, pour des raisons de sécurité (notamment pour le cache spoofing).
  • Payload len sur 7 bits -> Ce champ, avec son suivant Extended Payload Length, est utilisé pour encoder la taille totale du payload pour la trame. Sous 126 octets de payload, la taille est donnée dans le champ Payload len; au-dessus, on utilise le champ étendu.
  • Masking-key -> Comme vu sur la section dédiée au bit MASK, toutes les données sont chiffrées avec un chiffre sur 32 bits. Le champ masking-key la contient si le bit MASK est à 1; sinon, ce champ n'existe pas.
  • Payload-data -> Les données (gné).

Fermer une connexion WebSocket : le handshake de fermeture

La fermeture de connexion se fait par l'envoi d'une trame de fermeture avec l'opcode 0x08. En plus de l'opcode, la trame contient un corps décrivant les raisons de la fermeture. Client ou serveur, tout pair recevant une close frame doit en renvoyer une en réponse, et plus aucune donnée ne doit être envoyée. La connexion TCP est ensuite fermée.