confiable es una capa de confiabilidad para conexiones UDP en Go.
Con sólo 9 bytes de sobrecarga de paquetes como máximo, lo que confiable hace para su aplicación basada en UDP es:
** ¡Este proyecto sigue siendo un WIP! Revise el código fuente, escriba algunas pruebas unitarias, ayude con la documentación o abra una edición de Github si desea ayudar o tiene alguna pregunta.
confiable utiliza el mismo diseño de encabezado de paquete descrito en networkprotocol/reliable.io
.
Todos los paquetes comienzan con un solo byte (8 bits) que representa 8 banderas diferentes. Los paquetes son secuenciales y se numeran utilizando un entero de 16 bits sin signo incluido en el encabezado del paquete, a menos que el paquete esté marcado como no confiable.
Los acuses de recibo de paquetes (ACK) se incluyen de forma redundante en cada paquete enviado utilizando un total de 5 bytes: dos bytes que representan un número de secuencia de paquete de 16 bits sin firmar (ack) y tres bytes que representan un campo de bits de 32 bits (ackBits).
El diseño del encabezado del paquete, muy parecido a networkprotocol/reliable.io
, está codificado en delta y RLE para reducir la sobrecarga de tamaño por paquete.
Dado un paquete que acabamos de recibir de nuestro par, para cada bit establecido (i) en el campo de bits (ackBits), marcamos un paquete que hemos enviado para ser reconocido si su número de secuencia es (ack - i).
En el caso de que el par A envíe paquetes a B, y B no envíe ningún paquete a A, B enviará un paquete vacío por cada 32 paquetes recibidos de A para que A sepa que B ha acusado recibo de sus paquetes.
Más explícitamente, se mantiene un contador (lui) que representa el último número de secuencia de paquete consecutivo que hemos recibido cuyo reconocimiento le hemos comunicado a nuestro par.
Por ejemplo, si (lui) es 0 y hemos enviado acuses de recibo para paquetes cuyos números de secuencia son 2, 3, 4 y 6, y luego hemos reconocido el paquete número de secuencia 1, entonces lui sería 4.
Al actualizar (lui), si los siguientes 32 números de secuencia consecutivos son números de secuencia de paquetes que hemos recibido anteriormente, incrementaremos (lui) en 32 y enviaremos un único paquete vacío que contiene los siguientes acuses de recibo de paquetes: (ack=lui+31, ackBits=[lui,lui+31]).
Se mantienen dos buffers de secuencia de tamaño fijo para los paquetes que hemos enviado (wq) y los paquetes que hemos recibido (rq). El tamaño fijado para estos buffers debe dividirse uniformemente entre el valor máximo de un entero de 16 bits sin signo (65536). La estructura de datos se describe en esta publicación de blog de Glenn Fiedler.
Realizamos un seguimiento de un contador (oui), que representa el último número de secuencia consecutivo de un paquete que hemos enviado y que fue reconocido por nuestro par. Por ejemplo, si hemos enviado paquetes cuyos números de secuencia están en el rango [0, 256] y hemos recibido acuses de recibo de paquetes (0, 1, 2, 3, 4, 8, 9, 10, 11, 12), entonces (oui) sería 4.
Sea cap(q) el tamaño fijo o la capacidad del búfer de secuencia q.
Mientras enviamos paquetes, detenemos y almacenamos en búfer de forma intermitente el envío de paquetes si creemos que enviar más paquetes desbordaría el búfer de lectura de nuestro destinatario. Más explícitamente, si al siguiente paquete que enviamos se le asigna un número de paquete mayor que (oui + cap(rq)), detenemos todos los envíos hasta que (oui) haya aumentado a través del destinatario de un paquete de nuestro par.
La lógica para retransmitir paquetes enviados obsoletos y no reconocidos y para mantener los acuses de recibo se atribuye a esta publicación de blog de Glenn Fiedler.
Se sospecha que los paquetes se pierden si su destinatario no los reconoce después de 100 ms. Una vez que se sospecha que un paquete se ha perdido, se reenvía. A partir de ahora, los paquetes se reenvían un máximo de 10 veces.
Podría ser prudente no permitir que los paquetes se reenvíen un número limitado de veces y dejarlo en manos del desarrollador. Sin embargo, eso está abierto a una discusión que me complace tener en mi servidor de Discord o mediante un problema de Github.
En mi búsqueda por encontrar una solución viable contra el bloqueo de cabecera de línea de TCP, revisé muchas bibliotecas UDP confiables en Go, la mayoría de las cuales son adecuadas principalmente para transferencia de archivos o juegos:
Al revisarlos todos, sentí que hicieron demasiado por mí. Para mi trabajo y proyectos paralelos, he estado trabajando intensamente en protocolos de redes p2p descentralizados. La naturaleza de estos protocolos es que sufren en gran medida el bloqueo de cabecera de línea de TCP que opera en entornos de alta latencia/alta pérdida de paquetes.
En muchos casos, muchas de las funciones proporcionadas por estas bibliotecas no eran necesarias o, sinceramente, se sentía que el desarrollador que las utilizaba sería mejor que las manejara y pensara en ellas. Por ejemplo:
Entonces, comencé a trabajar en un enfoque modular y decidí abstraer la parte de confiabilidad de los protocolos que había creado en una biblioteca separada.
Siento que este enfoque es mejor frente a las alternativas populares como QUIC o SCTP que, dependiendo de sus circunstancias, pueden hacer demasiado por usted. Después de todo, lograr que sólo los bits de confiabilidad de un protocolo basado en UDP sean correctos y bien probados ya es bastante difícil.
net.PacketConn
para obtener una abstracción más fina.net.UDPAddr
pasado.usos confiables de los módulos Go. Para incluirlo en su proyecto, ejecute el siguiente comando:
$ go get github.com/lithdew/reliable
Si solo desea poner en marcha rápidamente un proyecto o una demostración, utilice Endpoint
. Si necesita más flexibilidad, considere trabajar directamente con Conn
.
Tenga en cuenta que es necesario iniciar algún tipo de mecanismo de mantenimiento de actividad o sistema de latidos en la parte superior; de lo contrario, los paquetes pueden reenviarse indefinidamente ya que no habrán sido reconocidos.
WithReadBufferSize
. El tamaño del búfer de lectura predeterminado es 256.WithWriteBufferSize
. El tamaño del búfer de escritura predeterminado es 256.WithResendTimeout
. El tiempo de espera de reenvío predeterminado es de 100 milisegundos.WithEndpointPacketHandler
o WithProtocolPacketHandler
. De forma predeterminada, se proporciona un controlador nulo que ignora todos los paquetes entrantes.WithEndpointErrorHandler
o WithProtocolErrorHandler
. De forma predeterminada, se proporciona un controlador nulo que ignora todos los errores.WithBufferPool
. De forma predeterminada, se crea una instancia de un nuevo grupo de búfer de bytes. Se realizó una prueba comparativa utilizando cmd/benchmark
de Japón en un servidor DigitalOcean de 2 GB/60 GB de disco/NYC3.
La tarea de referencia era enviar spam a paquetes de 1400 bytes desde Japón a Nueva York. Dada una latencia de ping de aproximadamente 220 milisegundos, el rendimiento fue de aproximadamente 1,2 MiB/seg.
También se han realizado pruebas comparativas unitarias, como se muestra a continuación.
$ 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
Puede ejecutar el siguiente ejemplo ejecutando el siguiente comando:
$ go run github.com/lithdew/reliable/examples/basic
Este ejemplo demuestra:
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
}