Reliable は、 Go の UDP 接続の信頼性レイヤーです。
パケット オーバーヘッドが最大でも 9 バイトしかないため、UDP ベースのアプリケーションに対してReliable が行うことは次のとおりです。
** このプロジェクトはまだ作業中です。ソース コードを調べたり、単体テストを書いたり、ドキュメントを手伝ったり、手助けしたい場合や質問がある場合は Github のイシューを開いてください。
Reliable は、 networkprotocol/reliable.io
で説明されているのと同じパケット ヘッダー レイアウトを使用します。
すべてのパケットは、8 つの異なるフラグを表す 1 バイト (8 ビット) で始まります。パケットは連続しており、パケットが信頼できないとマークされていない限り、パケット ヘッダーに含まれる符号なし 16 ビット整数を使用して番号が付けられます。
パケット確認応答 (ACK) は、合計 5 バイトを使用してすべての送信パケットに冗長的に含まれます。2 バイトは符号なし 16 ビットのパケット シーケンス番号 (ack) を表し、3 バイトは 32 ビットのビットフィールド (ackBits) を表します。
networkprotocol/reliable.io
と同様に、パケット ヘッダー レイアウトはデルタ エンコードおよび RLE エンコードされ、パケットあたりのサイズ オーバーヘッドが削減されます。
ピアから受信したばかりのパケットを考えると、ビットフィールド (ackBits) の設定ビット (i) ごとに、シーケンス番号が (ack - i) である場合、送信したパケットに確認応答のマークを付けます。
ピア A が B にパケットを送信し、B が A にパケットをまったく送信していない場合、B は A から受信した 32 パケットごとに空のパケットを送信するため、B が自分のパケットを確認したことを A が認識できるようになります。
より明示的には、受信した最後の連続パケット シーケンス番号を表すカウンター (lui) が維持され、その確認応答がピアに伝えられます。
たとえば、(lui) が 0 で、シーケンス番号が 2、3、4、および 6 であるパケットの確認応答を送信し、その後パケット シーケンス番号 1 を確認した場合、lui は 4 になります。
(lui) の更新時に、次の 32 の連続するシーケンス番号が以前に受信したパケットのシーケンス番号である場合、(lui) を 32 ずつインクリメントし、次のパケット確認応答を含む単一の空のパケットを送信します: (ack=lui+31, ackBits=[lui,lui+31])。
送信したパケット (wq) と受信したパケット (rq) 用に、2 つの固定サイズのシーケンス バッファーが維持されます。これらのバッファーに固定されたサイズは、符号なし 16 ビット整数の最大値 (65536) に均等に分割する必要があります。データ構造については、Glenn Fiedler によるこのブログ投稿で説明されています。
私たちは、送信してピアによって確認されたパケットの最後の連続シーケンス番号を表すカウンター (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 行頭ブロックに対する実行可能な解決策を見つける探求の中で、Go で信頼できる UDP ライブラリを多数検討しました。そのほとんどは主にファイル転送またはゲームのいずれかに適しています。
それらすべてを経験してみると、彼らは私にとって少しやりすぎていると感じました。私の仕事とサイドプロジェクトでは、分散型 P2P ネットワーキングプロトコルに重点的に取り組んでいます。これらのプロトコルの性質上、高遅延/高パケット損失環境で動作する TCP 行頭ブロックの影響を大きく受けます。
多くの場合、これらのライブラリによって提供される機能の多くは必要ないか、正直なところ、これらのライブラリを使用する開発者が処理して熟考するのが最善であると感じていました。例えば:
そこで、私はモジュール式のアプローチに取り組み始め、別のライブラリに構築したプロトコルの信頼性の部分を抽象化することにしました。
このアプローチは、状況によっては少しやりすぎてしまう可能性がある QUIC や SCTP などの一般的な代替手段よりも最適だと思います。結局のところ、UDP ベースのプロトコルの信頼性の部分だけを正確に取得し、十分にテストするだけでも十分に困難です。
net.PacketConn
関連のビットをカプセル化します。net.UDPAddr
の文字列表現のキャッシュを保持します。Reliable はGo モジュールを使用します。これをプロジェクトに含めるには、次のコマンドを実行します。
$ go get github.com/lithdew/reliable
プロジェクトまたはデモをすぐに起動して実行したいだけの場合は、 Endpoint
を使用してください。さらに柔軟性が必要な場合は、 Conn
を直接操作することを検討してください。
何らかのキープアライブ メカニズムまたはハートビート システムを最上位でブートストラップする必要があることに注意してください。そうしないと、パケットは確認応答に失敗するため、無期限に再送信される可能性があります。
WithReadBufferSize
使用して構成できます。デフォルトの読み取りバッファ サイズは 256 です。WithWriteBufferSize
使用して構成できます。デフォルトの書き込みバッファ サイズは 256 です。WithResendTimeout
使用して構成できます。デフォルトの再送信タイムアウトは 100 ミリ秒です。WithEndpointPacketHandler
またはWithProtocolPacketHandler
を使用して構成できます。デフォルトでは、すべての受信パケットを無視する nil ハンドラーが提供されます。WithEndpointErrorHandler
またはWithProtocolErrorHandler
使用して構成できる接続でエラーが発生したときに呼び出されるエラー ハンドラー。デフォルトでは、すべてのエラーを無視する nil ハンドラーが提供されます。WithBufferPool
使用して渡すことができます。デフォルトでは、新しいバイト バッファ プールがインスタンス化されます。 ベンチマークは、日本から DigitalOcean 2GB / 60 GB ディスク / NYC3 サーバーに対してcmd/benchmark
を使用して実行されました。
ベンチマーク タスクは、1400 バイトのパケットを日本からニューヨークにスパム送信することでした。 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
}