Contactez-nous

WebSockets

Découvrez WebSockets en Go : guide complet sur la communication bidirectionnelle et temps réel. Maîtrisez la création de serveurs, clients, la gestion des messages et les cas d'usage pour des applications interactives.

Introduction aux WebSockets : Le temps réel pour vos applications Go

Dans le monde moderne du développement web, les applications temps réel sont devenues omniprésentes. Des chats en direct aux jeux multijoueurs, en passant par les tableaux de bord interactifs et les mises à jour de données instantanées, la capacité à établir une communication bidirectionnelle et persistante entre le client et le serveur est cruciale. C'est là que le protocole WebSockets entre en jeu, offrant une alternative performante et efficace aux requêtes HTTP traditionnelles.

Contrairement à HTTP, qui est basé sur un modèle requête-réponse où le client initie toujours la communication, WebSockets établit une connexion persistante (full-duplex) entre le client et le serveur. Une fois la connexion WebSocket établie, le serveur et le client peuvent s'envoyer des messages à tout moment, de manière bidirectionnelle et en temps réel, sans avoir à ouvrir une nouvelle connexion pour chaque message. Cette nature persistante et bidirectionnelle fait de WebSockets le protocole de choix pour les applications temps réel.

Ce chapitre vous introduit au monde des WebSockets en Go. Nous allons explorer en détail le protocole WebSocket, comment créer des serveurs WebSocket en Go pour gérer les connexions entrantes, comment construire des clients WebSocket pour se connecter à des serveurs, comment envoyer et recevoir des messages en temps réel, et les cas d'utilisation typiques des WebSockets. Que vous souhaitiez développer un chat, un jeu en ligne, ou une application temps réel complexe, ce guide complet vous fournira les bases nécessaires pour maîtriser les WebSockets avec Go.

Le Handshake WebSocket : Etablir la connexion bidirectionnelle

Avant d'entamer une communication WebSocket bidirectionnelle, une étape cruciale est le handshake WebSocket. Ce handshake est un processus d'upgrade (mise à niveau) d'une connexion HTTP existante vers une connexion WebSocket. Il permet au client et au serveur de négocier et d'établir les paramètres de la connexion WebSocket.

Processus du Handshake WebSocket :

Le handshake WebSocket se déroule en plusieurs étapes :

  1. Requête HTTP Upgrade du client : Le client HTTP envoie une requête HTTP standard au serveur, mais avec des headers spécifiques indiquant une demande d'upgrade vers le protocole WebSocket. Ces headers incluent notamment :
    • Upgrade: websocket : Indique la demande d'upgrade vers WebSocket.
    • Connection: Upgrade : Indique que la connexion doit être mise à niveau.
    • Sec-WebSocket-Key : Une clé de sécurité générée aléatoirement par le client.
    • Sec-WebSocket-Version : La version du protocole WebSocket souhaitée par le client.
  2. Réponse HTTP Switching Protocols du serveur : Si le serveur accepte la demande d'upgrade WebSocket, il répond avec une réponse HTTP spéciale 101 Switching Protocols. Cette réponse confirme l'upgrade de la connexion et inclut des headers de réponse WebSocket, notamment :
    • Upgrade: websocket : Confirmation de l'upgrade vers WebSocket.
    • Connection: Upgrade : Confirmation de la mise à niveau de la connexion.
    • Sec-WebSocket-Accept : Une clé de sécurité calculée par le serveur à partir de la Sec-WebSocket-Key du client.
  3. Connexion WebSocket établie : Une fois le handshake réussi (réponse 101 Switching Protocols reçue par le client), la connexion HTTP est transformée en connexion WebSocket bidirectionnelle. Le client et le serveur peuvent désormais s'envoyer des messages WebSocket de manière asynchrone et en temps réel sur cette connexion persistante.

Handshake WebSocket en Go (serveur et client) :

La bibliothèque gorilla/websocket, un package tiers populaire pour WebSockets en Go ("github.com/gorilla/websocket"), simplifie grandement la gestion du handshake WebSocket côté serveur et côté client. Elle fournit des fonctions pour effectuer l'upgrade de la connexion HTTP côté serveur (Upgrader) et pour initier le handshake côté client (Dialer). Les exemples de code des sections suivantes illustreront l'utilisation de gorilla/websocket pour le handshake et la communication WebSocket en Go.

Serveur WebSocket en Go : Accepter les connexions et gérer les messages

Pour créer un serveur WebSocket en Go, vous utiliserez principalement la bibliothèque gorilla/websocket. Voici les étapes clés pour implémenter un serveur WebSocket simple :

Etapes pour créer un serveur WebSocket :

  1. Créer un Upgrader : Créez une instance de websocket.Upgrader. L'Upgrader configure le processus d'upgrade de la connexion HTTP vers WebSocket. Vous pouvez personnaliser certaines options de l'Upgrader (comme la taille du buffer, la gestion des headers, etc.).
  2. Définir un handler HTTP : Définissez un handler HTTP (fonction avec la signature func(http.ResponseWriter, *http.Request)) pour la route WebSocket (par exemple, /ws). Ce handler sera appelé lorsqu'un client tente d'établir une connexion WebSocket sur cette route.
  3. Effectuer l'upgrade de la connexion dans le handler : Dans le handler HTTP, utilisez la méthode Upgrader.Upgrade(w http.ResponseWriter, r *http.Request, headers http.Header) (*websocket.Conn, error) pour effectuer l'upgrade de la connexion HTTP vers WebSocket. Upgrade prend en arguments le ResponseWriter, le Request, et éventuellement des headers à ajouter à la réponse WebSocket. Elle retourne une connexion WebSocket *websocket.Conn (si l'upgrade réussit) et une erreur error (en cas d'échec de l'upgrade).
  4. Gérer la connexion WebSocket (goroutine par connexion) : Une fois l'upgrade réussie, vous obtenez une connexion WebSocket *websocket.Conn. Il est courant de lancer une nouvelle goroutine pour gérer chaque connexion WebSocket individuellement. Dans cette goroutine, vous pouvez :
    • Recevoir des messages du client : Utiliser la méthode conn.ReadMessage() pour lire les messages WebSocket envoyés par le client. ReadMessage() est une opération bloquante qui attend la réception d'un message.
    • Envoyer des messages au client : Utiliser les méthodes conn.WriteMessage(messageType int, data []byte) (pour envoyer des messages texte ou binaires) ou conn.WriteJSON(v interface{}) (pour envoyer des données JSON) pour envoyer des messages WebSocket au client.
    • Gérer les erreurs de connexion et de communication : Gérer les erreurs potentielles lors de la lecture et de l'écriture des messages, et lors de la fermeture de la connexion.
    • Fermer la connexion WebSocket : Lorsque la communication est terminée ou en cas d'erreur, fermer la connexion WebSocket avec la méthode conn.Close().
  5. Lancer le serveur HTTP : Utiliser http.ListenAndServe (ou http.ListenAndServeTLS pour HTTPS) pour lancer le serveur HTTP et le mettre en écoute sur l'adresse spécifiée.

Exemple de serveur WebSocket simple en Go :

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{ // Configuration de l'Upgrader
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func websocketHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil) // Upgrade de la connexion HTTP vers WebSocket
    if err != nil {
        log.Println("Erreur lors de l'upgrade de la connexion:", err)
        return
    }
    defer conn.Close() // Fermer la connexion WebSocket à la sortie du handler

    log.Println("Client WebSocket connecté")

    for {
        messageType, p, err := conn.ReadMessage() // Lecture d'un message depuis la connexion WebSocket (bloquant)
        if err != nil {
            log.Println("Erreur lors de la lecture du message:", err)
            break
        }

        log.Printf("Message reçu: type=%d, payload=%s\n", messageType, p)

        // Renvoyer un message (echo)
        err = conn.WriteMessage(messageType, p) // Envoi du message en réponse (echo)
        if err != nil {
            log.Println("Erreur lors de l'envoi du message:", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/ws", websocketHandler) // Route '/ws' gérée par 'websocketHandler'
    log.Println("Serveur WebSocket en écoute sur :8080")
    log.Fatal(http.ListenAndServe(":8080", nil)) // Lancement du serveur HTTP
}

Cet exemple illustre la création d'un serveur WebSocket simple en Go, capable d'accepter les connexions WebSocket sur la route /ws, de lire les messages envoyés par les clients, de les logger, et de renvoyer un message en réponse (fonctionnalité d'echo). Chaque connexion WebSocket est gérée dans une goroutine séparée (implicitement dans l'handler HTTP).

Client WebSocket en Go : Se connecter et échanger des messages

Pour créer un client WebSocket en Go, vous utiliserez également la bibliothèque gorilla/websocket. Voici les étapes clés pour implémenter un client WebSocket simple :

Etapes pour créer un client WebSocket :

  1. Créer un Dialer : Créez une instance de websocket.Dialer. Le Dialer configure le processus de connexion au serveur WebSocket. Vous pouvez personnaliser certaines options du Dialer (timeouts, headers, etc.). Dans de nombreux cas, vous pouvez utiliser le Dialer par défaut (websocket.DefaultDialer).
  2. Se connecter au serveur avec Dial : Utilisez la méthode Dial(urlStr string, headers http.Header) (conn *websocket.Conn, resp *http.Response, err error) du Dialer pour établir une connexion WebSocket avec le serveur. Dial prend en argument l'URL WebSocket du serveur (au format "ws://host:port/path" ou "wss://host:port/path" pour WebSocket Secure) et éventuellement des headers HTTP à envoyer lors du handshake. Elle retourne une connexion WebSocket *websocket.Conn (si la connexion réussit), la réponse HTTP *http.Response du handshake, et une erreur error (en cas d'échec de la connexion).
  3. Gérer la connexion WebSocket : Une fois la connexion WebSocket établie, vous obtenez une connexion *websocket.Conn. Vous pouvez ensuite :
    • Envoyer des messages au serveur : Utiliser les méthodes conn.WriteMessage(messageType int, data []byte) ou conn.WriteJSON(v interface{}) pour envoyer des messages WebSocket au serveur.
    • Recevoir des messages du serveur : Utiliser la méthode conn.ReadMessage() pour lire les messages WebSocket envoyés par le serveur.
    • Gérer les erreurs de connexion et de communication : Gérer les erreurs potentielles lors de la connexion, de la lecture et de l'écriture des messages, et lors de la fermeture de la connexion.
    • Fermer la connexion WebSocket : Lorsque la communication est terminée ou en cas d'erreur, fermer la connexion WebSocket avec la méthode conn.Close().

Exemple de client WebSocket simple en Go :

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/gorilla/websocket"
)

func main() {
    urlServeur := "ws://localhost:8080/ws" // URL du serveur WebSocket

    conn, _, err := websocket.DefaultDialer.Dial(urlServeur, nil) // Connexion au serveur WebSocket avec websocket.DefaultDialer.Dial
    if err != nil {
        log.Fatal("Erreur lors de la connexion au serveur WebSocket:", err)
        return
    }
    defer conn.Close() // Fermer la connexion WebSocket à la sortie de la fonction main

    log.Println("Connecté au serveur WebSocket", urlServeur)

    done := make(chan struct{}) // Channel pour signaler la fin de la communication

    // Goroutine pour recevoir les messages du serveur
    go func() {
        defer close(done)
        for {
            _, message, err := conn.ReadMessage() // Lecture des messages du serveur (bloquant)
            if err != nil {
                log.Println("Erreur lors de la lecture du message du serveur:", err)
                return
            }
            log.Printf("Message du serveur reçu: %s\n", message)
        }
    }()

    // Envoi de messages au serveur
    for i := 0; i < 5; i++ {
        message := fmt.Sprintf("Message client #%d", i+1)
        err = conn.WriteMessage(websocket.TextMessage, []byte(message)) // Envoi d'un message texte au serveur
        if err != nil {
            log.Println("Erreur lors de l'envoi du message:", err)
            return
        }
        log.Printf("Message client envoyé: %s\n", message)
        time.Sleep(1 * time.Second)
    }

    // Fermeture de la connexion après l'envoi des messages
    err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Client se déconnecte"))
    if err != nil {
        log.Println("Erreur lors de l'envoi du message de fermeture:", err)
        return
    }

    select {
    case <-done:
        // Goroutine de réception terminée
    case <-time.After(time.Second):
        log.Println("Timeout lors de la fermeture de la connexion")
    }
}

Cet exemple illustre la création d'un client WebSocket simple en Go, capable de se connecter à un serveur WebSocket, d'envoyer des messages texte au serveur, de recevoir et de logger les messages du serveur, et de fermer la connexion proprement.

Gestion des types de messages WebSocket : Text, Binary, Ping, Pong, Close

Le protocole WebSocket définit différents types de messages, permettant de distinguer le type de données transportées et de gérer les messages de contrôle de la connexion. Lorsque vous travaillez avec WebSockets en Go, il est important de comprendre et de gérer correctement ces différents types de messages.

Types de messages WebSocket principaux :

  • websocket.TextMessage : Messages texte (UTF-8) Utilisé pour envoyer et recevoir des messages textuels encodés en UTF-8. C'est le type de message le plus couramment utilisé pour les applications de chat, les mises à jour de données textuelles, etc.
  • websocket.BinaryMessage : Messages binaires Utilisé pour envoyer et recevoir des données binaires brutes. Utile pour transporter des images, des fichiers binaires, des données encodées, etc.
  • websocket.PingMessage : Messages Ping (contrôle de la connexion) Utilisé par le serveur ou le client pour envoyer des pings à l'autre partie afin de vérifier si la connexion est toujours active et réactive. Les messages Ping sont des messages de contrôle, et non des messages de données applicatives.
  • websocket.PongMessage : Messages Pong (réponse aux Pings) Utilisé pour répondre aux messages Ping. Lorsqu'un côté reçoit un message Ping, il doit répondre avec un message Pong en retour. Les messages Pong confirment que la connexion est toujours active et bidirectionnelle.
  • websocket.CloseMessage : Messages de fermeture de connexion Utilisé pour initier ou confirmer la fermeture de la connexion WebSocket de manière ordonnée. Les messages de fermeture contiennent un code de fermeture et un message de fermeture optionnel, indiquant la raison de la fermeture.

Gestion des types de messages lors de la lecture (conn.ReadMessage()) :

La méthode conn.ReadMessage() retourne le type de message (messageType int) en plus du payload du message (p []byte) et de l'erreur error.

messageType, p, err := conn.ReadMessage()
switch messageType {
case websocket.TextMessage:
    // Traitement d'un message texte
    fmt.Println("Message texte reçu :", string(p))
case websocket.BinaryMessage:
    // Traitement d'un message binaire
    fmt.Println("Message binaire reçu :", p)
case websocket.PingMessage:
    // Réception d'un Ping (répondre avec un Pong)
    conn.WriteMessage(websocket.PongMessage, nil) // Répondre avec un Pong
case websocket.PongMessage:
    // Réception d'un Pong (réponse à notre Ping)
    fmt.Println("Pong reçu du serveur.")
case websocket.CloseMessage:
    // Réception d'un message de fermeture de connexion
    fmt.Println("Message de fermeture reçu.")
    return // Quitter la goroutine de gestion de la connexion
default:
    fmt.Println("Type de message inconnu :", messageType)
}

Envoi de messages avec différents types (conn.WriteMessage()) :

La méthode conn.WriteMessage(messageType int, data []byte) prend en argument le type de message (websocket.TextMessage, websocket.BinaryMessage, websocket.PingMessage, websocket.PongMessage, websocket.CloseMessage) et le payload du message (data []byte). Pour les messages Ping, Pong et Close, le payload peut être nil ou vide.

err = conn.WriteMessage(websocket.TextMessage, []byte("Bonjour serveur !")) // Envoi d'un message texte
err = conn.WriteMessage(websocket.BinaryMessage, binaryData)          // Envoi de données binaires
err = conn.WriteMessage(websocket.PingMessage, nil)             // Envoi d'un Ping
err = conn.WriteMessage(websocket.PongMessage, nil)             // Envoi d'un Pong
err = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Fermeture ordonnée")) // Envoi d'un message de fermeture

La gestion correcte des différents types de messages WebSocket est essentielle pour construire des applications WebSocket robustes et conformes au protocole, en particulier pour la gestion des messages de contrôle de la connexion (Ping/Pong, Close) et pour la distinction entre les données textuelles et binaires.

Bonnes pratiques pour le développement WebSocket en Go

Pour développer des applications WebSocket robustes, performantes et maintenables en Go, voici quelques bonnes pratiques à suivre :

  • Utiliser une bibliothèque WebSocket éprouvée (gorilla/websocket) : Privilégiez l'utilisation de la bibliothèque gorilla/websocket ("github.com/gorilla/websocket"), qui est la bibliothèque la plus populaire et la plus mature pour WebSockets en Go. Elle offre une API complète, flexible et bien documentée, et gère de nombreux détails techniques du protocole WebSocket.
  • Gérer correctement les erreurs de connexion et de communication : Implémentez une gestion des erreurs robuste lors de la connexion au serveur WebSocket (côté client) et lors de l'upgrade de la connexion HTTP (côté serveur). Gérez également les erreurs potentielles lors de la lecture et de l'écriture des messages WebSocket, et lors de la fermeture de la connexion. Logguez les erreurs de manière informative pour faciliter le débogage.
  • Fermer les connexions WebSocket proprement : Assurez-vous de fermer les connexions WebSocket de manière ordonnée, tant côté serveur que côté client, lorsque la communication est terminée ou en cas d'erreur. L'échange de messages CloseMessage permet de signaler une fermeture ordonnée à l'autre partie. Utilisez defer conn.Close() pour garantir la fermeture de la connexion, même en cas de panic.
  • Gérer les différents types de messages WebSocket : Implémentez une logique de gestion appropriée pour les différents types de messages WebSocket (TextMessage, BinaryMessage, PingMessage, PongMessage, CloseMessage). En particulier, répondez aux messages PingMessage avec des messages PongMessage pour maintenir la connexion active (heartbeat).
  • Mettre en place un mécanisme de heartbeat (Ping/Pong) : Implémentez un mécanisme de heartbeat (battement de coeur) basé sur les messages Ping/Pong pour vérifier périodiquement si la connexion WebSocket est toujours active et réactive, et pour détecter les connexions inactives ou rompues. Envoyez des messages Ping à intervalles réguliers et attendez les messages Pong en réponse. Si un Pong n'est pas reçu dans un délai raisonnable, considérez la connexion comme rompue et fermez-la.
  • Gérer la concurrence et le multiplexage des connexions : Dans un serveur WebSocket, chaque connexion client doit généralement être gérée par une goroutine dédiée. Utilisez des mécanismes de concurrence appropriés (channels, mutex, worker pools) pour gérer les connexions concurrentes, le flux des messages, et la synchronisation entre les goroutines de gestion des connexions. Un routeur WebSocket (comme ceux fournis par certains frameworks web) peut faciliter l'organisation et le multiplexage des connexions.
  • Sécuriser les connexions WebSocket avec WSS (WebSocket Secure) : Pour les applications web en production, utilisez WSS (WebSocket Secure) au lieu de WS pour chiffrer les communications WebSocket via TLS/SSL. Utilisez wss:// dans l'URL WebSocket côté client, et configurez votre serveur HTTPS pour supporter les connexions WebSocket sécurisées (avec http.ListenAndServeTLS et des certificats SSL/TLS valides).

En appliquant ces bonnes pratiques, vous développerez des applications WebSocket en Go robustes, performantes, sécurisées et faciles à maintenir, en tirant pleinement parti de la puissance du protocole WebSocket et de la bibliothèque gorilla/websocket.