1119 lines
40 KiB
C#
1119 lines
40 KiB
C#
// Kcp based on https://github.com/skywind3000/kcp
|
||
// Kept as close to original as possible.
|
||
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace kcp2k
|
||
{
|
||
public class Kcp
|
||
{
|
||
// original Kcp has a define option, which is not defined by default:
|
||
// #define FASTACK_CONSERVE
|
||
|
||
public const int RTO_NDL = 30; // no delay min rto
|
||
public const int RTO_MIN = 100; // normal min rto
|
||
public const int RTO_DEF = 200; // default RTO
|
||
public const int RTO_MAX = 60000; // maximum RTO
|
||
public const int CMD_PUSH = 81; // cmd: push data
|
||
public const int CMD_ACK = 82; // cmd: ack
|
||
public const int CMD_WASK = 83; // cmd: window probe (ask)
|
||
public const int CMD_WINS = 84; // cmd: window size (tell/insert)
|
||
public const int ASK_SEND = 1; // need to send CMD_WASK
|
||
public const int ASK_TELL = 2; // need to send CMD_WINS
|
||
public const int WND_SND = 32; // default send window
|
||
public const int WND_RCV = 128; // default receive window. must be >= max fragment size
|
||
public const int MTU_DEF = 1200; // default MTU (reduced to 1200 to fit all cases: https://en.wikipedia.org/wiki/Maximum_transmission_unit ; steam uses 1200 too!)
|
||
public const int ACK_FAST = 3;
|
||
public const int INTERVAL = 100;
|
||
public const int OVERHEAD = 24;
|
||
public const int FRG_MAX = byte.MaxValue; // kcp encodes 'frg' as byte. so we can only ever send up to 255 fragments.
|
||
public const int DEADLINK = 20; // default maximum amount of 'xmit' retransmissions until a segment is considered lost
|
||
public const int THRESH_INIT = 2;
|
||
public const int THRESH_MIN = 2;
|
||
public const int PROBE_INIT = 7000; // 7 secs to probe window size
|
||
public const int PROBE_LIMIT = 120000; // up to 120 secs to probe window
|
||
public const int FASTACK_LIMIT = 5; // max times to trigger fastack
|
||
|
||
// kcp members.
|
||
internal int state;
|
||
readonly uint conv; // conversation
|
||
internal uint mtu;
|
||
internal uint mss; // maximum segment size := MTU - OVERHEAD
|
||
internal uint snd_una; // unacknowledged. e.g. snd_una is 9 it means 8 has been confirmed, 9 and 10 have been sent
|
||
internal uint snd_nxt; // forever growing send counter for sequence numbers
|
||
internal uint rcv_nxt; // forever growing receive counter for sequence numbers
|
||
internal uint ssthresh; // slow start threshold
|
||
internal int rx_rttval; // average deviation of rtt, used to measure the jitter of rtt
|
||
internal int rx_srtt; // smoothed round trip time (a weighted average of rtt)
|
||
internal int rx_rto;
|
||
internal int rx_minrto;
|
||
internal uint snd_wnd; // send window
|
||
internal uint rcv_wnd; // receive window
|
||
internal uint rmt_wnd; // remote window
|
||
internal uint cwnd; // congestion window
|
||
internal uint probe;
|
||
internal uint interval;
|
||
internal uint ts_flush; // last flush timestamp in milliseconds
|
||
internal uint xmit;
|
||
internal uint nodelay; // not a bool. original Kcp has '<2 else' check.
|
||
internal bool updated;
|
||
internal uint ts_probe; // probe timestamp
|
||
internal uint probe_wait;
|
||
internal uint dead_link; // maximum amount of 'xmit' retransmissions until a segment is considered lost
|
||
internal uint incr;
|
||
internal uint current; // current time (milliseconds). set by Update.
|
||
|
||
internal int fastresend;
|
||
internal int fastlimit;
|
||
internal bool nocwnd; // congestion control, negated. heavily restricts send/recv window sizes.
|
||
internal readonly Queue<Segment> snd_queue = new Queue<Segment>(16); // send queue
|
||
internal readonly Queue<Segment> rcv_queue = new Queue<Segment>(16); // receive queue
|
||
// snd_buffer needs index removals.
|
||
// C# LinkedList allocates for each entry, so let's keep List for now.
|
||
internal readonly List<Segment> snd_buf = new List<Segment>(16); // send buffer
|
||
// rcv_buffer needs index insertions and backwards iteration.
|
||
// C# LinkedList allocates for each entry, so let's keep List for now.
|
||
internal readonly List<Segment> rcv_buf = new List<Segment>(16); // receive buffer
|
||
internal readonly List<AckItem> acklist = new List<AckItem>(16);
|
||
|
||
// memory buffer
|
||
// size depends on MTU.
|
||
// MTU can be changed at runtime, which resizes the buffer.
|
||
internal byte[] buffer;
|
||
|
||
// output function of type <buffer, size>
|
||
readonly Action<byte[], int> output;
|
||
|
||
// segment pool to avoid allocations in C#.
|
||
// this is not part of the original C code.
|
||
readonly Pool<Segment> SegmentPool = new Pool<Segment>(
|
||
// create new segment
|
||
() => new Segment(),
|
||
// reset segment before reuse
|
||
(segment) => segment.Reset(),
|
||
// initial capacity
|
||
32
|
||
);
|
||
|
||
// ikcp_create
|
||
// create a new kcp control object, 'conv' must equal in two endpoint
|
||
// from the same connection.
|
||
public Kcp(uint conv, Action<byte[], int> output)
|
||
{
|
||
this.conv = conv;
|
||
this.output = output;
|
||
snd_wnd = WND_SND;
|
||
rcv_wnd = WND_RCV;
|
||
rmt_wnd = WND_RCV;
|
||
mtu = MTU_DEF;
|
||
mss = mtu - OVERHEAD;
|
||
rx_rto = RTO_DEF;
|
||
rx_minrto = RTO_MIN;
|
||
interval = INTERVAL;
|
||
ts_flush = INTERVAL;
|
||
ssthresh = THRESH_INIT;
|
||
fastlimit = FASTACK_LIMIT;
|
||
dead_link = DEADLINK;
|
||
buffer = new byte[(mtu + OVERHEAD) * 3];
|
||
}
|
||
|
||
// ikcp_segment_new
|
||
// we keep the original function and add our pooling to it.
|
||
// this way we'll never miss it anywhere.
|
||
Segment SegmentNew() => SegmentPool.Take();
|
||
|
||
// ikcp_segment_delete
|
||
// we keep the original function and add our pooling to it.
|
||
// this way we'll never miss it anywhere.
|
||
void SegmentDelete(Segment seg) => SegmentPool.Return(seg);
|
||
|
||
// calculate how many packets are waiting to be sent
|
||
public int WaitSnd => snd_buf.Count + snd_queue.Count;
|
||
|
||
// ikcp_wnd_unused
|
||
// returns the remaining space in receive window (rcv_wnd - rcv_queue)
|
||
internal uint WndUnused()
|
||
{
|
||
if (rcv_queue.Count < rcv_wnd)
|
||
return rcv_wnd - (uint)rcv_queue.Count;
|
||
return 0;
|
||
}
|
||
|
||
// ikcp_recv
|
||
// receive data from kcp state machine
|
||
// returns number of bytes read.
|
||
// returns negative on error.
|
||
// note: pass negative length to peek.
|
||
public int Receive(byte[] buffer, int len)
|
||
{
|
||
// kcp's ispeek feature is not supported.
|
||
// this makes 'merge fragment' code significantly easier because
|
||
// we can iterate while queue.Count > 0 and dequeue each time.
|
||
// if we had to consider ispeek then count would always be > 0 and
|
||
// we would have to remove only after the loop.
|
||
//
|
||
//bool ispeek = len < 0;
|
||
if (len < 0)
|
||
throw new NotSupportedException("Receive ispeek for negative len is not supported!");
|
||
|
||
if (rcv_queue.Count == 0)
|
||
return -1;
|
||
|
||
if (len < 0) len = -len;
|
||
|
||
int peeksize = PeekSize();
|
||
|
||
if (peeksize < 0)
|
||
return -2;
|
||
|
||
if (peeksize > len)
|
||
return -3;
|
||
|
||
bool recover = rcv_queue.Count >= rcv_wnd;
|
||
|
||
// merge fragment.
|
||
int offset = 0;
|
||
len = 0;
|
||
// original KCP iterates rcv_queue and deletes if !ispeek.
|
||
// removing from a c# queue while iterating is not possible, but
|
||
// we can change to 'while Count > 0' and remove every time.
|
||
// (we can remove every time because we removed ispeek support!)
|
||
while (rcv_queue.Count > 0)
|
||
{
|
||
// unlike original kcp, we dequeue instead of just getting the
|
||
// entry. this is fine because we remove it in ANY case.
|
||
Segment seg = rcv_queue.Dequeue();
|
||
|
||
// copy segment data into our buffer
|
||
Buffer.BlockCopy(seg.data.GetBuffer(), 0, buffer, offset, (int)seg.data.Position);
|
||
offset += (int)seg.data.Position;
|
||
|
||
len += (int)seg.data.Position;
|
||
uint fragment = seg.frg;
|
||
|
||
// note: ispeek is not supported in order to simplify this loop
|
||
|
||
// unlike original kcp, we don't need to remove seg from queue
|
||
// because we already dequeued it.
|
||
// simply delete it
|
||
SegmentDelete(seg);
|
||
|
||
if (fragment == 0)
|
||
break;
|
||
}
|
||
|
||
// move available data from rcv_buf -> rcv_queue
|
||
int removed = 0;
|
||
foreach (Segment seg in rcv_buf)
|
||
{
|
||
if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd)
|
||
{
|
||
// can't remove while iterating. remember how many to remove
|
||
// and do it after the loop.
|
||
// note: don't return segment. we only add it to rcv_queue
|
||
++removed;
|
||
// add
|
||
rcv_queue.Enqueue(seg);
|
||
// increase sequence number for next segment
|
||
rcv_nxt++;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
rcv_buf.RemoveRange(0, removed);
|
||
|
||
// fast recover
|
||
if (rcv_queue.Count < rcv_wnd && recover)
|
||
{
|
||
// ready to send back CMD_WINS in flush
|
||
// tell remote my window size
|
||
probe |= ASK_TELL;
|
||
}
|
||
|
||
return len;
|
||
}
|
||
|
||
// ikcp_peeksize
|
||
// check the size of next message in the recv queue.
|
||
// returns -1 if there is no message, or if the message is still incomplete.
|
||
public int PeekSize()
|
||
{
|
||
int length = 0;
|
||
|
||
// empty queue?
|
||
if (rcv_queue.Count == 0) return -1;
|
||
|
||
// peek the first segment
|
||
Segment seq = rcv_queue.Peek();
|
||
|
||
// seg.frg is 0 if the message requires no fragmentation.
|
||
// in that case, the segment's size is the final message size.
|
||
if (seq.frg == 0) return (int)seq.data.Position;
|
||
|
||
// check if all fragment parts were received yet.
|
||
// seg.frg is the n-th fragment, but in reverse.
|
||
// this way the first received segment tells us how many fragments there are for the message.
|
||
// for example, if a message contains 3 segments:
|
||
// first segment: .frg is 2 (index in reverse)
|
||
// second segment: .frg is 1 (index in reverse)
|
||
// third segment: .frg is 0 (index in reverse)
|
||
if (rcv_queue.Count < seq.frg + 1) return -1;
|
||
|
||
// recv_queue contains all the fragments necessary to reconstruct the message.
|
||
// sum all fragment's sizes to get the full message size.
|
||
foreach (Segment seg in rcv_queue)
|
||
{
|
||
length += (int)seg.data.Position;
|
||
if (seg.frg == 0) break;
|
||
}
|
||
|
||
return length;
|
||
}
|
||
|
||
// ikcp_send
|
||
// splits message into MTU sized fragments, adds them to snd_queue.
|
||
public int Send(byte[] buffer, int offset, int len)
|
||
{
|
||
// fragment count
|
||
int count;
|
||
|
||
if (len < 0) return -1;
|
||
|
||
// streaming mode: removed. we never want to send 'hello' and
|
||
// receive 'he' 'll' 'o'. we want to always receive 'hello'.
|
||
|
||
// calculate amount of fragments necessary for 'len'
|
||
if (len <= mss) count = 1;
|
||
else count = (int)((len + mss - 1) / mss);
|
||
|
||
// IMPORTANT kcp encodes 'frg' as 1 byte.
|
||
// so we can only support up to 255 fragments.
|
||
// (which limits max message size to around 288 KB)
|
||
// this is difficult to debug. let's make this 100% obvious.
|
||
if (count > FRG_MAX)
|
||
throw new Exception($"Send len={len} requires {count} fragments, but kcp can only handle up to {FRG_MAX} fragments.");
|
||
|
||
// original kcp uses WND_RCV const instead of rcv_wnd runtime:
|
||
// https://github.com/skywind3000/kcp/pull/291/files
|
||
// which always limits max message size to 144 KB:
|
||
//if (count >= WND_RCV) return -2;
|
||
// using configured rcv_wnd uncorks max message size to 'any':
|
||
if (count >= rcv_wnd) return -2;
|
||
|
||
if (count == 0) count = 1;
|
||
|
||
// fragment
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
int size = len > (int)mss ? (int)mss : len;
|
||
Segment seg = SegmentNew();
|
||
|
||
if (len > 0)
|
||
{
|
||
seg.data.Write(buffer, offset, size);
|
||
}
|
||
// seg.len = size: WriteBytes sets segment.Position!
|
||
|
||
// set fragment number.
|
||
// if the message requires no fragmentation, then
|
||
// seg.frg becomes 1-0-1 = 0
|
||
seg.frg = (uint)(count - i - 1);
|
||
snd_queue.Enqueue(seg);
|
||
offset += size;
|
||
len -= size;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// ikcp_update_ack
|
||
void UpdateAck(int rtt) // round trip time
|
||
{
|
||
// https://tools.ietf.org/html/rfc6298
|
||
if (rx_srtt == 0)
|
||
{
|
||
rx_srtt = rtt;
|
||
rx_rttval = rtt / 2;
|
||
}
|
||
else
|
||
{
|
||
int delta = rtt - rx_srtt;
|
||
if (delta < 0) delta = -delta;
|
||
rx_rttval = (3 * rx_rttval + delta) / 4;
|
||
rx_srtt = (7 * rx_srtt + rtt) / 8;
|
||
if (rx_srtt < 1) rx_srtt = 1;
|
||
}
|
||
int rto = rx_srtt + Math.Max((int)interval, 4 * rx_rttval);
|
||
rx_rto = Utils.Clamp(rto, rx_minrto, RTO_MAX);
|
||
}
|
||
|
||
// ikcp_shrink_buf
|
||
internal void ShrinkBuf()
|
||
{
|
||
if (snd_buf.Count > 0)
|
||
{
|
||
Segment seg = snd_buf[0];
|
||
snd_una = seg.sn;
|
||
}
|
||
else
|
||
{
|
||
snd_una = snd_nxt;
|
||
}
|
||
}
|
||
|
||
// ikcp_parse_ack
|
||
// removes the segment with 'sn' from send buffer
|
||
internal void ParseAck(uint sn)
|
||
{
|
||
if (Utils.TimeDiff(sn, snd_una) < 0 || Utils.TimeDiff(sn, snd_nxt) >= 0)
|
||
return;
|
||
|
||
// for-int so we can erase while iterating
|
||
for (int i = 0; i < snd_buf.Count; ++i)
|
||
{
|
||
// is this the segment?
|
||
Segment seg = snd_buf[i];
|
||
if (sn == seg.sn)
|
||
{
|
||
// remove and return
|
||
snd_buf.RemoveAt(i);
|
||
SegmentDelete(seg);
|
||
break;
|
||
}
|
||
if (Utils.TimeDiff(sn, seg.sn) < 0)
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ikcp_parse_una
|
||
// removes all unacknowledged segments with sequence numbers < una from send buffer
|
||
internal void ParseUna(uint una)
|
||
{
|
||
int removed = 0;
|
||
foreach (Segment seg in snd_buf)
|
||
{
|
||
// if (Utils.TimeDiff(una, seg.sn) > 0)
|
||
if (seg.sn < una)
|
||
{
|
||
// can't remove while iterating. remember how many to remove
|
||
// and do it after the loop.
|
||
++removed;
|
||
SegmentDelete(seg);
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
snd_buf.RemoveRange(0, removed);
|
||
}
|
||
|
||
// ikcp_parse_fastack
|
||
internal void ParseFastack(uint sn, uint ts) // serial number, timestamp
|
||
{
|
||
// sn needs to be between snd_una and snd_nxt
|
||
// if !(snd_una <= sn && sn < snd_nxt) return;
|
||
|
||
// if (Utils.TimeDiff(sn, snd_una) < 0)
|
||
if (sn < snd_una)
|
||
return;
|
||
|
||
// if (Utils.TimeDiff(sn, snd_nxt) >= 0)
|
||
if (sn >= snd_nxt)
|
||
return;
|
||
|
||
foreach (Segment seg in snd_buf)
|
||
{
|
||
// if (Utils.TimeDiff(sn, seg.sn) < 0)
|
||
if (sn < seg.sn)
|
||
{
|
||
break;
|
||
}
|
||
else if (sn != seg.sn)
|
||
{
|
||
#if !FASTACK_CONSERVE
|
||
seg.fastack++;
|
||
#else
|
||
if (Utils.TimeDiff(ts, seg.ts) >= 0)
|
||
seg.fastack++;
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
|
||
// ikcp_ack_push
|
||
// appends an ack.
|
||
void AckPush(uint sn, uint ts) // serial number, timestamp
|
||
{
|
||
acklist.Add(new AckItem{ serialNumber = sn, timestamp = ts });
|
||
}
|
||
|
||
// ikcp_parse_data
|
||
void ParseData(Segment newseg)
|
||
{
|
||
uint sn = newseg.sn;
|
||
|
||
if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) >= 0 ||
|
||
Utils.TimeDiff(sn, rcv_nxt) < 0)
|
||
{
|
||
SegmentDelete(newseg);
|
||
return;
|
||
}
|
||
|
||
InsertSegmentInReceiveBuffer(newseg);
|
||
MoveReceiveBufferReadySegmentsToQueue();
|
||
}
|
||
|
||
// inserts the segment into rcv_buf, ordered by seg.sn.
|
||
// drops the segment if one with the same seg.sn already exists.
|
||
// goes through receive buffer in reverse order for performance.
|
||
//
|
||
// note: see KcpTests.InsertSegmentInReceiveBuffer test!
|
||
// note: 'insert or delete' can be done in different ways, but let's
|
||
// keep consistency with original C kcp.
|
||
internal void InsertSegmentInReceiveBuffer(Segment newseg)
|
||
{
|
||
bool repeat = false; // 'duplicate'
|
||
|
||
// original C iterates backwards, so we need to do that as well.
|
||
// note if rcv_buf.Count == 0, i becomes -1 and no looping happens.
|
||
int i;
|
||
for (i = rcv_buf.Count - 1; i >= 0; i--)
|
||
{
|
||
Segment seg = rcv_buf[i];
|
||
if (seg.sn == newseg.sn)
|
||
{
|
||
// duplicate segment found. nothing will be added.
|
||
repeat = true;
|
||
break;
|
||
}
|
||
if (Utils.TimeDiff(newseg.sn, seg.sn) > 0)
|
||
{
|
||
// this entry's sn is < newseg.sn, so let's stop
|
||
break;
|
||
}
|
||
}
|
||
|
||
// no duplicate? then insert.
|
||
if (!repeat)
|
||
{
|
||
rcv_buf.Insert(i + 1, newseg);
|
||
}
|
||
// duplicate. just delete it.
|
||
else
|
||
{
|
||
SegmentDelete(newseg);
|
||
}
|
||
}
|
||
|
||
// move ready segments from rcv_buf -> rcv_queue.
|
||
// moves only the ready segments which are in rcv_nxt sequence order.
|
||
// some may still be missing an inserted later.
|
||
void MoveReceiveBufferReadySegmentsToQueue()
|
||
{
|
||
int removed = 0;
|
||
foreach (Segment seg in rcv_buf)
|
||
{
|
||
// move segments while they are in 'rcv_nxt' sequence order.
|
||
// some may still be missing and inserted later, in this case it stops immediately
|
||
// because segments always need to be received in the exact sequence order.
|
||
if (seg.sn == rcv_nxt && rcv_queue.Count < rcv_wnd)
|
||
{
|
||
// can't remove while iterating. remember how many to remove
|
||
// and do it after the loop.
|
||
++removed;
|
||
rcv_queue.Enqueue(seg);
|
||
// increase sequence number for next segment
|
||
rcv_nxt++;
|
||
}
|
||
else
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
rcv_buf.RemoveRange(0, removed);
|
||
}
|
||
|
||
// ikcp_input
|
||
// used when you receive a low level packet (e.g. UDP packet)
|
||
// => original kcp uses offset=0, we made it a parameter so that high
|
||
// level can skip the channel byte more easily
|
||
public int Input(byte[] data, int offset, int size)
|
||
{
|
||
uint prev_una = snd_una;
|
||
uint maxack = 0;
|
||
uint latest_ts = 0;
|
||
int flag = 0;
|
||
|
||
if (data == null || size < OVERHEAD) return -1;
|
||
|
||
while (true)
|
||
{
|
||
// enough data left to decode segment (aka OVERHEAD bytes)?
|
||
if (size < OVERHEAD) break;
|
||
|
||
// decode segment
|
||
offset += Utils.Decode32U(data, offset, out uint conv_);
|
||
if (conv_ != conv) return -1;
|
||
|
||
offset += Utils.Decode8u(data, offset, out byte cmd);
|
||
// IMPORTANT kcp encodes 'frg' as 1 byte.
|
||
// so we can only support up to 255 fragments.
|
||
// (which limits max message size to around 288 KB)
|
||
offset += Utils.Decode8u(data, offset, out byte frg);
|
||
offset += Utils.Decode16U(data, offset, out ushort wnd);
|
||
offset += Utils.Decode32U(data, offset, out uint ts);
|
||
offset += Utils.Decode32U(data, offset, out uint sn);
|
||
offset += Utils.Decode32U(data, offset, out uint una);
|
||
offset += Utils.Decode32U(data, offset, out uint len);
|
||
|
||
// reduce remaining size by what was read
|
||
size -= OVERHEAD;
|
||
|
||
// enough remaining to read 'len' bytes of the actual payload?
|
||
// note: original kcp casts uint len to int for <0 check.
|
||
if (size < len || (int)len < 0) return -2;
|
||
|
||
// validate command type
|
||
if (cmd != CMD_PUSH && cmd != CMD_ACK &&
|
||
cmd != CMD_WASK && cmd != CMD_WINS)
|
||
return -3;
|
||
|
||
rmt_wnd = wnd;
|
||
ParseUna(una);
|
||
ShrinkBuf();
|
||
|
||
if (cmd == CMD_ACK)
|
||
{
|
||
if (Utils.TimeDiff(current, ts) >= 0)
|
||
{
|
||
UpdateAck(Utils.TimeDiff(current, ts));
|
||
}
|
||
ParseAck(sn);
|
||
ShrinkBuf();
|
||
if (flag == 0)
|
||
{
|
||
flag = 1;
|
||
maxack = sn;
|
||
latest_ts = ts;
|
||
}
|
||
else
|
||
{
|
||
if (Utils.TimeDiff(sn, maxack) > 0)
|
||
{
|
||
#if !FASTACK_CONSERVE
|
||
maxack = sn;
|
||
latest_ts = ts;
|
||
#else
|
||
if (Utils.TimeDiff(ts, latest_ts) > 0)
|
||
{
|
||
maxack = sn;
|
||
latest_ts = ts;
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
else if (cmd == CMD_PUSH)
|
||
{
|
||
if (Utils.TimeDiff(sn, rcv_nxt + rcv_wnd) < 0)
|
||
{
|
||
AckPush(sn, ts);
|
||
if (Utils.TimeDiff(sn, rcv_nxt) >= 0)
|
||
{
|
||
Segment seg = SegmentNew();
|
||
seg.conv = conv_;
|
||
seg.cmd = cmd;
|
||
seg.frg = frg;
|
||
seg.wnd = wnd;
|
||
seg.ts = ts;
|
||
seg.sn = sn;
|
||
seg.una = una;
|
||
if (len > 0)
|
||
{
|
||
seg.data.Write(data, offset, (int)len);
|
||
}
|
||
ParseData(seg);
|
||
}
|
||
}
|
||
}
|
||
else if (cmd == CMD_WASK)
|
||
{
|
||
// ready to send back CMD_WINS in flush
|
||
// tell remote my window size
|
||
probe |= ASK_TELL;
|
||
}
|
||
else if (cmd == CMD_WINS)
|
||
{
|
||
// do nothing
|
||
}
|
||
else
|
||
{
|
||
return -3;
|
||
}
|
||
|
||
offset += (int)len;
|
||
size -= (int)len;
|
||
}
|
||
|
||
if (flag != 0)
|
||
{
|
||
ParseFastack(maxack, latest_ts);
|
||
}
|
||
|
||
// cwnd update when packet arrived
|
||
if (Utils.TimeDiff(snd_una, prev_una) > 0)
|
||
{
|
||
if (cwnd < rmt_wnd)
|
||
{
|
||
if (cwnd < ssthresh)
|
||
{
|
||
cwnd++;
|
||
incr += mss;
|
||
}
|
||
else
|
||
{
|
||
if (incr < mss) incr = mss;
|
||
incr += (mss * mss) / incr + (mss / 16);
|
||
if ((cwnd + 1) * mss <= incr)
|
||
{
|
||
cwnd = (incr + mss - 1) / ((mss > 0) ? mss : 1);
|
||
}
|
||
}
|
||
if (cwnd > rmt_wnd)
|
||
{
|
||
cwnd = rmt_wnd;
|
||
incr = rmt_wnd * mss;
|
||
}
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// flush helper function
|
||
void MakeSpace(ref int size, int space)
|
||
{
|
||
if (size + space > mtu)
|
||
{
|
||
output(buffer, size);
|
||
size = 0;
|
||
}
|
||
}
|
||
|
||
// flush helper function
|
||
void FlushBuffer(int size)
|
||
{
|
||
// flush buffer up to 'offset' (<= MTU)
|
||
if (size > 0)
|
||
{
|
||
output(buffer, size);
|
||
}
|
||
}
|
||
|
||
// ikcp_flush
|
||
// flush remain ack segments.
|
||
// flush may output multiple <= MTU messages from MakeSpace / FlushBuffer.
|
||
// the amount of messages depends on the sliding window.
|
||
// configured by send/receive window sizes + congestion control.
|
||
// with congestion control, the window will be extremely small(!).
|
||
public void Flush()
|
||
{
|
||
int size = 0; // amount of bytes to flush. 'buffer ptr' in C.
|
||
bool lost = false; // lost segments
|
||
|
||
// update needs to be called before flushing
|
||
if (!updated) return;
|
||
|
||
// kcp only stack allocates a segment here for performance, leaving
|
||
// its data buffer null because this segment's data buffer is never
|
||
// used. that's fine in C, but in C# our segment is a class so we
|
||
// need to allocate and most importantly, not forget to deallocate
|
||
// it before returning.
|
||
Segment seg = SegmentNew();
|
||
seg.conv = conv;
|
||
seg.cmd = CMD_ACK;
|
||
seg.wnd = WndUnused();
|
||
seg.una = rcv_nxt;
|
||
|
||
// flush acknowledges
|
||
foreach (AckItem ack in acklist)
|
||
{
|
||
MakeSpace(ref size, OVERHEAD);
|
||
// ikcp_ack_get assigns ack[i] to seg.sn, seg.ts
|
||
seg.sn = ack.serialNumber;
|
||
seg.ts = ack.timestamp;
|
||
size += seg.Encode(buffer, size);
|
||
}
|
||
acklist.Clear();
|
||
|
||
// probe window size (if remote window size equals zero)
|
||
if (rmt_wnd == 0)
|
||
{
|
||
if (probe_wait == 0)
|
||
{
|
||
probe_wait = PROBE_INIT;
|
||
ts_probe = current + probe_wait;
|
||
}
|
||
else
|
||
{
|
||
if (Utils.TimeDiff(current, ts_probe) >= 0)
|
||
{
|
||
if (probe_wait < PROBE_INIT)
|
||
probe_wait = PROBE_INIT;
|
||
probe_wait += probe_wait / 2;
|
||
if (probe_wait > PROBE_LIMIT)
|
||
probe_wait = PROBE_LIMIT;
|
||
ts_probe = current + probe_wait;
|
||
probe |= ASK_SEND;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ts_probe = 0;
|
||
probe_wait = 0;
|
||
}
|
||
|
||
// flush window probing commands
|
||
if ((probe & ASK_SEND) != 0)
|
||
{
|
||
seg.cmd = CMD_WASK;
|
||
MakeSpace(ref size, OVERHEAD);
|
||
size += seg.Encode(buffer, size);
|
||
}
|
||
|
||
// flush window probing commands
|
||
if ((probe & ASK_TELL) != 0)
|
||
{
|
||
seg.cmd = CMD_WINS;
|
||
MakeSpace(ref size, OVERHEAD);
|
||
size += seg.Encode(buffer, size);
|
||
}
|
||
|
||
probe = 0;
|
||
|
||
// calculate the window size which is currently safe to send.
|
||
// it's send window, or remote window, whatever is smaller.
|
||
// for our max
|
||
uint cwnd_ = Math.Min(snd_wnd, rmt_wnd);
|
||
|
||
// double negative: if congestion window is enabled:
|
||
// limit window size to cwnd.
|
||
//
|
||
// note this may heavily limit window sizes.
|
||
// for our max message size test with super large windows of 32k,
|
||
// 'congestion window' limits it down from 32.000 to 2.
|
||
if (!nocwnd) cwnd_ = Math.Min(cwnd, cwnd_);
|
||
|
||
// move cwnd_ 'window size' messages from snd_queue to snd_buf
|
||
// 'snd_nxt' is what we want to send.
|
||
// 'snd_una' is what hasn't been acked yet.
|
||
// copy up to 'cwnd_' difference between them (sliding window)
|
||
while (Utils.TimeDiff(snd_nxt, snd_una + cwnd_) < 0)
|
||
{
|
||
if (snd_queue.Count == 0) break;
|
||
|
||
Segment newseg = snd_queue.Dequeue();
|
||
|
||
newseg.conv = conv;
|
||
newseg.cmd = CMD_PUSH;
|
||
newseg.wnd = seg.wnd;
|
||
newseg.ts = current;
|
||
newseg.sn = snd_nxt;
|
||
snd_nxt += 1; // increase sequence number for next segment
|
||
newseg.una = rcv_nxt;
|
||
newseg.resendts = current;
|
||
newseg.rto = rx_rto;
|
||
newseg.fastack = 0;
|
||
newseg.xmit = 0;
|
||
snd_buf.Add(newseg);
|
||
}
|
||
|
||
// calculate resent
|
||
uint resent = fastresend > 0 ? (uint)fastresend : 0xffffffff;
|
||
uint rtomin = nodelay == 0 ? (uint)rx_rto >> 3 : 0;
|
||
|
||
// flush data segments
|
||
int change = 0;
|
||
foreach (Segment segment in snd_buf)
|
||
{
|
||
bool needsend = false;
|
||
|
||
// initial transmit
|
||
if (segment.xmit == 0)
|
||
{
|
||
needsend = true;
|
||
segment.xmit++;
|
||
segment.rto = rx_rto;
|
||
segment.resendts = current + (uint)segment.rto + rtomin;
|
||
}
|
||
// RTO
|
||
else if (Utils.TimeDiff(current, segment.resendts) >= 0)
|
||
{
|
||
needsend = true;
|
||
segment.xmit++;
|
||
xmit++;
|
||
if (nodelay == 0)
|
||
{
|
||
segment.rto += Math.Max(segment.rto, rx_rto);
|
||
}
|
||
else
|
||
{
|
||
int step = (nodelay < 2) ? segment.rto : rx_rto;
|
||
segment.rto += step / 2;
|
||
}
|
||
segment.resendts = current + (uint)segment.rto;
|
||
lost = true;
|
||
}
|
||
// fast retransmit
|
||
else if (segment.fastack >= resent)
|
||
{
|
||
if (segment.xmit <= fastlimit || fastlimit <= 0)
|
||
{
|
||
needsend = true;
|
||
segment.xmit++;
|
||
segment.fastack = 0;
|
||
segment.resendts = current + (uint)segment.rto;
|
||
change++;
|
||
}
|
||
}
|
||
|
||
if (needsend)
|
||
{
|
||
segment.ts = current;
|
||
segment.wnd = seg.wnd;
|
||
segment.una = rcv_nxt;
|
||
|
||
int need = OVERHEAD + (int)segment.data.Position;
|
||
MakeSpace(ref size, need);
|
||
|
||
size += segment.Encode(buffer, size);
|
||
|
||
if (segment.data.Position > 0)
|
||
{
|
||
Buffer.BlockCopy(segment.data.GetBuffer(), 0, buffer, size, (int)segment.data.Position);
|
||
size += (int)segment.data.Position;
|
||
}
|
||
|
||
// dead link happens if a message was resent N times, but an
|
||
// ack was still not received.
|
||
if (segment.xmit >= dead_link)
|
||
{
|
||
state = -1;
|
||
}
|
||
}
|
||
}
|
||
|
||
// kcp stackallocs 'seg'. our C# segment is a class though, so we
|
||
// need to properly delete and return it to the pool now that we are
|
||
// done with it.
|
||
SegmentDelete(seg);
|
||
|
||
// flush remaining segments
|
||
FlushBuffer(size);
|
||
|
||
// update ssthresh
|
||
// rate halving, https://tools.ietf.org/html/rfc6937
|
||
if (change > 0)
|
||
{
|
||
uint inflight = snd_nxt - snd_una;
|
||
ssthresh = inflight / 2;
|
||
if (ssthresh < THRESH_MIN)
|
||
ssthresh = THRESH_MIN;
|
||
cwnd = ssthresh + resent;
|
||
incr = cwnd * mss;
|
||
}
|
||
|
||
// congestion control, https://tools.ietf.org/html/rfc5681
|
||
if (lost)
|
||
{
|
||
// original C uses 'cwnd', not kcp->cwnd!
|
||
ssthresh = cwnd_ / 2;
|
||
if (ssthresh < THRESH_MIN)
|
||
ssthresh = THRESH_MIN;
|
||
cwnd = 1;
|
||
incr = mss;
|
||
}
|
||
|
||
if (cwnd < 1)
|
||
{
|
||
cwnd = 1;
|
||
incr = mss;
|
||
}
|
||
}
|
||
|
||
// ikcp_update
|
||
// update state (call it repeatedly, every 10ms-100ms), or you can ask
|
||
// Check() when to call it again (without Input/Send calling).
|
||
//
|
||
// 'current' - current timestamp in millisec. pass it to Kcp so that
|
||
// Kcp doesn't have to do any stopwatch/deltaTime/etc. code
|
||
//
|
||
// time as uint, likely to minimize bandwidth.
|
||
// uint.max = 4294967295 ms = 1193 hours = 49 days
|
||
public void Update(uint currentTimeMilliSeconds)
|
||
{
|
||
current = currentTimeMilliSeconds;
|
||
|
||
// not updated yet? then set updated and last flush time.
|
||
if (!updated)
|
||
{
|
||
updated = true;
|
||
ts_flush = current;
|
||
}
|
||
|
||
// slap is time since last flush in milliseconds
|
||
int slap = Utils.TimeDiff(current, ts_flush);
|
||
|
||
// hard limit: if 10s elapsed, always flush no matter what
|
||
if (slap >= 10000 || slap < -10000)
|
||
{
|
||
ts_flush = current;
|
||
slap = 0;
|
||
}
|
||
|
||
// last flush is increased by 'interval' each time.
|
||
// so slap >= is a strange way to check if interval has elapsed yet.
|
||
if (slap >= 0)
|
||
{
|
||
// increase last flush time by one interval
|
||
ts_flush += interval;
|
||
|
||
// if last flush is still behind, increase it to current + interval
|
||
// if (Utils.TimeDiff(current, ts_flush) >= 0) // original kcp.c
|
||
if (current >= ts_flush) // less confusing
|
||
{
|
||
ts_flush = current + interval;
|
||
}
|
||
Flush();
|
||
}
|
||
}
|
||
|
||
// ikcp_check
|
||
// Determine when should you invoke update
|
||
// Returns when you should invoke update in millisec, if there is no
|
||
// input/send calling. you can call update in that time, instead of
|
||
// call update repeatly.
|
||
//
|
||
// Important to reduce unnecessary update invoking. use it to schedule
|
||
// update (e.g. implementing an epoll-like mechanism, or optimize update
|
||
// when handling massive kcp connections).
|
||
public uint Check(uint current_)
|
||
{
|
||
uint ts_flush_ = ts_flush;
|
||
// int tm_flush = 0x7fffffff; original kcp: useless assignment
|
||
int tm_packet = 0x7fffffff;
|
||
|
||
if (!updated)
|
||
{
|
||
return current_;
|
||
}
|
||
|
||
if (Utils.TimeDiff(current_, ts_flush_) >= 10000 ||
|
||
Utils.TimeDiff(current_, ts_flush_) < -10000)
|
||
{
|
||
ts_flush_ = current_;
|
||
}
|
||
|
||
if (Utils.TimeDiff(current_, ts_flush_) >= 0)
|
||
{
|
||
return current_;
|
||
}
|
||
|
||
int tm_flush = Utils.TimeDiff(ts_flush_, current_);
|
||
|
||
foreach (Segment seg in snd_buf)
|
||
{
|
||
int diff = Utils.TimeDiff(seg.resendts, current_);
|
||
if (diff <= 0)
|
||
{
|
||
return current_;
|
||
}
|
||
if (diff < tm_packet) tm_packet = diff;
|
||
}
|
||
|
||
uint minimal = (uint)(tm_packet < tm_flush ? tm_packet : tm_flush);
|
||
if (minimal >= interval) minimal = interval;
|
||
|
||
return current_ + minimal;
|
||
}
|
||
|
||
// ikcp_setmtu
|
||
// Change MTU (Maximum Transmission Unit) size.
|
||
public void SetMtu(uint mtu)
|
||
{
|
||
if (mtu < 50 || mtu < OVERHEAD)
|
||
throw new ArgumentException("MTU must be higher than 50 and higher than OVERHEAD");
|
||
|
||
buffer = new byte[(mtu + OVERHEAD) * 3];
|
||
this.mtu = mtu;
|
||
mss = mtu - OVERHEAD;
|
||
}
|
||
|
||
// ikcp_interval
|
||
public void SetInterval(uint interval)
|
||
{
|
||
// clamp interval between 10 and 5000
|
||
if (interval > 5000) interval = 5000;
|
||
else if (interval < 10) interval = 10;
|
||
this.interval = interval;
|
||
}
|
||
|
||
// ikcp_nodelay
|
||
// configuration: https://github.com/skywind3000/kcp/blob/master/README.en.md#protocol-configuration
|
||
// nodelay : Whether nodelay mode is enabled, 0 is not enabled; 1 enabled.
|
||
// interval :Protocol internal work interval, in milliseconds, such as 10 ms or 20 ms.
|
||
// resend :Fast retransmission mode, 0 represents off by default, 2 can be set (2 ACK spans will result in direct retransmission)
|
||
// nc :Whether to turn off flow control, 0 represents “Do not turn off” by default, 1 represents “Turn off”.
|
||
// Normal Mode: ikcp_nodelay(kcp, 0, 40, 0, 0);
|
||
// Turbo Mode: ikcp_nodelay(kcp, 1, 10, 2, 1);
|
||
public void SetNoDelay(uint nodelay, uint interval = INTERVAL, int resend = 0, bool nocwnd = false)
|
||
{
|
||
this.nodelay = nodelay;
|
||
if (nodelay != 0)
|
||
{
|
||
rx_minrto = RTO_NDL;
|
||
}
|
||
else
|
||
{
|
||
rx_minrto = RTO_MIN;
|
||
}
|
||
|
||
if (interval >= 0)
|
||
{
|
||
// clamp interval between 10 and 5000
|
||
if (interval > 5000) interval = 5000;
|
||
else if (interval < 10) interval = 10;
|
||
this.interval = interval;
|
||
}
|
||
|
||
if (resend >= 0)
|
||
{
|
||
fastresend = resend;
|
||
}
|
||
|
||
this.nocwnd = nocwnd;
|
||
}
|
||
|
||
// ikcp_wndsize
|
||
public void SetWindowSize(uint sendWindow, uint receiveWindow)
|
||
{
|
||
if (sendWindow > 0)
|
||
{
|
||
snd_wnd = sendWindow;
|
||
}
|
||
|
||
if (receiveWindow > 0)
|
||
{
|
||
// must >= max fragment size
|
||
rcv_wnd = Math.Max(receiveWindow, WND_RCV);
|
||
}
|
||
}
|
||
}
|
||
}
|