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()
}
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)
}