confiável é uma camada de confiabilidade para conexões UDP em Go.
Com apenas 9 bytes de sobrecarga de pacote, no máximo, o que é confiável para seu aplicativo baseado em UDP é:
** Este projeto ainda é um WIP! Examine o código-fonte, escreva alguns testes de unidade, ajude com a documentação ou abra um problema no Github se quiser ajudar ou tiver alguma dúvida!
confiável usa o mesmo layout de cabeçalho de pacote descrito em networkprotocol/reliable.io
.
Todos os pacotes começam com um único byte (8 bits) representando 8 flags diferentes. Os pacotes são sequenciais e numerados usando um número inteiro não assinado de 16 bits incluído no cabeçalho do pacote, a menos que o pacote esteja marcado como não confiável.
As confirmações de pacote (ACKs) são incluídas de forma redundante em cada pacote enviado usando um total de 5 bytes: dois bytes representando um número de sequência de pacote de 16 bits não assinado (ack) e três bytes representando um campo de bits de 32 bits (ackBits).
O layout do cabeçalho do pacote, assim como networkprotocol/reliable.io
, é codificado em delta e codificado em RLE para reduzir a sobrecarga de tamanho por pacote.
Dado um pacote que acabamos de receber de nosso par, para cada bit definido (i) no campo de bits (ackBits), marcamos um pacote que enviamos para ser reconhecido se seu número de sequência for (ack - i).
No caso do par A enviar pacotes para B, com B não enviando nenhum pacote para A, B enviará um pacote vazio para cada 32 pacotes recebidos de A para que A saiba que B reconheceu seus pacotes.
Mais explicitamente, um contador (lui) é mantido representando o último número de sequência de pacote consecutivo que recebemos e cujo reconhecimento informamos ao nosso par.
Por exemplo, se (lui) for 0, e tivermos enviado confirmações para pacotes cujos números de sequência são 2, 3, 4 e 6, e tivermos então reconhecido o número de sequência do pacote 1, então lui seria 4.
Após a atualização (lui), se os próximos 32 números de sequência consecutivos forem números de sequência de pacotes que recebemos anteriormente, incrementaremos (lui) em 32 e enviaremos um único pacote vazio contendo as seguintes confirmações de pacote: (ack=lui+31, ackBits=[lui,lui+31]).
Dois buffers de sequência de tamanho fixo são mantidos para pacotes que enviamos (wq) e pacotes que recebemos (rq). O tamanho fixado para esses buffers deve ser dividido igualmente no valor máximo de um número inteiro não assinado de 16 bits (65536). A estrutura de dados é descrita nesta postagem de Glenn Fiedler.
Acompanhamos um contador (oui), que representa o último número de sequência consecutivo de um pacote que enviamos e que foi reconhecido pelo nosso par. Por exemplo, se enviamos pacotes cujos números de sequência estão no intervalo [0, 256] e recebemos confirmações de pacotes (0, 1, 2, 3, 4, 8, 9, 10, 11, 12), então (oui) seria 4.
Seja cap(q) o tamanho fixo ou capacidade do buffer de sequência q.
Ao enviar pacotes, paramos e armazenamos intermitentemente o envio de pacotes se acreditarmos que o envio de mais pacotes iria estourar o buffer de leitura do nosso destinatário. Mais explicitamente, se ao próximo pacote que enviamos for atribuído um número de pacote maior que (oui + cap(rq)), paramos todos os envios até que (oui) tenha incrementado através do destinatário de um pacote do nosso par.
A lógica para retransmitir pacotes enviados obsoletos e não confirmados e manter confirmações foi considerada com crédito nesta postagem do blog por Glenn Fiedler.
Suspeita-se que os pacotes sejam perdidos se não forem reconhecidos pelo destinatário após 100 ms. Quando há suspeita de perda de um pacote, ele é reenviado. A partir de agora, os pacotes são reenviados no máximo 10 vezes.
Pode ser aconselhável não permitir que os pacotes sejam reenviados um número limitado de vezes e deixar isso para o desenvolvedor. No entanto, isso está aberto para discussão, o que estou feliz em ter em meu servidor Discord ou por meio de um problema no Github.
Em minha busca para encontrar uma solução viável contra o bloqueio inicial de TCP, examinei muitas bibliotecas UDP confiáveis em Go, com a maioria adequada principalmente para transferência de arquivos ou jogos:
Passando por todos eles, senti que eles fizeram um pouco demais por mim. Para meu trabalho e projetos paralelos, tenho trabalhado intensamente em protocolos de rede p2p descentralizados. A natureza desses protocolos é que eles sofrem muito com o bloqueio head-of-line do TCP operando em ambientes de alta latência/alta perda de pacotes.
Em muitos casos, muitos dos recursos fornecidos por essas bibliotecas não eram necessários ou, honestamente, parecia que seriam melhor manipulados e pensados pelo desenvolvedor que usa essas bibliotecas. Por exemplo:
Então, comecei a trabalhar em uma abordagem modular e decidi abstrair a parte de confiabilidade dos protocolos que construí em uma biblioteca separada.
Acho que essa abordagem é melhor em comparação com alternativas populares como QUIC ou SCTP, que podem, dependendo das circunstâncias, fazer um pouco demais por você. Afinal, obter apenas os bits de confiabilidade de um protocolo baseado em UDP corretos e bem testados já é bastante difícil.
net.PacketConn
para uma abstração mais refinada.net.UDPAddr
transmitido.confiável usa módulos Go. Para incluí-lo em seu projeto, execute o seguinte comando:
$ go get github.com/lithdew/reliable
Se você deseja apenas colocar um projeto ou demonstração em funcionamento rapidamente, use Endpoint
. Se precisar de mais flexibilidade, considere trabalhar diretamente com Conn
.
Observe que algum tipo de mecanismo keep-alive ou sistema de pulsação precisa ser inicializado na parte superior, caso contrário, os pacotes podem ser reenviados indefinidamente, pois não foram reconhecidos.
WithReadBufferSize
. O tamanho padrão do buffer de leitura é 256.WithWriteBufferSize
. O tamanho padrão do buffer de gravação é 256.WithResendTimeout
. O tempo limite de reenvio padrão é 100 milissegundos.WithEndpointPacketHandler
ou WithProtocolPacketHandler
. Por padrão, é fornecido um manipulador nulo que ignora todos os pacotes recebidos.WithEndpointErrorHandler
ou WithProtocolErrorHandler
. Por padrão, é fornecido um manipulador nulo que ignora todos os erros.WithBufferPool
. Por padrão, um novo buffer pool de bytes é instanciado. Um benchmark foi feito usando cmd/benchmark
do Japão para um servidor DigitalOcean 2GB/60 GB Disk/NYC3.
A tarefa de referência era enviar pacotes de 1.400 bytes do Japão para Nova York. Dada uma latência de ping de aproximadamente 220 milissegundos, a taxa de transferência foi de aproximadamente 1,2 MiB/s.
Benchmarks de testes unitários também foram realizados, conforme mostrado abaixo.
$ cat /proc/cpuinfo | grep 'model name' | uniq
model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
$ go test -bench=. -benchtime=10s
goos: linux
goarch: amd64
pkg: github.com/lithdew/reliable
BenchmarkEndpointWriteReliablePacket-8 2053717 5941 ns/op 183 B/op 9 allocs/op
BenchmarkEndpointWriteUnreliablePacket-8 2472392 4866 ns/op 176 B/op 8 allocs/op
BenchmarkMarshalPacketHeader-8 749060137 15.7 ns/op 0 B/op 0 allocs/op
BenchmarkUnmarshalPacketHeader-8 835547473 14.6 ns/op 0 B/op 0 allocs/op
Você pode executar o exemplo abaixo executando o seguinte comando:
$ go run github.com/lithdew/reliable/examples/basic
Este exemplo demonstra:
package main
import (
"bytes"
"errors"
"github.com/davecgh/go-spew/spew"
"github.com/lithdew/reliable"
"io"
"log"
"net"
"os"
"os/signal"
"sync"
"sync/atomic"
"time"
)
var (
PacketData = bytes . Repeat ([] byte ( "x" ), 1400 )
NumPackets = uint64 ( 0 )
)
func check ( err error ) {
if err != nil && ! errors . Is ( err , io . EOF ) {
log . Panic ( err )
}
}
func listen ( addr string ) net. PacketConn {
conn , err := net . ListenPacket ( "udp" , addr )
check ( err )
return conn
}
func handler ( buf [] byte , _ net. Addr ) {
if bytes . Equal ( buf , PacketData ) || len ( buf ) == 0 {
return
}
spew . Dump ( buf )
os . Exit ( 1 )
}
func main () {
exit := make ( chan struct {})
var wg sync. WaitGroup
wg . Add ( 2 )
ca := listen ( "127.0.0.1:44444" )
cb := listen ( "127.0.0.1:55555" )
a := reliable . NewEndpoint ( ca , reliable . WithEndpointPacketHandler ( handler ))
b := reliable . NewEndpoint ( cb , reliable . WithEndpointPacketHandler ( handler ))
defer func () {
check ( ca . SetDeadline ( time . Now (). Add ( 1 * time . Millisecond )))
check ( cb . SetDeadline ( time . Now (). Add ( 1 * time . Millisecond )))
close ( exit )
check ( a . Close ())
check ( b . Close ())
check ( ca . Close ())
check ( cb . Close ())
wg . Wait ()
}()
go a . Listen ()
go b . Listen ()
// The two goroutines below have endpoint A spam endpoint B, and print out how
// many packets of data are being sent per second.
go func () {
defer wg . Done ()
for {
select {
case <- exit :
return
default :
}
check ( a . WriteReliablePacket ( PacketData , b . Addr ()))
atomic . AddUint64 ( & NumPackets , 1 )
}
}()
go func () {
defer wg . Done ()
ticker := time . NewTicker ( 1 * time . Second )
defer ticker . Stop ()
for {
select {
case <- exit :
return
case <- ticker . C :
numPackets := atomic . SwapUint64 ( & NumPackets , 0 )
numBytes := float64 ( numPackets ) * 1400.0 / 1024.0 / 1024.0
log . Printf (
"Sent %d packet(s) comprised of %.2f MiB worth of data." ,
numPackets ,
numBytes ,
)
}
}
}()
ch := make ( chan os. Signal , 1 )
signal . Notify ( ch , os . Interrupt )
<- ch
}