เชื่อถือได้ คือเลเยอร์ความน่าเชื่อถือสำหรับการเชื่อมต่อ UDP ใน Go
ด้วยแพ็กเก็ตโอเวอร์เฮดสูงสุดเพียง 9 ไบต์ สิ่ง ที่เชื่อถือได้ สำหรับแอปพลิเคชันที่ใช้ UDP ของคุณคือ:
** โปรเจ็กต์นี้ยังอยู่ใน WIP! ศึกษาซอร์สโค้ด เขียนการทดสอบหน่วย ช่วยเหลือด้านเอกสาร หรือเปิดปัญหา 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) โครงสร้างข้อมูลอธิบายไว้ในบล็อกโพสต์นี้โดย Glenn Fiedler
เราติดตามตัวนับ (oui) ซึ่งแสดงถึงหมายเลขลำดับสุดท้ายของแพ็กเก็ตที่เราส่งไปซึ่งได้รับการยอมรับจากเพื่อนของเรา ตัวอย่างเช่น ถ้าเราส่งแพ็กเก็ตที่มีหมายเลขลำดับอยู่ในช่วง [0, 256] และเราได้รับการตอบรับสำหรับแพ็กเก็ต (0, 1, 2, 3, 4, 8, 9, 10, 11, 12) แล้ว (อุ้ย) จะเป็น 4
ให้ cap(q) เป็นขนาดคงที่หรือความจุของบัฟเฟอร์ลำดับ q
ในขณะที่ส่งแพ็กเก็ต เราจะหยุดและบัฟเฟอร์การส่งแพ็กเก็ตเป็นระยะ หากเราเชื่อว่าการส่งแพ็กเก็ตมากขึ้นจะทำให้บัฟเฟอร์การอ่านของผู้รับล้นเกิน ให้ชัดเจนยิ่งขึ้น หากแพ็กเก็ตถัดไปที่เราส่งถูกกำหนดหมายเลขแพ็กเก็ตที่มากกว่า (oui + cap(rq)) เราจะหยุดการส่งทั้งหมดจนกว่า (oui) จะเพิ่มขึ้นผ่านผู้รับแพ็กเก็ตจากเพียร์ของเรา
ตรรกะในการส่งแพ็กเก็ตที่เก่าและไม่ได้รับการตอบรับซ้ำอีกครั้ง และการรักษาการตอบรับนั้นได้รับเครดิตในบล็อกโพสต์นี้โดย Glenn Fiedler
สงสัยว่าแพ็กเก็ตจะหายไปหากผู้รับไม่ตอบรับหลังจาก 100 มิลลิวินาที เมื่อสงสัยว่าแพ็กเก็ตสูญหาย ก็จะถูกส่งอีกครั้ง ณ ขณะนี้ แพ็กเก็ตจะถูกส่งซ้ำสูงสุด 10 ครั้ง
อาจเป็นการดีที่จะไม่อนุญาตให้ส่งแพ็กเก็ตซ้ำตามจำนวนครั้งและปล่อยให้เป็นหน้าที่ของนักพัฒนา อย่างไรก็ตาม นั่นเปิดให้มีการสนทนาได้ ซึ่งฉันยินดีที่จะพูดคุยผ่านเซิร์ฟเวอร์ Discord ของฉันหรือผ่านปัญหา Github
ในการค้นหาโซลูชันที่เป็นไปได้สำหรับการบล็อก TCP head-of-line ฉันได้ดูไลบรารี UDP ที่เชื่อถือได้ จำนวนมาก ใน Go โดยส่วนใหญ่เหมาะสำหรับการถ่ายโอนไฟล์หรือเล่นเกมเป็นหลัก:
เมื่อผ่านมาทั้งหมดแล้ว ฉันรู้สึกว่าพวกเขาทำอะไรให้ฉันมากเกินไปนิดหน่อย สำหรับงานและโครงการรองของฉัน ฉันทำงานอย่างหนักกับโปรโตคอลเครือข่าย p2p แบบกระจายอำนาจ ลักษณะของโปรโตคอลเหล่านี้ก็คือ โปรโตคอลเหล่านี้ประสบปัญหาอย่างมากจากการบล็อก TCP head-of-line ซึ่งทำงานในสภาพแวดล้อมที่มีความหน่วงสูง/การสูญเสียแพ็กเก็ตสูง
ในหลายกรณี คุณลักษณะมากมายที่ไลบรารีเหล่านี้มอบให้นั้นไม่จำเป็น หรือรู้สึกโดยสัตย์จริงว่าควรได้รับการจัดการและไตร่ตรองโดยนักพัฒนาที่ใช้ไลบรารีเหล่านี้อย่างดีที่สุด ตัวอย่างเช่น:
ดังนั้นฉันจึงเริ่มทำงานในรูปแบบโมดูลาร์และตัดสินใจที่จะสรุปส่วนความน่าเชื่อถือของโปรโตคอลที่ฉันสร้างไว้ในไลบรารีแยกต่างหาก
ฉันรู้สึกว่าแนวทางนี้ดีที่สุดเมื่อเทียบกับทางเลือกอื่นยอดนิยมอย่าง QUIC หรือ SCTP ซึ่งอาจช่วยอะไรคุณมากเกินไปก็ได้ ขึ้นอยู่กับสถานการณ์ของคุณ ท้ายที่สุดแล้ว การได้รับ เพียง บิตความน่าเชื่อถือของโปรโตคอลที่ใช้ UDP ที่ถูกต้องและผ่านการทดสอบอย่างดีนั้นก็ยากเพียงพอแล้ว
net.PacketConn
เพื่อให้ได้นามธรรมที่ละเอียดยิ่งขึ้นnet.UDPAddr
ใช้โมดูล Go ที่เชื่อถือได้ หากต้องการรวมไว้ในโปรเจ็กต์ของคุณ ให้รันคำสั่งต่อไปนี้:
$ go get github.com/lithdew/reliable
หากคุณต้องการเริ่มต้นโครงการหรือการสาธิตอย่างรวดเร็ว ให้ใช้ Endpoint
หากคุณต้องการความยืดหยุ่นมากขึ้น ลองพิจารณาร่วมงานกับ Conn
โดยตรง
โปรดทราบว่ากลไก Keep-alive หรือระบบการเต้นของหัวใจบางประเภทจำเป็นต้องบูตเครื่องไว้ด้านบน ไม่เช่นนั้นแพ็กเก็ตอาจถูกส่งซ้ำอย่างไม่มีกำหนดเนื่องจากจะไม่ได้รับการยอมรับ
WithReadBufferSize
ขนาดบัฟเฟอร์การอ่านเริ่มต้นคือ 256WithWriteBufferSize
ขนาดบัฟเฟอร์การเขียนเริ่มต้นคือ 256WithResendTimeout
การหมดเวลาการส่งซ้ำเริ่มต้นคือ 100 มิลลิวินาทีWithEndpointPacketHandler
หรือ WithProtocolPacketHandler
ตามค่าเริ่มต้น ตัวจัดการ nil จะถูกจัดเตรียมไว้ซึ่งจะละเว้นแพ็กเก็ตขาเข้าทั้งหมดWithEndpointErrorHandler
หรือ WithProtocolErrorHandler
ตามค่าเริ่มต้น จะมีการระบุตัวจัดการ nil ซึ่งจะละเว้นข้อผิดพลาดทั้งหมดWithBufferPool
โดยค่าเริ่มต้น พูลบัฟเฟอร์ไบต์ใหม่จะถูกสร้างอินสแตนซ์ การวัดประสิทธิภาพเสร็จสิ้นโดยใช้ cmd/benchmark
จากญี่ปุ่นไปยังเซิร์ฟเวอร์ DigitalOcean 2GB / 60 GB Disk / NYC3
งานวัดประสิทธิภาพคือการสแปมแพ็กเก็ตขนาด 1,400 ไบต์จากญี่ปุ่นไปยังนิวยอร์ก เมื่อพิจารณาถึงเวลาแฝงในการ Ping ที่ประมาณ 220 มิลลิวินาที ปริมาณงานจะอยู่ที่ประมาณ 1.2 MiB/วินาที
ยังได้ดำเนินการวัดประสิทธิภาพการทดสอบหน่วย ดังที่แสดงด้านล่าง
$ 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
}