mirror of
https://github.com/xtaci/kcp-go.git
synced 2026-10-20 19:37:33 +00:00
510 lines
14 KiB
Go
510 lines
14 KiB
Go
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2015 xtaci
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package kcp
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFECEncodeConsecutive(t *testing.T) {
|
|
const dataSize = 10
|
|
const paritySize = 3
|
|
const payLoad = 1500
|
|
|
|
encoder := newFECEncoder(dataSize, paritySize, 0)
|
|
t.Logf("dataSize:%v, paritySize:%v", dataSize, paritySize)
|
|
group := 0
|
|
sent := 0
|
|
for i := range 100 {
|
|
if i%dataSize == 0 {
|
|
group++
|
|
}
|
|
|
|
data := make([]byte, payLoad)
|
|
duration := time.Duration(rand.Int()%300) * time.Millisecond
|
|
t.Logf("Sleep: %v, packet %v", duration, sent)
|
|
<-time.After(duration)
|
|
|
|
ps := encoder.encode(data, 200)
|
|
sent++
|
|
|
|
if len(ps) > 0 {
|
|
t.Log("has parity:", len(ps))
|
|
for idx, p := range ps {
|
|
seqid := binary.LittleEndian.Uint32(p)
|
|
expected := uint32((group-1)*(dataSize+paritySize) + dataSize + idx)
|
|
if seqid != expected {
|
|
t.Fatalf("expected parity shard:%v actual seqid %v", expected, seqid)
|
|
return
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
if sent%dataSize == 0 {
|
|
t.Log("no parity:", len(ps))
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFECDecodeLoss(t *testing.T) {
|
|
// This function lose 3 random packet from 10 datashards and 3 parity shards.
|
|
// so each group of 13 packets should be able to recover from the loss.
|
|
const dataShards = 10
|
|
const parityShards = 3
|
|
const groupSize = dataShards + parityShards
|
|
const payLoad = 1400
|
|
decoder := newFECDecoder(dataShards, parityShards)
|
|
t.Logf("dataSize:%v, paritySize:%v", dataShards, parityShards)
|
|
sent := 0
|
|
totalRecovered := 0
|
|
totalParityLost := 0
|
|
|
|
for group := range 100 {
|
|
losses := make(map[int]bool)
|
|
|
|
lost := 0
|
|
parityLost := 0
|
|
for lost < parityShards {
|
|
pos := rand.Intn(dataShards + parityShards)
|
|
if !losses[pos] {
|
|
losses[pos] = true
|
|
if pos >= dataShards {
|
|
totalParityLost++
|
|
parityLost++
|
|
}
|
|
lost++
|
|
}
|
|
}
|
|
|
|
if len(losses) != parityShards {
|
|
t.Fatalf("Expected %v losses, got %v", parityShards, len(losses))
|
|
return
|
|
}
|
|
|
|
recovered := 0
|
|
for i := range dataShards + parityShards {
|
|
sent++
|
|
if losses[i] {
|
|
t.Logf("Lost packet %v in group %v", groupSize*group+i, group)
|
|
continue
|
|
}
|
|
|
|
pkt := make([]byte, payLoad)
|
|
binary.LittleEndian.PutUint32(pkt, uint32(groupSize*group+i))
|
|
if i%(dataShards+parityShards) >= dataShards {
|
|
binary.LittleEndian.PutUint16(pkt[4:], typeParity)
|
|
} else {
|
|
binary.LittleEndian.PutUint16(pkt[4:], typeData)
|
|
}
|
|
|
|
rec := decoder.decode(pkt)
|
|
if len(rec) > 0 {
|
|
totalRecovered += len(rec)
|
|
recovered += len(rec)
|
|
t.Log("Recovered", len(rec), "packets from group", group)
|
|
}
|
|
}
|
|
|
|
// the recovered packets should equal to the lost data packets
|
|
if recovered != lost-parityLost {
|
|
t.Fatalf("Expected recovered %v packets, got %v", lost-parityLost, recovered)
|
|
}
|
|
}
|
|
t.Log("Total recovered packets:", totalRecovered)
|
|
t.Log("Total parity lost:", totalParityLost)
|
|
}
|
|
|
|
func TestFECDecodeVariablePacketSizes(t *testing.T) {
|
|
const (
|
|
dataShards = 8
|
|
parityShards = 3
|
|
groups = 32
|
|
rto = math.MaxInt32
|
|
minPayload = 8
|
|
maxPayload = 900
|
|
)
|
|
|
|
encoder := newFECEncoder(dataShards, parityShards, 0)
|
|
decoder := newFECDecoder(dataShards, parityShards)
|
|
if encoder == nil || decoder == nil {
|
|
t.Fatalf("failed to create FEC encoder/decoder")
|
|
}
|
|
|
|
rnd := rand.New(rand.NewSource(42))
|
|
var totalLost, totalRecovered int
|
|
|
|
feed := func(raw []byte) {
|
|
t.Helper()
|
|
packet := append([]byte(nil), raw...)
|
|
recovered := decoder.decode(fecPacket(packet))
|
|
for _, r := range recovered {
|
|
if len(r) < 2 {
|
|
t.Fatalf("recovered shard too small: %d", len(r))
|
|
}
|
|
sz := binary.LittleEndian.Uint16(r)
|
|
if int(sz) > len(r) {
|
|
t.Fatalf("invalid size %d for buffer len %d", sz, len(r))
|
|
}
|
|
payload := r[2:sz]
|
|
if len(payload) < minPayload {
|
|
t.Fatalf("payload shorter than expected: %d", len(payload))
|
|
}
|
|
if int(sz) != len(payload)+2 {
|
|
t.Fatalf("size field mismatch: got %d expect %d", sz, len(payload)+2)
|
|
}
|
|
|
|
groupID := binary.LittleEndian.Uint32(payload)
|
|
shardIdx := binary.LittleEndian.Uint32(payload[4:])
|
|
for i := 8; i < len(payload); i++ {
|
|
expected := byte((int(groupID) + int(shardIdx) + i) & 0xff)
|
|
if payload[i] != expected {
|
|
t.Fatalf("content mismatch: group %d shard %d offset %d got %d expect %d",
|
|
groupID, shardIdx, i-8, payload[i], expected)
|
|
}
|
|
}
|
|
defaultBufferPool.Put(r)
|
|
totalRecovered++
|
|
}
|
|
}
|
|
|
|
for group := 0; group < groups; group++ {
|
|
losses := map[int]struct{}{
|
|
group % dataShards: {},
|
|
(group + 3) % dataShards: {},
|
|
}
|
|
|
|
for shard := 0; shard < dataShards; shard++ {
|
|
payloadLen := minPayload + rnd.Intn(maxPayload-minPayload+1)
|
|
buf := make([]byte, fecHeaderSizePlus2+payloadLen)
|
|
payload := buf[fecHeaderSizePlus2:]
|
|
binary.LittleEndian.PutUint32(payload, uint32(group))
|
|
binary.LittleEndian.PutUint32(payload[4:], uint32(shard))
|
|
for i := 8; i < len(payload); i++ {
|
|
payload[i] = byte((group + shard + i) & 0xff)
|
|
}
|
|
|
|
ps := encoder.encode(buf, uint32(rto))
|
|
t.Log("Encoded group", group, "shard", shard, "payloadLen", payloadLen, "parityShards", len(ps))
|
|
if _, drop := losses[shard]; drop {
|
|
totalLost++
|
|
} else {
|
|
feed(buf)
|
|
}
|
|
|
|
if len(ps) > 0 {
|
|
for _, p := range ps {
|
|
parity := append([]byte(nil), p...)
|
|
feed(parity)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if totalRecovered != totalLost {
|
|
t.Fatalf("expected %d recoveries, got %d", totalLost, totalRecovered)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFECDecode(b *testing.B) {
|
|
const dataSize = 10
|
|
const paritySize = 3
|
|
const payLoad = 1500
|
|
decoder := newFECDecoder(dataSize, paritySize)
|
|
b.ReportAllocs()
|
|
b.SetBytes(payLoad)
|
|
for i := 0; b.Loop(); i++ {
|
|
if rand.Int()%(dataSize+paritySize) == 0 { // random loss
|
|
continue
|
|
}
|
|
pkt := make([]byte, payLoad)
|
|
binary.LittleEndian.PutUint32(pkt, uint32(i))
|
|
if i%(dataSize+paritySize) >= dataSize {
|
|
binary.LittleEndian.PutUint16(pkt[4:], typeParity)
|
|
} else {
|
|
binary.LittleEndian.PutUint16(pkt[4:], typeData)
|
|
}
|
|
decoder.decode(pkt)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFECEncode(b *testing.B) {
|
|
const dataSize = 10
|
|
const paritySize = 3
|
|
const payLoad = 1500
|
|
|
|
b.ReportAllocs()
|
|
b.SetBytes(payLoad)
|
|
encoder := newFECEncoder(dataSize, paritySize, 0)
|
|
for b.Loop() {
|
|
data := make([]byte, payLoad)
|
|
encoder.encode(data, 200)
|
|
}
|
|
}
|
|
|
|
func TestFECPAWS(t *testing.T) {
|
|
const dataShards = 10
|
|
const parityShards = 3
|
|
const shardSize = dataShards + parityShards
|
|
const payLoad = 1500
|
|
|
|
encoder := newFECEncoder(dataShards, parityShards, 0)
|
|
decoder := newFECDecoder(dataShards, parityShards)
|
|
|
|
// Manually set the next sequence number to be near the PAWS boundary
|
|
// We want to test the transition from the last group to the first group
|
|
// paws is a multiple of shardSize.
|
|
// We set next to paws - shardSize, so we are at the start of the last group.
|
|
encoder.next = encoder.paws - uint32(shardSize)
|
|
|
|
t.Logf("PAWS: %v, Initial Next: %v", encoder.paws, encoder.next)
|
|
|
|
var packets []fecPacket
|
|
|
|
// 1. Encode the last group before PAWS
|
|
// This will generate 'dataShards' data packets and 'parityShards' parity packets.
|
|
// Total 'shardSize' packets.
|
|
// Their seqids should be [paws-shardSize, ..., paws-1]
|
|
for i := range dataShards {
|
|
data := make([]byte, payLoad)
|
|
// We can put some recognizable data
|
|
// Note: fecEncoder writes header at 0-6, and size at 6-8. Payload starts at 8.
|
|
binary.LittleEndian.PutUint32(data[8:], uint32(i))
|
|
|
|
ps := encoder.encode(data, 200)
|
|
|
|
// Copy data packet
|
|
pkt := make([]byte, len(data))
|
|
copy(pkt, data)
|
|
packets = append(packets, fecPacket(pkt))
|
|
|
|
// Copy parity packets
|
|
for _, p := range ps {
|
|
pCopy := make([]byte, len(p))
|
|
copy(pCopy, p)
|
|
packets = append(packets, fecPacket(pCopy))
|
|
}
|
|
}
|
|
|
|
// Verify seqids of the first group
|
|
for i, pkt := range packets {
|
|
seqid := pkt.seqid()
|
|
expected := encoder.paws - uint32(shardSize) + uint32(i)
|
|
if seqid != expected {
|
|
t.Fatalf("Group 1: expected seqid %v, got %v", expected, seqid)
|
|
}
|
|
}
|
|
t.Log("Group 1 generated successfully")
|
|
|
|
// 2. Encode the first group after PAWS
|
|
// Their seqids should be [0, ..., shardSize-1]
|
|
startIdx := len(packets)
|
|
for i := range dataShards {
|
|
data := make([]byte, payLoad)
|
|
binary.LittleEndian.PutUint32(data[8:], uint32(i+100)) // Different data
|
|
|
|
ps := encoder.encode(data, 200)
|
|
|
|
pkt := make([]byte, len(data))
|
|
copy(pkt, data)
|
|
packets = append(packets, fecPacket(pkt))
|
|
|
|
for _, p := range ps {
|
|
pCopy := make([]byte, len(p))
|
|
copy(pCopy, p)
|
|
packets = append(packets, fecPacket(pCopy))
|
|
}
|
|
}
|
|
|
|
// Verify seqids of the second group
|
|
for i, pkt := range packets[startIdx:] {
|
|
seqid := pkt.seqid()
|
|
expected := uint32(i)
|
|
if seqid != expected {
|
|
t.Fatalf("Group 2: expected seqid %v, got %v", expected, seqid)
|
|
}
|
|
}
|
|
t.Log("Group 2 generated successfully")
|
|
|
|
// 3. Feed to decoder with some loss
|
|
// We will lose the last data packet of Group 1 and the first data packet of Group 2
|
|
// to test recovery across the boundary (though recovery is per-group).
|
|
|
|
// We drop index 9 (seqid paws-4) and index 13 (seqid 0).
|
|
// Group 1 indices: 0-12. Data: 0-9. Parity: 10-12.
|
|
// Group 2 indices: 13-25. Data: 13-22. Parity: 23-25.
|
|
|
|
dropped := make(map[int]bool)
|
|
dropped[9] = true
|
|
dropped[13] = true
|
|
|
|
recoveredCount := 0
|
|
|
|
for i, pkt := range packets {
|
|
if dropped[i] {
|
|
t.Logf("Dropping packet index %v, seqid %v", i, pkt.seqid())
|
|
continue
|
|
}
|
|
|
|
recovered := decoder.decode(pkt)
|
|
if len(recovered) > 0 {
|
|
t.Logf("Recovered %v packets at step %v (seqid %v)", len(recovered), i, pkt.seqid())
|
|
recoveredCount += len(recovered)
|
|
for _, r := range recovered {
|
|
// Verify recovered data
|
|
// r[0:2] is size. r[2:] is payload.
|
|
val := binary.LittleEndian.Uint32(r[2:])
|
|
if val == 9 {
|
|
t.Log("Recovered packet 9 correctly")
|
|
} else if val == 100 {
|
|
t.Log("Recovered packet 13 (val 100) correctly")
|
|
} else {
|
|
t.Errorf("Recovered unexpected data: %v", val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if recoveredCount != 2 {
|
|
t.Fatalf("Expected 2 recovered packets, got %v", recoveredCount)
|
|
}
|
|
|
|
t.Log("PAWS wrap test passed")
|
|
}
|
|
|
|
func TestFECRTOAndSkipParity(t *testing.T) {
|
|
const dataShards = 3
|
|
const parityShards = 2
|
|
const rto = 50 // 50ms RTO
|
|
|
|
enc := newFECEncoder(dataShards, parityShards, 0)
|
|
|
|
// Helper to extract seqid from packet
|
|
getSeq := func(b []byte) uint32 {
|
|
return binary.LittleEndian.Uint32(b)
|
|
}
|
|
|
|
// --- Scenario 1: Normal case (Time < RTO) ---
|
|
// Send 3 packets quickly
|
|
t.Log("--- Scenario 1: Normal case (Time < RTO) ---")
|
|
|
|
// Packet 0
|
|
p0 := make([]byte, 100)
|
|
ps := enc.encode(p0, rto)
|
|
if len(ps) != 0 {
|
|
t.Fatalf("Expected no parity shards yet")
|
|
}
|
|
if seq := getSeq(p0); seq != 0 {
|
|
t.Fatalf("Expected seq 0, got %d", seq)
|
|
}
|
|
|
|
// Packet 1
|
|
p1 := make([]byte, 100)
|
|
ps = enc.encode(p1, rto)
|
|
if len(ps) != 0 {
|
|
t.Fatalf("Expected no parity shards yet")
|
|
}
|
|
if seq := getSeq(p1); seq != 1 {
|
|
t.Fatalf("Expected seq 1, got %d", seq)
|
|
}
|
|
|
|
// Packet 2 (Trigger parity generation)
|
|
p2 := make([]byte, 100)
|
|
ps = enc.encode(p2, rto)
|
|
if len(ps) != parityShards {
|
|
t.Fatalf("Expected %d parity shards, got %d", parityShards, len(ps))
|
|
}
|
|
if seq := getSeq(p2); seq != 2 {
|
|
t.Fatalf("Expected seq 2, got %d", seq)
|
|
}
|
|
|
|
// Check parity seqids
|
|
if seq := getSeq(ps[0]); seq != 3 {
|
|
t.Fatalf("Expected parity[0] seq 3, got %d", seq)
|
|
}
|
|
if seq := getSeq(ps[1]); seq != 4 {
|
|
t.Fatalf("Expected parity[1] seq 4, got %d", seq)
|
|
}
|
|
|
|
// --- Scenario 2: Timeout case (Time > RTO) ---
|
|
// Send 2 packets quickly, then sleep, then send 3rd
|
|
t.Log("--- Scenario 2: Timeout case (Time > RTO) ---")
|
|
|
|
// Packet 3 (Next data seq should be 5)
|
|
p3 := make([]byte, 100)
|
|
ps = enc.encode(p3, rto)
|
|
if len(ps) != 0 {
|
|
t.Fatalf("Expected no parity shards yet")
|
|
}
|
|
if seq := getSeq(p3); seq != 5 {
|
|
t.Fatalf("Expected seq 5, got %d", seq)
|
|
}
|
|
|
|
// Packet 4
|
|
p4 := make([]byte, 100)
|
|
ps = enc.encode(p4, rto)
|
|
if len(ps) != 0 {
|
|
t.Fatalf("Expected no parity shards yet")
|
|
}
|
|
if seq := getSeq(p4); seq != 6 {
|
|
t.Fatalf("Expected seq 6, got %d", seq)
|
|
}
|
|
|
|
// Sleep longer than RTO
|
|
time.Sleep(time.Duration(rto+20) * time.Millisecond)
|
|
|
|
// Packet 5 (Trigger parity check -> should skip)
|
|
p5 := make([]byte, 100)
|
|
ps = enc.encode(p5, rto)
|
|
|
|
// Expect NO parity shards because of timeout
|
|
if len(ps) != 0 {
|
|
t.Fatalf("Expected 0 parity shards due to timeout, got %d", len(ps))
|
|
}
|
|
if seq := getSeq(p5); seq != 7 {
|
|
t.Fatalf("Expected seq 7, got %d", seq)
|
|
}
|
|
|
|
// Even though parity was skipped, the sequence ID should have advanced by parityShards (2)
|
|
// So next data packet should be 7 + 1 (current) + 2 (skipped parity) = 10?
|
|
// Wait, let's trace:
|
|
// p5 gets seq 7.
|
|
// encode() calls skipParity() -> enc.next += parityShards.
|
|
// enc.next was 8 (after p5). 8 + 2 = 10.
|
|
// So next packet should have seq 10.
|
|
|
|
// --- Verify Sequence ID Growth after Skip ---
|
|
t.Log("--- Verify Sequence ID Growth after Skip ---")
|
|
|
|
p6 := make([]byte, 100)
|
|
ps = enc.encode(p6, rto)
|
|
if seq := getSeq(p6); seq != 10 {
|
|
t.Fatalf("Expected seq 10 after skipped parity, got %d", seq)
|
|
}
|
|
}
|