1119 lines
40 KiB
C#
Raw Normal View History

2024-10-17 17:23:05 +03:00
// 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);
}
}
}
}