reliabel adalah lapisan keandalan untuk koneksi UDP di Go.
Dengan overhead paket paling banyak hanya 9 byte, apa yang dapat diandalkan untuk aplikasi berbasis UDP Anda adalah:
**Proyek ini masih WIP! Pelajari kode sumber, tulis beberapa pengujian unit, bantu dokumentasi, atau buka masalah Github jika Anda ingin membantu atau memiliki pertanyaan!
reliabel menggunakan tata letak header paket yang sama seperti yang dijelaskan dalam networkprotocol/reliable.io
.
Semua paket dimulai dengan satu byte (8 bit) yang mewakili 8 flag berbeda. Paket-paket bersifat berurutan, dan diberi nomor menggunakan bilangan bulat 16-bit yang tidak ditandatangani yang disertakan dalam header paket kecuali paket tersebut ditandai sebagai tidak dapat diandalkan.
Pengakuan paket (ACK) disertakan secara berlebihan dalam setiap paket yang dikirim menggunakan total 5 byte: dua byte mewakili nomor urut paket 16-bit yang tidak ditandatangani (ack), dan tiga byte mewakili bitfield 32-bit (ackBits).
Tata letak header paket, seperti networkprotocol/reliable.io
, dikodekan delta dan dikodekan RLE untuk mengurangi ukuran overhead per paket.
Mengingat sebuah paket yang baru saja kita terima dari rekan kita, untuk setiap set bit (i) di bitfield (ackBits), kita menandai paket yang telah kita kirimkan untuk diakui jika nomor urutnya adalah (ack - i).
Dalam kasus rekan A mengirim paket ke B, dengan B tidak mengirimkan paket apa pun ke A, B akan mengirimkan paket kosong untuk setiap 32 paket yang diterima dari A sehingga A akan mengetahui bahwa B telah mengakui paketnya.
Lebih eksplisitnya, sebuah counter (lui) dipertahankan yang mewakili nomor urut paket terakhir berturut-turut yang telah kita terima yang pengakuannya telah kita sampaikan kepada rekan kita.
Misalnya, jika (lui) adalah 0, dan kita telah mengirimkan balasan untuk paket-paket yang nomor urutnya adalah 2, 3, 4, dan 6, dan kita kemudian telah mengonfirmasi nomor urut paket 1, maka lui akan menjadi 4.
Setelah memperbarui (lui), jika 32 nomor urut berturut-turut berikutnya adalah nomor urut paket yang telah kami terima sebelumnya, kami akan menambah (lui) sebesar 32 dan mengirimkan satu paket kosong yang berisi pengakuan paket berikut: (ack=lui+31, ackBits=[lui,lui+31]).
Dua buffer urutan berukuran tetap dipertahankan untuk paket yang telah kami kirim (wq), dan paket yang telah kami terima (rq). Ukuran yang ditetapkan untuk buffer ini harus dibagi rata ke dalam nilai maksimal bilangan bulat 16-bit yang tidak ditandatangani (65536). Struktur data dijelaskan dalam postingan blog ini oleh Glenn Fiedler.
Kami melacak penghitung (oui), yang mewakili nomor urut terakhir berturut-turut dari paket yang kami kirim yang diakui oleh rekan kami. Misalnya, jika kita telah mengirimkan paket yang nomor urutnya berada pada rentang [0, 256], dan kita telah menerima balasan untuk paket tersebut (0, 1, 2, 3, 4, 8, 9, 10, 11, 12), maka (oui) akan menjadi 4.
Misalkan cap(q) adalah ukuran tetap atau kapasitas buffer urutan q.
Saat mengirim paket, kami sesekali menghentikan dan melakukan buffering pada pengiriman paket jika kami yakin mengirim lebih banyak paket akan melebihi buffer baca penerima kami. Lebih jelasnya, jika paket berikutnya yang kami kirim diberi nomor paket lebih besar dari (oui + cap(rq)), kami menghentikan semua pengiriman hingga (oui) bertambah melalui penerima paket dari rekan kami.
Logika untuk mentransmisikan ulang paket terkirim yang basi dan tidak diakui serta mempertahankan pengakuan diambil dari entri blog ini oleh Glenn Fiedler.
Paket diduga hilang jika tidak dikenali oleh penerimanya setelah 100 ms. Jika sebuah paket dicurigai hilang, maka paket tersebut akan dikirim ulang. Saat ini, paket dikirim ulang maksimal 10 kali.
Mungkin bijaksana untuk tidak mengizinkan paket dikirim ulang beberapa kali, dan menyerahkannya kepada pengembang. Namun, hal ini terbuka untuk diskusi yang dengan senang hati saya sampaikan di server Discord saya atau melalui masalah Github.
Dalam pencarian saya untuk menemukan solusi yang layak terhadap pemblokiran head-of-line TCP, saya mencari banyak perpustakaan UDP yang dapat diandalkan di Go, dan sebagian besar cocok untuk transfer file atau bermain game:
Melihat semuanya, aku merasa mereka berbuat terlalu banyak untukku. Untuk pekerjaan dan proyek sampingan saya, saya telah banyak bekerja pada protokol jaringan p2p yang terdesentralisasi. Sifat dari protokol-protokol ini adalah bahwa mereka sangat menderita karena pemblokiran head-of-line TCP yang beroperasi di lingkungan latensi tinggi/kehilangan paket tinggi.
Dalam banyak kasus, banyak fitur yang disediakan oleh perpustakaan ini tidak diperlukan, atau sejujurnya dirasa sebaiknya fitur tersebut ditangani dan dipikirkan dengan matang oleh pengembang yang menggunakan perpustakaan ini. Misalnya:
Jadi, saya mulai mengerjakan pendekatan modular dan memutuskan untuk mengabstraksikan bagian keandalan protokol yang telah saya buat ke dalam perpustakaan terpisah.
Saya merasa bahwa pendekatan ini adalah yang terbaik dibandingkan dengan alternatif populer seperti QUIC atau SCTP yang mungkin, tergantung pada keadaan Anda, memberikan manfaat yang terlalu banyak bagi Anda. Lagi pula, mendapatkan bit reliabilitas dari protokol berbasis UDP yang benar dan teruji dengan baik sudah cukup sulit.
net.PacketConn
untuk abstraksi yang lebih baik.net.UDPAddr
yang diteruskan.dapat diandalkan menggunakan modul Go. Untuk memasukkannya ke dalam proyek Anda, jalankan perintah berikut:
$ go get github.com/lithdew/reliable
Jika Anda hanya ingin menjalankan dan menjalankan proyek atau demo dengan cepat, gunakan Endpoint
. Jika Anda memerlukan lebih banyak fleksibilitas, pertimbangkan untuk bekerja langsung dengan Conn
.
Perhatikan bahwa semacam mekanisme tetap hidup atau sistem detak jantung perlu di-bootstrap di atas, jika tidak, paket mungkin akan dikirim ulang tanpa batas waktu karena gagal dikenali.
WithReadBufferSize
. Ukuran buffer baca default adalah 256.WithWriteBufferSize
. Ukuran buffer tulis default adalah 256.WithResendTimeout
. Batas waktu pengiriman ulang default adalah 100 milidetik.WithEndpointPacketHandler
atau WithProtocolPacketHandler
. Secara default, disediakan nil handler yang mengabaikan semua paket masuk.WithEndpointErrorHandler
atau WithProtocolErrorHandler
. Secara default, nil handler disediakan yang mengabaikan semua kesalahan.WithBufferPool
. Secara default, kumpulan buffer byte baru dibuat. Benchmark dilakukan menggunakan cmd/benchmark
dari Jepang ke server DigitalOcean 2GB/60 GB Disk/NYC3.
Tugas patokannya adalah mengirim spam ke paket 1400 byte dari Jepang ke New York. Mengingat latensi ping sekitar 220 milidetik, throughputnya kira-kira 1,2 MiB/detik.
Tolok ukur pengujian unit juga telah dilakukan, seperti yang ditunjukkan di bawah ini.
$ 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
Anda dapat menjalankan contoh di bawah ini dengan menjalankan perintah berikut:
$ go run github.com/lithdew/reliable/examples/basic
Contoh ini menunjukkan:
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
}