الموثوقية هي طبقة موثوقية لاتصالات UDP في Go.
مع وجود 9 بايت فقط من حزم البيانات على الأكثر، فإن ما يمكن الاعتماد عليه لتطبيقك المستند إلى UDP هو:
** هذا المشروع لا يزال قيد التنفيذ! قم بمسح الكود المصدري، أو كتابة بعض اختبارات الوحدة، أو المساعدة في التوثيق، أو فتح مشكلة Github إذا كنت ترغب في المساعدة أو لديك أي أسئلة!
موثوق يستخدم نفس تخطيط رأس الحزمة الموضح في networkprotocol/reliable.io
.
تبدأ جميع الحزم ببايت واحد (8 بتات) يمثل 8 أعلام مختلفة. تكون الحزم متسلسلة، ويتم ترقيمها باستخدام عدد صحيح 16 بت غير موقّع مضمن في رأس الحزمة ما لم يتم وضع علامة على الحزمة بأنها غير موثوقة.
يتم تضمين إقرارات الحزمة (ACKs) بشكل متكرر في كل حزمة مرسلة باستخدام إجمالي 5 بايت: بايتتان تمثلان رقم تسلسل حزمة 16 بت غير موقع (ack)، وثلاث بايت تمثل حقل بت 32 بت (ackBits).
تخطيط رأس الحزمة، يشبه إلى حد كبير networkprotocol/reliable.io
، يتم ترميزه بواسطة دلتا وترميزه بواسطة RLE لتقليل الحجم الزائد لكل حزمة.
بالنظر إلى الحزمة التي تلقيناها للتو من نظيرنا، لكل مجموعة بت (i) في حقل البت (ackBits)، فإننا نضع علامة على الحزمة التي أرسلناها ليتم الاعتراف بها إذا كان رقمها التسلسلي (ack - i).
في حالة قيام النظير "أ" بإرسال حزم إلى "ب"، مع عدم إرسال "ب" أي حزم على الإطلاق إلى "أ"، سيرسل "ب" حزمة فارغة لكل 32 حزمة مستلمة من "أ" بحيث يكون "أ" على علم بأن "ب" قد اعترف بحزمه.
بشكل أكثر وضوحًا، يتم الاحتفاظ بالعداد (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) من خلال مستلم الحزمة من نظيرنا.
تم أخذ منطق إعادة إرسال الحزم المرسلة التي لا معنى لها والتي لم يتم الإقرار بها والحفاظ على الإقرارات في منشور المدونة هذا بواسطة Glenn Fiedler.
يُشتبه في فقدان الحزم إذا لم يتعرف عليها المستلم بعد 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
. افتراضيًا، يتم توفير معالج nil الذي يتجاهل كافة الحزم الواردة.WithEndpointErrorHandler
أو WithProtocolErrorHandler
. افتراضيًا، يتم توفير معالج nil الذي يتجاهل كافة الأخطاء.WithBufferPool
. افتراضيًا، يتم إنشاء مثيل لتجمع المخزن المؤقت للبايت الجديد. تم إجراء اختبار باستخدام cmd/benchmark
من اليابان إلى خادم DigitalOcean 2GB / 60 GB Disk / NYC3.
كانت المهمة المرجعية هي إرسال بريد عشوائي يبلغ 1400 حزمة بايت من اليابان إلى نيويورك. نظرًا لزمن انتقال ping يبلغ حوالي 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
}