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);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|