надежный — уровень надежности для UDP-соединений в Go.
Учитывая не более 9 байт служебных данных пакета, надежность вашего приложения на основе UDP заключается в следующем:
** Этот проект все еще находится в стадии разработки! Просмотрите исходный код, напишите несколько модульных тестов, помогите с документацией или откройте проблему на Github, если вы хотите помочь или у вас есть вопросы!
надежный использует тот же макет заголовка пакета, который описан в networkprotocol/reliable.io
.
Все пакеты начинаются с одного байта (8 бит), представляющего 8 различных флагов. Пакеты являются последовательными и нумеруются с использованием 16-битного целого числа без знака, включенного в заголовок пакета, если только пакет не помечен как ненадежный.
Подтверждения пакетов (ACK) избыточно включаются в каждый отправленный пакет, используя в общей сложности 5 байтов: два байта представляют собой 16-битный порядковый номер пакета без знака (ack), и три байта представляют 32-битное битовое поле (ackBits).
Структура заголовка пакета, очень похожая на networkprotocol/reliable.io
, имеет дельта-кодирование и RLE-кодирование для уменьшения накладных расходов на размер пакета.
Учитывая пакет, который мы только что получили от нашего партнера, для каждого установленного бита (i) в битовом поле (ackBits) мы помечаем отправленный пакет как подлежащий подтверждению, если его порядковый номер равен (ack - i).
В случае, когда узел A отправляет пакеты B, а B вообще не отправляет никаких пакетов A, B будет отправлять пустой пакет на каждые 32 пакета, полученные от A, так что A будет знать, что B подтвердил свои пакеты.
Точнее, поддерживается счетчик (lui), представляющий последний последовательный порядковый номер полученного нами пакета, о подтверждении которого мы сообщили нашему партнеру.
Например, если (lui) равен 0, и мы отправили подтверждения для пакетов с порядковыми номерами 2, 3, 4 и 6, а затем подтвердили порядковый номер пакета 1, тогда lui будет 4.
Если после обновления (lui) следующие 32 последовательных номера являются порядковыми номерами пакетов, которые мы получили ранее, мы увеличим (lui) на 32 и отправим один пустой пакет, содержащий следующие подтверждения пакета: (ack=lui+31, ackBits=[lui,lui+31]).
Два буфера последовательности фиксированного размера поддерживаются для отправленных нами пакетов (wq) и полученных пакетов (rq). Размер, фиксированный для этих буферов, должен равномерно делиться на максимальное значение 16-битного целого числа без знака (65536). Структура данных описана в этой записи блога Гленном Фидлером.
Мы отслеживаем счетчик (oui), представляющий последний последовательный порядковый номер отправленного нами пакета, который был подтвержден нашим партнером. Например, если мы отправили пакеты, чьи порядковые номера находятся в диапазоне [0, 256], и получили подтверждения для пакетов (0, 1, 2, 3, 4, 8, 9, 10, 11, 12), тогда (oui) будет 4.
Пусть cap(q) — фиксированный размер или емкость буфера последовательности q.
При отправке пакетов мы периодически останавливаем и буферизуем отправку пакетов, если считаем, что отправка большего количества пакетов приведет к переполнению буфера чтения нашего получателя. Точнее говоря, если следующему отправленному нами пакету присвоен номер пакета, превышающий (oui + cap(rq)), мы прекращаем все отправки до тех пор, пока (oui) не увеличится через получателя пакета от нашего узла.
Логика повторной передачи устаревших, неподтвержденных отправленных пакетов и сохранения подтверждений была взята из этого поста в блоге Гленном Фидлером.
Предполагается, что пакеты будут потеряны, если они не подтверждены получателем через 100 мс. Если есть подозрение, что пакет потерян, он отправляется повторно. На данный момент пакеты пересылаются максимум 10 раз.
Возможно, было бы разумно не допускать повторной отправки пакетов ограниченное количество раз и оставить это на усмотрение разработчика. Тем не менее, это открыто для обсуждения, которое я буду рад обсудить на своем сервере Discord или в выпуске Github.
В поисках реального решения против блокировки TCP-заголовка я просмотрел множество надежных UDP-библиотек в Go, большинство из которых в первую очередь подходят либо для передачи файлов, либо для игр:
Проходя их все, я чувствовал, что они сделали для меня слишком много. В своей работе и побочных проектах я много работал над децентрализованными сетевыми протоколами p2p. Природа этих протоколов такова, что они сильно страдают от блокировки TCP-заголовка, работающей в средах с высокой задержкой и высокой потерей пакетов.
Во многих случаях многие функции, предоставляемые этими библиотеками, либо были не нужны, либо, честно говоря, казалось, что их лучше всего использовать и продумывать разработчик, использующий эти библиотеки. Например:
Итак, я начал работать над модульным подходом и решил выделить часть надежности протоколов, которые я построил, в отдельную библиотеку.
Я считаю, что этот подход лучше по сравнению с популярными альтернативами, такими как QUIC или SCTP, которые, в зависимости от ваших обстоятельств, могут сделать для вас слишком много. В конце концов, добиться правильного и хорошо протестированного протокола на основе UDP достаточно сложно.
net.PacketConn
для более тонкой абстракции.net.UDPAddr
.надежный использует модули Go. Чтобы включить его в свой проект, выполните следующую команду:
$ go get github.com/lithdew/reliable
Если вы просто хотите быстро запустить проект или демонстрационную версию, используйте Endpoint
. Если вам требуется больше гибкости, рассмотрите возможность прямой работы с Conn
.
Обратите внимание, что сверху должен быть запущен какой-то механизм поддержания активности или система тактового сигнала, в противном случае пакеты могут отправляться повторно в течение неопределенного времени, поскольку они не будут подтверждены.
WithReadBufferSize
. Размер буфера чтения по умолчанию составляет 256.WithWriteBufferSize
. Размер буфера записи по умолчанию равен 256.WithResendTimeout
. Тайм-аут повторной отправки по умолчанию составляет 100 миллисекунд.WithEndpointPacketHandler
или WithProtocolPacketHandler
. По умолчанию предоставляется нулевой обработчик, который игнорирует все входящие пакеты.WithEndpointErrorHandler
или WithProtocolErrorHandler
. По умолчанию предоставляется обработчик nil, который игнорирует все ошибки.WithBufferPool
. По умолчанию создается новый пул байтовых буферов. Тестирование было выполнено с использованием cmd/benchmark
из Японии на сервере DigitalOcean 2 ГБ/60 ГБ/NYC3.
Тестовой задачей была рассылка спам-пакетов размером 1400 байт из Японии в Нью-Йорк. Учитывая задержку пинга примерно 220 миллисекунд, пропускная способность составила примерно 1,2 МБ/сек.
Также были проведены тесты модульного тестирования, как показано ниже.
$ 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
Вы можете запустить приведенный ниже пример, выполнив следующую команду:
$ go run github.com/lithdew/reliable/examples/basic
Этот пример демонстрирует:
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
}