
Aqueles que 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 , incluindo UDP e TCP. Inspirado por , , 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.

são suportados pelos sistemas finais, mas não pelos roteadores de rede (exceto — ). 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.

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.

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.

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.

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.

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 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.

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

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 práticas estão reunidas na implementação prática . E se alguém souber como melhorar - .
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 .
Fonte: habr.com
