Principes fondamentaux d'un transfert de données fiable

Principes fondamentaux d'un transfert de données fiable

À ceux qui cherche Dédié à la compréhension des réseaux et des protocoles.

Brièvement

L'article traite des bases d'une transmission de données fiable, met en œuvre des exemples sur Go, y compris UDP et TCP. Basé sur temps, два, trois et les livres "Réseaux informatiques. Approche descendante", sinon tout le monde ne parle que de Tannenbaum et Oliferov.

Protocole de couche transport

Fournit une connexion logique entre les processus d’application exécutés sur différents hôtes. Du point de vue de l'application, une connexion logique ressemble à un canal qui connecte directement les processus.

Principes fondamentaux d'un transfert de données fiable

Protocoles de couche transport sont pris en charge par les systèmes finaux, mais pas par les routeurs réseau (sauf - DPI). Du côté de l'expéditeur, la couche transport convertit les données de la couche application qu'elle reçoit du processus d'application émetteur en paquets de couche transport appelés segments.

Principes fondamentaux d'un transfert de données fiable

Cela se fait en divisant (si nécessaire) les messages de la couche application en fragments et en ajoutant un en-tête de couche transport à chacun d'eux.

Principes fondamentaux d'un transfert de données fiable

La couche transport transmet ensuite le segment à la couche réseau de l'expéditeur, où le segment est encapsulé dans un paquet de couche réseau (datagramme) et envoyé. À la réception, la couche réseau extrait le segment de la couche transport du datagramme et le transmet à la couche transport. Ensuite, la couche transport traite le segment reçu afin que ses données deviennent disponibles pour l'application réceptrice.

Principes fondamentaux d'un transfert de données fiable

Principes d'une transmission fiable des données

Transmission de données fiable sur un canal entièrement sécurisé

Le cas le plus simple. Le côté expéditeur reçoit simplement les données de la couche supérieure, crée un paquet les contenant et l'envoie au canal.

Serveur

package main

import (
    "log"
    "net"
)

func main() {
    // IP-адрес сервера и порт
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }

    // создаем сокет с портом
    serverConn, err := net.ListenUDP("udp", serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // отложенное закрытие соединения
    defer serverConn.Close()

    // создаем буфер для данных
    buf := make([]byte, 1024)

    // ждем соединение
    for {
        // читаем запрос
        n, addr, err := serverConn.ReadFromUDP(buf)
        // передаем данные в ВЕРХНИЙ уровень: в нашем случае stdout
        println(string(buf[0:n]), " form ", addr.IP.String())
        if err != nil {
            log.Fatal(err)
        }
        // ответа нет, т.к. это UDP + надежный канал
    }
}

Client

package main

import (
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    // IP-адрес сервера и порт
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }
    // локальный IP-адрес и порт
    localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
    if err != nil {
        log.Fatal(err)
    }
    // установка соединения
    conn, err := net.DialUDP("udp", localAddr, serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // отложенное закрытие соединения
    defer conn.Close()

    for {
        // получение данных от ВЕРХНЕГО уровня
        fmt.Print("Введите строчное предложение > ")
        var msg string
        _, err := fmt.Scanf("%s", &msg)
        if err != nil {
            log.Fatal(err)
        }
        // передается поток байт, а не строка
        buf := []byte(msg)
        // запись (передача) в соединение
        _, err = conn.Write(buf)
        if err != nil {
            log.Fatal(err)
        }
        // 1 секундочку
        time.Sleep(time.Second * 1)
    }
}

Transmission de données fiable sur un canal avec erreurs possibles

L'étape suivante consiste à supposer que tous les paquets transmis sont reçus dans l'ordre dans lequel ils ont été envoyés, mais que les bits qu'ils contiennent peuvent être corrompus en raison du fait que le canal transmet parfois des données avec des distorsions.

Principes fondamentaux d'un transfert de données fiable

Dans ce cas, les mécanismes suivants sont utilisés :

  • détection d'erreur;
  • retour;
  • retransmission.

Les protocoles de transfert de données fiables dotés de mécanismes similaires pour répéter la transmission plusieurs fois sont appelés protocoles ARQ (Automatic Repeat reQuest).
De plus, il convient de considérer la possibilité d'erreurs dans les réceptions, lorsque le destinataire ne recevra aucune information sur les résultats du transfert du dernier paquet.
La solution à ce problème, également utilisée dans TCP, consiste à ajouter un nouveau champ au paquet de données contenant le numéro de séquence du paquet.

Principes fondamentaux d'un transfert de données fiable

Transmission de données fiable sur un canal peu fiable sujet à la distorsion et à la perte de paquets

Malheureusement, en plus de la distorsion, il y a une perte de paquets dans le réseau.
Et pour résoudre ce problème, des mécanismes sont nécessaires :

  • déterminer le fait de perte de paquets ;
  • relivraison des paquets perdus au destinataire.

De plus, outre la perte du colis, il faut prévoir la possibilité de perte du récépissé ou, si rien n'est perdu, sa livraison avec un retard important. Dans tous les cas, on fait la même chose : le paquet est retransmis. Pour contrôler le temps, ce mécanisme utilise un compte à rebours, qui permet de déterminer la fin de l'intervalle d'attente. Donc dans le paquet net TCPKeepAlive est défini sur 15 secondes par défaut :

// defaultTCPKeepAlive is a default constant value for TCPKeepAlive times
// See golang.org/issue/31510
const (
    defaultTCPKeepAlive = 15 * time.Second
)

Le côté expéditeur doit démarrer un minuteur à chaque fois qu'un paquet est transmis (à la fois la première et la deuxième fois), gérer les interruptions du minuteur et l'arrêter.

Ainsi, nous nous sommes familiarisés avec les concepts clés des protocoles de transfert de données fiables :

  • sommes de contrôle ;
  • numéros de séquence des paquets ;
  • minuteries;
  • recettes positives et négatives.

Mais ce n'est pas tout!

Protocole de transfert de données fiable avec pipeline

Dans la variante que nous avons déjà envisagée, le protocole de livraison fiable est très inefficace. Il commence à « ralentir » la transmission fournie par le canal de communication à mesure que le RTT augmente. Pour augmenter son efficacité et mieux utiliser la capacité du canal de communication, le pipeline est utilisé.

Principes fondamentaux d'un transfert de données fiable

L’utilisation du pipeline conduit à :

  • augmenter la plage des numéros de séquence, puisque tous les paquets envoyés (à l'exception des retransmissions) doivent être identifiés de manière unique ;
  • la nécessité d’augmenter les tampons du côté de l’émission et de la réception.

La plage de numéros de séquence et les exigences en matière de taille de tampon dépendent des actions entreprises par le protocole en réponse à la corruption, à la perte et au retard des paquets. Dans le cas du pipelining, il existe deux méthodes pour corriger les erreurs :

  • renvoyer N paquets ;
  • répétition sélective.

Revenir en arrière sur N paquets - protocole à fenêtre glissante

Principes fondamentaux d'un transfert de données fiable

L'expéditeur doit prendre en charge trois types d'événements :

  • appel par un protocole de niveau supérieur. Lorsque la fonction d'envoi de données est appelée « par le haut », l'expéditeur vérifie d'abord le degré de remplissage de la fenêtre (c'est-à-dire la présence de N messages envoyés en attente de réception). Si la fenêtre est vide, un nouveau paquet est généré et transmis, et les valeurs des variables sont mises à jour. Sinon, le côté expéditeur renvoie les données à la couche supérieure, ce qui indique implicitement que la fenêtre est pleine. Généralement, la couche supérieure essaiera de transmettre à nouveau les données après un certain temps. Dans une application réelle, l'expéditeur mettrait probablement les données en mémoire tampon (au lieu de les envoyer immédiatement) ou disposerait d'un mécanisme de synchronisation (comme un sémaphore ou un indicateur) qui permettrait à la couche supérieure d'appeler la fonction d'envoi uniquement lorsque la fenêtre est vide. .
  • recevoir une confirmation. Dans le protocole, pour un paquet portant un numéro de séquence N, un accusé de réception général est émis indiquant que tous les paquets dont le numéro de séquence précède N ont été reçus avec succès.
  • le délai d'attente est expiré. Pour déterminer les faits de pertes et de retards de paquets et de réceptions, le protocole utilise une minuterie. Si le délai d'attente expire, le côté expéditeur renvoie tous les paquets envoyés sans accusé de réception.

Répétition sélective

Lorsque la taille de la fenêtre et le produit débit-délai de propagation sont importants, un grand nombre de paquets peuvent être dans le pipeline. Dans un tel cas, une seule erreur de paquet peut entraîner la retransmission d’un grand nombre de paquets, dont la plupart n’étaient pas nécessaires.

Exemple

Haut théorique les pratiques sont collectées dans la mise en œuvre pratique TCP. Et si quelqu'un sait comment faire au mieux - bienvenu.

Serveur

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "strings"
)

func main() {
    // создаем сокет с портом 
    ln, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatalln(err)
    }
    // ожидание вызова
    conn, _ := ln.Accept()

    for {
        // считывание данных
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // вывод сообщения в stdout
        fmt.Print("Message Received:", string(msg))
        // перевод строки в верхний регистр
        newMsg := strings.ToUpper(msg)
        // отправка данных
        conn.Write([]byte(newMsg + "n"))
    }
}

Client

package main

import (
    "bufio"
    "fmt"
    "log"
    "net"
    "os"
)

func main() {
    // установка соединения
    conn, err := net.Dial("tcp", "127.0.0.1:8081")
    if err != nil {
        log.Fatalln(err)
    }

    for {
        // считывание данных с stdin
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Text to send: ")
        // построчно
        text, err := reader.ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // отправка
        fmt.Fprintf(conn, text+"n")
        // прием
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // вывод полученного ответа
        fmt.Print("Msg from Server: " + msg)
    }
}

conclusion

Mécanismes pour garantir un transfert et une utilisation fiables des données

Mécanisme
Candidature, commentaire

Somme de contrôle
Utilisé pour détecter les erreurs de bits dans un paquet transmis

minuteur
Compte à rebours le délai d'expiration et indique quand il a expiré. Ce dernier signifie qu'avec un degré de probabilité élevé, le paquet ou sa réception est perdu pendant la transmission. Si un paquet est livré avec un retard, mais n'est pas perdu (expiration prématurée du délai d'attente), ou si un reçu est perdu, la retransmission entraîne un paquet en double du côté réception.

Numéro de série
Utilisé pour la numérotation séquentielle des paquets de données transmis de l'expéditeur au destinataire. Les lacunes dans les numéros de séquence des paquets reçus permettent au récepteur de détecter la perte de paquets. Les mêmes numéros de séquence de paquets signifient que les paquets sont des doublons les uns des autres

la confirmation
Généré par l'extrémité réceptrice et indiquant à l'extrémité émettrice que le paquet ou le groupe de paquets correspondant a été reçu avec succès. Généralement, l'accusé de réception contient les numéros de séquence des paquets reçus avec succès. Selon le protocole, on distingue les confirmations individuelles et collectives

Confirmation négative
Utilisé par le destinataire pour informer l'expéditeur que le paquet n'a pas été reçu correctement. Un accusé de réception négatif inclut généralement le numéro de séquence du paquet qui n'a pas été correctement reçu.

Fenêtre, convoyeur
Limitez la plage de numéros de séquence pouvant être utilisée pour transmettre des paquets. La multidiffusion et la prise de contact peuvent augmenter considérablement le débit du protocole par rapport à l'attente des accusés de réception. Comme nous le verrons, la taille de la fenêtre peut être calculée en fonction des capacités de réception et de mise en mémoire tampon de l'extrémité réceptrice, ainsi que du niveau de charge du réseau.

Plus d'exemples d'utilisation de Go pour la mise en réseau

В référentiels.

Source: habr.com

Ajouter un commentaire