Lab 3 — Failure Detection

Dans ce TP, je crée un client UDP-like pour interagir avec un serveur. J'envoie des messages toutes les secondes et je récupère les réponses en temps réel via WebSocket. Comme le navigateur ne peut pas utiliser UDP direct, j'utilise WebSocket pour simuler.

1 — UDP client

Question 1 — Type et capacité du buffer

Je commence par observer la variable p qui stocke les données reçues depuis le serveur. Son type est []byte et sa capacité est celle définie à sa création, par exemple 2048.

Question 2 — Lecture UDP

La ligne _, err = bufio.NewReader(conn).Read(p) lit les données depuis le serveur. Je passe en paramètre le buffer p qui reçoit les octets. La fonction retourne le nombre d'octets lus et une erreur éventuelle. En deux lignes, ça donnerait:
reader := bufio.NewReader(conn)
n, err := reader.Read(p)

Question 3 — Client UDP complet

Je crée un client qui se connecte au serveur UDP à l'adresse 127.0.0.1:1234. Il envoie un message toutes les secondes et lit la réponse. Les logs s'affichent en direct via WebSocket.
package main

import (
	"bufio"
	"fmt"
	"net"
	"time"
)

func main() {
	p := make([]byte, 2048)
	conn, err := net.Dial("udp", "127.0.0.1:1234")
	if err != nil {
		fmt.Printf("Some error %v ", err)
		return
	}
	for {
		fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")
		_, err = bufio.NewReader(conn).Read(p)
		if err == nil {
			fmt.Printf("%s\n", p)
		} else {
			fmt.Printf("Some error %v\n", err)
		}
		time.Sleep(1 * time.Second)
	}
	conn.Close()
}

2 — UDP Server

Question 4 — Send Response

func sendResponse(conn *net.UDPConn, addr *net.UDPAddr) {
	_, err := conn.WriteToUDP([]byte("Hello UDP Client"), addr)
	if err != nil {
		fmt.Printf("Couldn't send response %v", err)
	}
}

Question 5 — Serveur UDP complet

package main

import (
	"fmt"
	"net"
)

func sendResponse(conn *net.UDPConn, addr *net.UDPAddr) {
	_, err := conn.WriteToUDP([]byte("Hello UDP Client"), addr)
	if err != nil {
		fmt.Printf("Couldn't send response %v", err)
	}
}

func main() {
	p := make([]byte, 2048)
	addr := net.UDPAddr{
		Port: 1234,
		IP:   net.ParseIP("127.0.0.1"),
	}
	for {
		ser, err := net.ListenUDP("udp", &addr)
		if err != nil {
			fmt.Printf("Some error %v\n", err)
			return
		}
		for {
			n, remoteaddr, err := ser.ReadFromUDP(p)
			fmt.Printf("Read a message from %v %s \n", remoteaddr, p[:n])
			if err != nil {
				fmt.Printf("Some error %v\n", err)
				continue
			}

			go sendResponse(ser, remoteaddr)
		}
	}
}
La première boucle est inutile, car le serveur UDP n’a besoin d’être ouvert qu’une seule fois. Une fois le socket créé avec ListenUDP, il reste actif en continu et peut traiter tous les messages entrants sans être recréé. Une seule ouverture suffit pour maintenir l’écoute et répondre aux clients. On peut donc simplifier le code ainsi:
package main

import (
	"fmt"
	"net"
)

func sendResponse(conn *net.UDPConn, addr *net.UDPAddr) {
	_, err := conn.WriteToUDP([]byte("Hello UDP Client"), addr)
	if err != nil {
		fmt.Printf("Couldn't send response %v", err)
	}
}

func main() {
	p := make([]byte, 2048)
	addr := net.UDPAddr{
		Port: 1234,
		IP:   net.ParseIP("127.0.0.1"),
	}
	ser, err := net.ListenUDP("udp", &addr)
	if err != nil {
		fmt.Printf("Some error %v\n", err)
		return
	}
	for {
		n, remoteaddr, err := ser.ReadFromUDP(p)
		fmt.Printf("Read a message from %v %s \n", remoteaddr, p[:n])
		if err != nil {
			fmt.Printf("Some error %v\n", err)
			continue
		}
		go sendResponse(ser, remoteaddr)
		
	}
}

Question 6 — Listen UDP

La fonction net.ListenUDP() sert à ouvrir un socket UDP et à le mettre en écoute sur une adresse précise. Une fois lancé, il reçoit tous les datagrammes envoyés à ce port sans établir de connexion persistante. Elle prend deux arguments : le protocole (souvent "udp") et un pointeur vers une net.UDPAddr qui contient l’adresse IP et le port.

Question 7 — Test Client - Server

Une fois le client et le serveur lancés, j’observe un petit échange comme pour le ping pong: le client envoie son message en boucle, le serveur le capte, répond, et les logs se succèdent sans pause.

3 — RTT Monitoring

Question 8 — Random Delay

Il suffit ici de modifier notre routine sendResponse pour y ajouter un délai aléatoire avant d'envoyer la réponse au client.
package main

import (
	"fmt"
	"math/rand"
	"net"
	"time"
)

func sendResponse(conn *net.UDPConn, addr *net.UDPAddr) {
	delay := rand.Intn(7000)
	fmt.Printf("Waiting %d ms before responding to %v\n", delay, addr)

	time.Sleep(time.Duration(delay) * time.Millisecond)
	_, err := conn.WriteToUDP([]byte("Hello UDP Client"), addr)
	if err != nil {
		fmt.Printf("Couldn't send response %v", err)
	}
}

func main() {
	p := make([]byte, 2048)
	addr := net.UDPAddr{
		Port: 1234,
		IP:   net.ParseIP("127.0.0.1"),
	}
	ser, err := net.ListenUDP("udp", &addr)
	if err != nil {
		fmt.Printf("Some error %v\n", err)
		return
	}
	for {
		n, remoteaddr, err := ser.ReadFromUDP(p)
		fmt.Printf("Read a message from %v %s \n", remoteaddr, p[:n])
		if err != nil {
			fmt.Printf("Some error %v\n", err)
			continue
		}
		go sendResponse(ser, remoteaddr)
	}
	
}

Question 9 — Measure Connectivity RTT

On utilise la fonction time.Now() avant d'envoyer le message au server puis on utilise la fonction time.Since() une fois la réception du message du server. Si le temps écoulé dépasse 5 secondes, on affiche un message d'erreur.
package main

import (
	"bufio"
	"fmt"
	"net"
	"time"
)

func main() {
  p := make([]byte, 2048)
  conn, err := net.Dial("udp", "127.0.0.1:1234")
  if err != nil {
  	fmt.Printf("Some error %v ", err)
  	return
  }
  for {
    start := time.Now()

    fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")
    _, err = bufio.NewReader(conn).Read(p) 

    // Mesure du temps écoulé
    elapsed := time.Since(start)  
    if elapsed > 5*time.Second {
      fmt.Printf("ERROR: Response took too long: %v\n", elapsed)
    } else {
      fmt.Printf("Response took %v\n", elapsed)
    }

    if err == nil {
      fmt.Printf("%s\n", p)
    } else {
      fmt.Printf("Some error %v\n", err)
    }
	  time.Sleep(1 * time.Second)
	}
	conn.Close()
}

Question 10 — Store RTT Measurements

On ajoute ici une slice globale pour stocker tous les RTTs et on calcule la moyenne et l'écart-type après chaque nouveau RTT.
package main

import (
  "bufio"
  "math"
  "fmt"
  "net"
  "time"
)

var rtts []time.Duration // Slice globale pour stocker tous les RTTs


func main() {
  p := make([]byte, 2048)
  conn, err := net.Dial("udp", "127.0.0.1:1234")
  if err != nil {
  	fmt.Printf("Some error %v ", err)
  	return
  }
  for {
    start := time.Now()

    fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")
    _, err = bufio.NewReader(conn).Read(p) 

    // Mesure du temps écoulé
    elapsed := time.Since(start).Seconds() // en secondes
    
    // Stocker dans la slice
    rtts = append(rtts, elapsed)

    if elapsed > 5*time.Second {
      fmt.Printf("ERROR: Response took too long: %v\n", elapsed)
    } else {
      fmt.Printf("Response took %v\n", elapsed)
    }

    
	  // Calculer la moyenne et l'écart-type après chaque nouveau RTT
    var sum float64
    for _, rtt := range rtts {
      sum += float64(rtt) / 1e9
    }
    avg := sum / float64(len(rtts))

    variance := 0.0
    for _, rtt := range rtts {
      variance += math.Pow(float64(rtt)/1e9 - avg, 2)
    }
    stdev := math.Sqrt(variance / float64(len(rtts)))


    fmt.Printf("Current RTT stats -> Average: %v  s | StdDev: %v  s\n\n", avg, stdev)


    if err == nil {
      fmt.Printf("%s\n", p)
    } else {
      fmt.Printf("Some error %v\n", err)
    }
	  time.Sleep(1 * time.Second)
	}
	conn.Close()
}

Question 11 — Timeout Condition

Pour ajouter le timeout, on a juste comme prévu utilisé la méthode SetReadDeadline sur la connexion UDP avec un timeout de 4 secondes avant la lecture. Ainsi, si la lecture dépasse ce délai, le paquet est considéré comme perdu et une erreur de timeout est renvoyée.
package main

import (
  "bufio"
  "fmt"
  "math"
  "net"
  "time"
)

var rtts []time.Duration // Slice globale pour stocker tous les RTTs

func main() {
  p := make([]byte, 2048)
  conn, err := net.Dial("udp", "127.0.0.1:1234")
  if err != nil {
  	fmt.Printf("Some error %v\n", err)
  	return
  }
  defer conn.Close()  
  for {
  	start := time.Now() 
  	// Envoyer le message
  	fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")  
  	// Définir un timeout pour la lecture
  	conn.SetReadDeadline(time.Now().Add(4 * time.Second)) 
  	_, err = bufio.NewReader(conn).Read(p)  
  	// Mesure du temps écoulé
  	elapsed := time.Since(start)
  	rtts = append(rtts, elapsed)  
  	if err != nil {
  		// Timeout
  		fmt.Printf("Packet lost or error: %v (took %v s)\n", err, elapsed.Seconds())
  	} else {
  		fmt.Printf("Response took %v s\n", elapsed.Seconds())
  	} 
  	// Calcul de la moyenne et de l'écart-type
  	var sum float64
  	for _, rtt := range rtts {
  		sum += rtt.Seconds()
  	}
  	avg := sum / float64(len(rtts)) 
  	var variance float64
  	for _, rtt := range rtts {
  		variance += math.Pow(rtt.Seconds()-avg, 2)
  	}
  	stdev := math.Sqrt(variance / float64(len(rtts))) 
  	fmt.Printf("Current RTT stats -> Average: %v  s | StdDev: %v  s\n\n", avg, stdev) 
  	time.Sleep(1 * time.Second)
  }
}

Question 12 — Count Sent and Lost Packets

On peut compter les paquets envoyés et perdus en utilisant deux variables entières. À chaque envoi de message, on incrémente le compteur de paquets envoyés. Si une erreur de timeout se produit lors de la lecture, on incrémente le compteur de paquets perdus. On affiche ensuite ces compteurs avec les statistiques RTT.
package main

import (
  "bufio"
  "fmt"
  "math"
  "net"
  "time"
)

var rtts []time.Duration // Slice globale pour stocker tous les RTTs

func main() {
  p := make([]byte, 2048)
  conn, err := net.Dial("udp", "127.0.0.1:1234")
  if err != nil {
  	fmt.Printf("Some error %v\n", err)
  	return
  }
  defer conn.Close()  
  sentCount := 0
  lostCount := 0  
  for {
  	start := time.Now() 
  	// Envoyer le message
  	fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")  
  	// Définir un timeout pour la lecture
  	conn.SetReadDeadline(time.Now().Add(4 * time.Second)) 
  	_, err = bufio.NewReader(conn).Read(p)  
  	// Mesure du temps écoulé
  	elapsed := time.Since(start)
  	rtts = append(rtts, elapsed)  
  	if err != nil {
  		// Timeout
      lostCount++
  		fmt.Printf("Packet lost or error: %v (took %v s)\n", err, elapsed.Seconds())
  	} else {
      sentCount++
  		fmt.Printf("Response took %v s\n", elapsed.Seconds())
  	} 
  	// Calcul de la moyenne et de l'écart-type
  	var sum float64
  	for _, rtt := range rtts {
  		sum += rtt.Seconds()
  	}
  	avg := sum / float64(len(rtts)) 
  	var variance float64
  	for _, rtt := range rtts {
  		variance += math.Pow(rtt.Seconds()-avg, 2)
  	}
  	stdev := math.Sqrt(variance / float64(len(rtts))) 
    lossPercent := float64(lostCount) / float64(sentCount) * 100  
    fmt.Printf("Current RTT stats -> Average: %v s | StdDev: %v s | Loss: %v\n\n",avg, stdev, lossPercent)  
  	time.Sleep(1 * time.Second)
  }
}

Question 13 — Periodic Statistics Output

On peut compter les paquets envoyés et perdus en utilisant deux variables entières. À chaque envoi de message, on incrémente le compteur de paquets envoyés. Si une erreur de timeout se produit lors de la lecture, on incrémente le compteur de paquets perdus. On affiche ensuite ces compteurs avec les statistiques RTT.
package main

import (
  "bufio"
  "fmt"
  "math"
  "net"
  "time"
)

var (
	rtts      []float64
	sentCount int
	lostCount int
)


func printStats(serverAddr *net.UDPAddr) {
  // En-tête (une seule fois)
  fmt.Printf("\n%-15s | %-14s | %-34s\n", "Host", "Pkt: Loss%% Sent", "RTT: Last Avg Best Wrst StDev")
  fmt.Println(strings.Repeat("-", 70))  
  for {
  	time.Sleep(2 * time.Second) 
  	// Rien à afficher
  	if len(rtts) == 0 && sentCount == 0 {
  		continue
  	} 
  	// Calculs
  	var sum, best, worst float64
  	best = math.MaxFloat64
  	worst = 0
  	last := 0.0 
  	for _, r := range rtts {
  		sum += r
  		if r < best {
  			best = r
  		}
  		if r > worst {
  			worst = r
  		}
  	} 
  	if len(rtts) > 0 {
  		last = rtts[len(rtts)-1]
  	} 
  	avg := 0.0
  	variance := 0.0
  	if len(rtts) > 0 {
  		avg = sum / float64(len(rtts))
  		for _, r := range rtts {
  			variance += math.Pow(r-avg, 2)
  		}
  		variance /= float64(len(rtts))
  	} 
  	stdev := math.Sqrt(variance)  
  	lossPercent := 0.0
  	if sentCount > 0 {
  		lossPercent = float64(lostCount) / float64(sentCount) * 100
  	} 
  	// Affichage
  	fmt.Printf("%-15s | %5.1f%% %4d      | %5.1f %5.1f %5.1f %5.1f %5.1f\n",
  		serverAddr.String(),
  		round2(lossPercent),
  		sentCount,
  		round2(last), round2(avg), round2(best), round2(worst), round2(stdev))
  }
}

func main() {
  p := make([]byte, 2048)
  conn, err := net.Dial("udp", "127.0.0.1:1234")
  if err != nil {
  	fmt.Printf("Some error %v\n", err)
  	return
  }
  defer conn.Close()   
  go func() {
    for {
    	start := time.Now() 
    	// Envoyer le message
    	fmt.Fprintf(conn, "Hi UDP Server, How are you doing?")  
    	// Définir un timeout pour la lecture
    	conn.SetReadDeadline(time.Now().Add(4 * time.Second)) 
    	_, err = bufio.NewReader(conn).Read(p)  
    	// Mesure du temps écoulé
    	elapsed := time.Since(start)
    	rtts = append(rtts, elapsed)  
    	if err != nil {
    		// Timeout
        lostCount++
    		fmt.Printf("Packet lost or error: %v (took %v s)\n", err, elapsed.Seconds())
    	} else {
        sentCount++
    		fmt.Printf("Response took %v s\n", elapsed.Seconds())
    	} 
    	// Calcul de la moyenne et de l'écart-type
    	var sum float64
    	for _, rtt := range rtts {
    		sum += rtt.Seconds()
    	}
    	avg := sum / float64(len(rtts)) 
    	var variance float64
    	for _, rtt := range rtts {
    		variance += math.Pow(rtt.Seconds()-avg, 2)
    	}
    	stdev := math.Sqrt(variance / float64(len(rtts))) 
      lossPercent := float64(lostCount) / float64(sentCount) * 100  
      fmt.Printf("Current RTT stats -> Average: %v s | StdDev: %v s | Loss: %v\n\n",avg, stdev, lossPercent)  
    	time.Sleep(1 * time.Second)
    }
  }()


	go printStats(serverAddr)

}