Fundamentos da transmissão segura de dados

Fundamentos da transmissão segura de dados

Aqueles que tende querem entender redes e protocolos, esta é para vocês.

Resumidamente

Neste artigo, abordamos os fundamentos da transmissão confiável de dados, com exemplos em Go, incluindo UDP e TCP. Inspirado por um, dois, três e o livro "Redes de Computadores. Abordagem Descendente", já que todos falam apenas sobre Tanenbaum e Olicher.

O protocolo de nível de transporte

proporciona uma conexão lógica entre processos de aplicativo que estão sendo executados em diferentes hosts. A conexão lógica, do ponto de vista dos aplicativos, se apresenta como um canal que conecta diretamente os processos.

Fundamentos da transmissão segura de dados

Os protocolos de nível de transporte são suportados pelos sistemas finais, mas não pelos roteadores de rede (exceto — DPI). No lado do remetente, o nível de transporte transforma os dados do nível de aplicação que recebe do processo de aplicação remetente em pacotes de nível de transporte, chamados segmentos.

Fundamentos da transmissão segura de dados

Isso é feito dividindo (quando necessário) mensagens de nível de aplicação em fragmentos e adicionando um cabeçalho de nível de transporte a cada um deles.

Fundamentos da transmissão segura de dados

Em seguida, o nível de transporte envia o segmento para o nível de rede do remetente, onde o segmento é encapsulado em um pacote de nível de rede (datagrama) e enviado. No lado do receptor, o nível de rede extrai o segmento de nível de transporte do datagrama e o repassa para o nível de transporte. O nível de transporte, então, processa o segmento recebido para que seus dados fiquem disponíveis para o aplicativo receptor.

Fundamentos da transmissão segura de dados

Princípios da transmissão confiável de dados

A transmissão confiável de dados por um canal absolutamente confiável

É o caso mais simples. O lado remetente simplesmente recebe os dados do nível superior, cria um pacote contendo-os e os envia pelo canal.

Servidor

package main

import (
    "log"
    "net"
)

func main() {
    // Endereço IP do servidor e porta
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }

    // Criar um socket na porta
    serverConn, err := net.ListenUDP("udp", serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Fechar a conexão ao final
    defer serverConn.Close()

    // Criar um buffer para os dados
    buf := make([]byte, 1024)

    // Aguardar conexão
    for {
        // Ler solicitação
        n, addr, err := serverConn.ReadFromUDP(buf)
        // Passar dados para o NÍVEL SUPERIOR: neste caso, stdout
        println(string(buf[0:n]), " form ", addr.IP.String())
        if err != nil {
            log.Fatal(err)
        }
        // Não há resposta, pois é UDP + canal confiável
    }
}

Cliente

package main

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

func main() {
    // Endereço IP do servidor e porta
    serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:12000")
    if err != nil {
        log.Fatal(err)
    }
    // Endereço IP local e porta
    localAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
    if err != nil {
        log.Fatal(err)
    }
    // Estabelecendo conexão
    conn, err := net.DialUDP("udp", localAddr, serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    // Fechamento atrasado da conexão
    defer conn.Close()

    for {
        // Recebendo dados do nível SUPERIOR
        fmt.Print("Digite uma frase > ")
        var msg string
        _, err := fmt.Scanf("%s", &msg)
        if err != nil {
            log.Fatal(err)
        }
        // Transmitindo um fluxo de bytes, não uma string
        buf := []byte(msg)
        // Escrita (transmissão) na conexão
        _, err = conn.Write(buf)
        if err != nil {
            log.Fatal(err)
        }
        // Um segundo
        time.Sleep(time.Second * 1)
    }
}

Transmissão confiável de dados sobre um canal com possíveis erros

O próximo passo é supor que todos os pacotes transmitidos foram recebidos na ordem em que foram enviados, mas os bits neles podem ter sido corrompidos, pois o canal às vezes transmite dados com distorções.

Fundamentos da transmissão segura de dados

Nesse caso, são aplicados os mecanismos:

  • detecção de erros;
  • feedback;
  • retransmissão.

Protocolos de transmissão de dados confiáveis, que possuem esses mecanismos de retransmissão, são chamados de protocolos com solicitação automática de retransmissão (Automatic Repeat reQuest, ARQ).
Além disso, deve-se prever a possibilidade de erros nas confirmações, quando a parte receptora não recebe nenhuma informação sobre os resultados da transmissão do último pacote.
A solução para esse problema, usada também no TCP, consiste em adicionar um novo campo de dados ao pacote, contendo o número de sequência do pacote.

Fundamentos da transmissão segura de dados

Transmissão confiável de dados por um canal não confiável, que permite distorções e perda de pacotes

Ao mesmo tempo, com as distorções, infelizmente, a perda de pacotes também está presente na rede.
E para resolver esse problema são necessários mecanismos:

  • determinação da perda de pacotes;
  • retransmissão dos pacotes perdidos para a parte receptora.

Além da perda de pacotes, é necessário considerar a possibilidade de perda de confirmação ou, se nada for perdido, a entrega com um atraso significativo. Em todos os casos, a mesma ação é tomada: retransmissão do pacote. Para controlar o tempo neste mecanismo, é utilizado um temporizador que permite determinar o fim do intervalo de espera. Assim, no pacote net o parâmetro TCPKeepAlive está definido como 15 segundos por padrão:

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

A parte transmissora deve iniciar o temporizador toda vez que um pacote for enviado (tanto na primeira quanto na retransmissão), processar interrupções do temporizador e pará-lo.

Assim, conhecemos os principais conceitos dos protocolos de transmissão de dados confiáveis:

  • somas de verificação;
  • números sequenciais de pacotes;
  • temporizadores;
  • confirmações positivas e negativas.

Mas isso não é tudo!

Protocolo de transmissão de dados confiáveis com canalização

Na versão que já analisamos, o protocolo de entrega confiável é muito ineficiente. Ele começa a "atrasar" a transmissão fornecida pelo canal de comunicação ao aumentar o RTT. Para melhorar sua eficiência e melhor utilização da largura de banda do canal de comunicação, utiliza-se a canalização.

Fundamentos da transmissão segura de dados

A implementação da canalização leva a:

  • aumento do intervalo de números sequenciais, uma vez que todos os pacotes enviados (exceto retransmissões) devem ser identificados de forma exclusiva;
  • a necessidade de aumentar os buffers nas partes transmissora e receptora.

O intervalo de números sequenciais e os requisitos de tamanhos dos buffers dependem das ações que o protocolo executa em resposta à distorção, perda e atraso de pacotes. No caso da canalização, existem dois métodos de correção de erros:

  • retorno de N pacotes para trás;
  • repetição seletiva.

Retorno de N pacotes para trás — protocolo de janela deslizante

Fundamentos da transmissão segura de dados

O remetente deve manter três tipos de eventos:

  • chamada por um protocolo de nível superior. Quando a função de envio de dados é chamada "de cima", a parte remetente primeiro verifica o grau de preenchimento da janela (ou seja, a presença de N mensagens enviadas aguardando confirmações). Se a janela estiver vazia, um novo pacote é formado e enviado, enquanto os valores das variáveis são atualizados. Caso contrário, a parte remetente devolve os dados ao nível superior, que é uma indicação implícita de que a janela está cheia. Normalmente, o nível superior tenta reenviar os dados após algum tempo. Em uma aplicação real, o remetente provavelmente teria ou um buffer para armazenar os dados (em vez de enviá-los imediatamente), ou um mecanismo de sincronização (como um semáforo ou uma bandeira) que permitisse ao nível superior chamar a função de envio de dados somente quando a janela estivesse vazia.
  • recebendo a confirmação. No protocolo, um pacote com o número de sequência N recebe um reconhecimento geral, indicando que todos os pacotes com números de sequência anteriores ao N foram recebidos com sucesso.
  • expiração do intervalo de espera. Para determinar a perda e os atrasos de pacotes e confirmações, o protocolo utiliza um temporizador. Se o intervalo de espera expira, a parte remetente reenvia todos os pacotes enviados que não têm confirmação.

Repetição seletiva

Quando o tamanho da janela e o produto da largura de banda pelo tempo de propagação são grandes, um grande número de pacotes pode estar em trânsito. Nesse caso, um erro em um único pacote pode causar o reenvio de muitos pacotes, a maioria dos quais não era necessária.

Exemplo

Melhores teóricas práticas estão reunidas na implementação prática TCP. E se alguém souber como melhorar - welcome.

Servidor

package main

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

func main() {
    // cria um socket na porta 
    ln, err := net.Listen("tcp", ":8081")
    if err != nil {
        log.Fatalln(err)
    }
    // esperando a chamada
    conn, _ := ln.Accept()

    for {
        // lendo dados
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // exibindo a mensagem no stdout
        fmt.Print("Mensagem Recebida:", string(msg))
        // convertendo a string para maiúsculas
        newMsg := strings.ToUpper(msg)
        // enviando os dados
        conn.Write([]byte(newMsg + "n"))
    }
}

Cliente

package main

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

func main() {
    // estabelecimento da conexão
    conn, err := net.Dial("tcp", "127.0.0.1:8081")
    if err != nil {
        log.Fatalln(err)
    }

    for {
        // leitura de dados do stdin
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Texto a enviar: ")
        // linha por linha
        text, err := reader.ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // envio
        fmt.Fprintf(conn, text+"n")
        // recepção
        msg, err := bufio.NewReader(conn).ReadString('n')
        if err != nil {
            log.Fatalln(err)
        }
        // exibição da resposta recebida
        fmt.Print("Msg do Servidor: " + msg)
    }
}

Saída

Mecanismos que garantem a transmissão e utilização confiável de dados

O mecanismo
Aplicação, comentário

Checksum
Usado para detectar erros de bits no pacote transmitido

Temporizador
Contagem de tempo de espera e indicação de sua expiração. A última significa que, com alta probabilidade, o pacote ou seu reconhecimento foram perdidos durante a transmissão. Caso o pacote seja entregue com atraso, mas não perdido (expiração prematura do tempo de espera), ou haja a perda do reconhecimento, a retransmissão leva à duplicação do pacote no lado do receptor

Número sequencial
Usado para a numeração sequencial dos pacotes de dados transmitidos do remetente ao receptor. Quebras nos números sequenciais dos pacotes recebidos permitem que o receptor detecte a perda de pacotes. Números sequenciais idênticos significam que os pacotes se duplicam

Confirmação
Gerada pelo lado receptor e indica ao lado transmissor que o pacote correspondente ou grupo de pacotes foi recebido com sucesso. Normalmente, a confirmação contém os números sequenciais dos pacotes recebidos com sucesso. Dependendo do protocolo, existem confirmações individuais e em grupo

Confirmação negativa
Usado pelo receptor para informar ao remetente que o pacote foi recebido de forma incorreta. A confirmação negativa normalmente inclui o número sequencial do pacote que não foi recebido corretamente

Janela, pipeline
Limitam o intervalo de números de sequência que podem ser usados para a transmissão de pacotes. A transmissão em grupo e o handshake permitem aumentar significativamente a largura de banda dos protocolos em comparação com o modo de espera por confirmações. Como veremos, o tamanho da janela pode ser calculado com base nas capacidades de recebimento e buffer da parte receptora, bem como no nível de carga da rede.

Outros exemplos de uso do Go para trabalhar com redes.

Em repositório.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster