// 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 snd_queue = new Queue(16); // send queue internal readonly Queue rcv_queue = new Queue(16); // receive queue // snd_buffer needs index removals. // C# LinkedList allocates for each entry, so let's keep List for now. internal readonly List snd_buf = new List(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 rcv_buf = new List(16); // receive buffer internal readonly List acklist = new List(16); // memory buffer // size depends on MTU. // MTU can be changed at runtime, which resizes the buffer. internal byte[] buffer; // output function of type readonly Action output; // segment pool to avoid allocations in C#. // this is not part of the original C code. readonly Pool SegmentPool = new Pool( // 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 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); } } } }