特定のアプリケーションでは、TCP を使用するだけではニーズを満たすことができません。 UDP データグラムを直接使用してもデータの信頼性は保証できないため、多くの場合、アプリケーション層で UDP に基づく信頼性の高い伝送プロトコルを実装する必要があります。
KCP プロトコルを直接使用するオプションは、堅牢な自動再送信プロトコルを実装し、その上で無料のパラメータ調整を提供します。構成パラメーターと適切な呼び出しメソッドを通じて、さまざまなシナリオのニーズに適応します。
KCP の概要:
KCP は、TCP よりも 10% ~ 20% 多い帯域幅を犠牲にして、平均遅延を 30% ~ 40% 削減し、最大遅延を 3 倍削減できる高速で信頼性の高いプロトコルです。純粋なアルゴリズムの実装は、基礎となるプロトコル (UDP など) の送受信を担当しません。ユーザーは、下位層のデータ パケットの送信メソッドを定義し、それをコールバックの形式で KCP に提供する必要があります。 クロックさえも外部から渡す必要があり、内部ではシステムコールは行われません。 プロトコル全体には、ikcp.h と ikcp.c の 2 つのソース ファイルしかなく、ユーザー独自のプロトコル スタックに簡単に統合できます。 P2P または UDP ベースのプロトコルは実装しているものの、完全で信頼性の高い ARQ プロトコルの実装が不足している場合は、これら 2 つのファイルを既存のプロジェクトにコピーし、数行のコードを記述するだけで使用できます。
この記事では、KCP プロトコルの基本的な送受信プロセス、輻輳ウィンドウ、タイムアウト アルゴリズムを簡単に紹介し、参考サンプル コードも提供します。
参照されている KCP のバージョンは、この記事の執筆時点での最新バージョンです。この記事では、KCP のすべてのソース コードを完全に貼り付けるのではなく、要所要所にソース コードの対応する場所へのリンクを追加します。
IKCPSEG 構造体は、送受信されたデータ セグメントのステータスを格納するために使用されます。
すべての IKCPSEG フィールドの説明:
struct IKCPSEG
{
/* 队列节点,IKCPSEG 作为一个队列元素,此结构指向了队列后前后元素 */
struct IQUEUEHEAD node;
/* 会话编号 */
IUINT32 conv;
/* 指令类型 */
IUINT32 cmd;
/* 分片号 (fragment)
发送数据大于 MSS 时将被分片,0为最后一个分片.
意味着数据可以被recv,如果是流模式,所有分片号都为0
*/
IUINT32 frg;
/* 窗口大小 */
IUINT32 wnd;
/* 时间戳 */
IUINT32 ts;
/* 序号 (sequence number) */
IUINT32 sn;
/* 未确认的序号 (unacknowledged) */
IUINT32 una;
/* 数据长度 */
IUINT32 len;
/* 重传时间 (resend timestamp) */
IUINT32 resendts;
/* 重传的超时时间 (retransmission timeout) */
IUINT32 rto;
/* 快速确认计数 (fast acknowledge) */
IUINT32 fastack;
/* 发送次数 (transmit) */
IUINT32 xmit;
/* 数据内容 */
char data[1];
};
構造体の末尾のdata
フィールドは、構造体の末尾にあるデータのインデックス付けに使用され、追加で割り当てられたメモリにより、実行時にデータ フィールド配列の実際の長さが拡張されます (ikcp.c:173)。
IKCPSEG 構造はメモリ状態のみであり、一部のフィールドのみがトランスポート プロトコルにエンコードされます。
ikcp_encode_seg 関数は、トランスポート プロトコル ヘッダーをエンコードします。
/* 协议头一共 24 字节 */
static char *ikcp_encode_seg(char *ptr, const IKCPSEG *seg)
{
/* 会话编号 (4 Bytes) */
ptr = ikcp_encode32u(ptr, seg->conv);
/* 指令类型 (1 Bytes) */
ptr = ikcp_encode8u(ptr, (IUINT8)seg->cmd);
/* 分片号 (1 Bytes) */
ptr = ikcp_encode8u(ptr, (IUINT8)seg->frg);
/* 窗口大小 (2 Bytes) */
ptr = ikcp_encode16u(ptr, (IUINT16)seg->wnd);
/* 时间戳 (4 Bytes) */
ptr = ikcp_encode32u(ptr, seg->ts);
/* 序号 (4 Bytes) */
ptr = ikcp_encode32u(ptr, seg->sn);
/* 未确认的序号 (4 Bytes) */
ptr = ikcp_encode32u(ptr, seg->una);
/* 数据长度 (4 Bytes) */
ptr = ikcp_encode32u(ptr, seg->len);
return ptr;
}
IKCPCB 構造体には KCP プロトコルのすべてのコンテキストが格納され、プロトコル通信は反対側で 2 つの IKCPCB オブジェクトを作成することによって実行されます。
struct IKCPCB
{
/* conv: 会话编号
mtu: 最大传输单元
mss: 最大报文长度
state: 此会话是否有效 (0: 有效 ~0:无效)
*/
IUINT32 conv, mtu, mss, state;
/* snd_una: 发送的未确认数据段序号
snd_nxt: 发送的下一个数据段序号
rcv_nxt: 期望接收到的下一个数据段的序号
*/
IUINT32 snd_una, snd_nxt, rcv_nxt;
/* ts_recent: (弃用字段?)
ts_lastack: (弃用字段?)
ssthresh: 慢启动阈值 (slow start threshold)
*/
IUINT32 ts_recent, ts_lastack, ssthresh;
/* rx_rttval: 平滑网络抖动时间
rx_srtt: 平滑往返时间
rx_rto: 重传超时时间
rx_minrto: 最小重传超时时间
*/
IINT32 rx_rttval, rx_srtt, rx_rto, rx_minrto;
/* snd_wnd: 发送窗口大小
rcv_wnd: 接收窗口大小
rmt_wnd: 远端窗口大小
cwnd: 拥塞窗口 (congestion window)
probe: 窗口探测标记位,在 flush 时发送特殊的探测包 (window probe)
*/
IUINT32 snd_wnd, rcv_wnd, rmt_wnd, cwnd, probe;
/* current: 当前时间 (ms)
interval: 内部时钟更新周期
ts_flush: 期望的下一次 update/flush 时间
xmit: 全局重传次数计数
*/
IUINT32 current, interval, ts_flush, xmit;
/* nrcv_buf: rcv_buf 接收缓冲区长度
nsnd_buf: snd_buf 发送缓冲区长度
nrcv_que: rcv_queue 接收队列长度
nsnd_que: snd_queue 发送队列长度
*/
IUINT32 nrcv_buf, nsnd_buf;
IUINT32 nrcv_que, nsnd_que;
/* nodelay: nodelay模式 (0:关闭 1:开启)
updated: 是否调用过 update 函数
*/
IUINT32 nodelay, updated;
/* ts_probe: 窗口探测标记位
probe_wait: 零窗口探测等待时间,默认 7000 (7秒)
*/
IUINT32 ts_probe, probe_wait;
/* dead_link: 死链接条件,默认为 20。
(单个数据段重传次数到达此值时 kcp->state 会被设置为 UINT_MAX)
incr: 拥塞窗口算法的一部分
*/
IUINT32 dead_link, incr;
/* 发送队列 */
struct IQUEUEHEAD snd_queue;
/* 接收队列 */
struct IQUEUEHEAD rcv_queue;
/* 发送缓冲区 */
struct IQUEUEHEAD snd_buf;
/* 接收缓冲区 */
struct IQUEUEHEAD rcv_buf;
/* 确认列表, 包含了序号和时间戳对(pair)的数组元素*/
IUINT32 *acklist;
/* 确认列表元素数量 */
IUINT32 ackcount;
/* 确认列表实际分配长度 */
IUINT32 ackblock;
/* 用户数据指针,传入到回调函数中 */
void *user;
/* 临时缓冲区 */
char *buffer;
/* 是否启用快速重传,0:不开启,1:开启 */
int fastresend;
/* 快速重传最大次数限制,默认为 5*/
int fastlimit;
/* nocwnd: 控流模式,0关闭,1不关闭
stream: 流模式, 0包模式 1流模式
*/
int nocwnd, stream;
/* 日志标记 */
int logmask;
/* 发送回调 */
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);
/* 日志回调 */
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user);
};
typedef struct IKCPCB ikcpcb;
KCP にはキュー構造が 2 つだけあります。
IQUEUEHEAD は、キューの開始 (前) 要素と最後の (次) 要素を指す単純な二重リンク リストです。
struct IQUEUEHEAD {
/*
next:
作为队列时: 队列的首元素 (head)
作为元素时: 当前元素所在队列的下一个节点
prev:
作为队列时: 队列的末元素 (last)
作为元素时: 当前元素所在队列的前一个节点
*/
struct IQUEUEHEAD *next, *prev;
};
typedef struct IQUEUEHEAD iqueue_head;
キューが空の場合、next/prev は NULL ではなくキュー自体を指します。
キュー要素としての IKCPSEG 構造ヘッダーも、IQUEUEHEAD 構造を再利用します。
struct IKCPSEG
{
struct IQUEUEHEAD node;
/* ... */
}
キュー要素として使用される場合、現在の要素が配置されているキュー内の前 (prev) 要素と次の要素 (next) が記録されます。
prev がキューを指す場合、現在の要素がキューの先頭にあることを意味し、next がキューを指す場合、現在の要素がキューの最後にあることを意味します。
すべてのキュー操作はマクロとして提供されます。
KCP が提供する構成方法は次のとおりです。
作業モードのオプション:
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
最大ウィンドウのオプション:
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
送信ウィンドウ サイズsndwnd
0 より大きく、受信ウィンドウ サイズrcvwnd
128 より大きくなければなりません。単位はバイトではなくパケットです。
最大伝送単位:
KCP は MTU の検出を行いません。デフォルト値は 1400 バイトです。この値は ikcp_setmtu を使用して設定できます。この値は、データ パケットを結合およびフラグメント化するときの最大送信単位に影響します。 MTU が小さいと、ルーティングの優先順位に影響します。
この記事では、基本的に KCP を実行できるコード kcp_basic.c を提供します。100 行未満のサンプル コードは、KCP への純粋なアルゴリズム呼び出しであり、ネットワーク スケジューリングは含まれません。 (重要: 記事に従ってデバッグし、試してみてください!)
これを使用すると、IKCPCB 構造体の基本構造フィールドを予備的に理解できます。
kcp.snd_queue
: 送信キュー ( kcp.nsnd_que
レコード長)kcp.snd_buf
: 送信バッファ ( kcp.nsnd_buf
レコード長)kcp.rcv_queue
: 受信キュー ( kcp.nrcv_que
レコード長)kcp.rcv_buf
: 受信バッファ ( kcp.nrcv_buf
レコード長)kcp.mtu
: 最大伝送単位kcp.mss
: 最大メッセージ長そして構造 IKCPSEG では次のようになります。
seg.sn
: シリアル番号seg.frg
: セグメント番号ikcp_create 関数を使用して、KCP コンテキスト構造体 IKCPCB を作成します。
IKCPCB は、外部から ikcp_send (送信者へのユーザー入力) および ikcp_input 入力データ (受信者への送信者入力) を呼び出すことによって、データとステータスを保存するための対応する IKCPSEG 構造体を内部的に作成します。
さらに、IKCPSEG 構造は、ikcp_recv (受信側からユーザーによって削除される) および ikcp_input 確認データ (受信側から送信側によって受信される) によって削除されます。
データ フローの方向の詳細については、 「キューとウィンドウ」セクションを参照してください。
IKCPSEG の作成と破棄は主に上記の 4 つの状況で発生しますが、その他の状況は内部キュー間の移動やその他の最適化でよく発生します。
以下の記事では、表示されるすべての IKCPCB および IKCPSEG 構造フィールドが
标记
によって強調表示されます (マークダウン ブラウズのみ。他の人には表示されない場合があります)。 IKCPCB 構造体のすべてのフィールドにはkcp.
という接頭辞が付けられ、IKCPSEG 構造体のすべてのフィールドにはseg.
という接頭辞が付きます。通常、ソース コード内の対応する変数名または関数パラメータ名もkcp
またはseg
です。
このコードは、指定された長さのデータを k1 という名前の KCP オブジェクトに書き込み、k2 オブジェクトからデータを読み取るだけです。 KCP はデフォルト モードで構成されます。
基本的なプロセスは、疑似コードを介して次のように簡単に説明できます。
/* 创建两个 KCP 对象 */
k1 = ikcp_create()
k2 = ikcp_create()
/* 向发送端 k1 写入数据 */
ikcp_send(k1, send_data)
/* 刷出数据,执行 kcp->output 回调 */
ikcp_flush(k1)
/* output 回调接收到带协议包头的分片数据,执行发送 */
k1->output(fragment_data)
/* 接收端 k2 收到输入数据 */
ikcp_input(k2, input_data)
/* 接收端刷出数据,会发送确认包到 k1 */
ikcp_flush(k2)
/* 发送端 k1 收到确认数据 */
recv_data = ikcp_recv(k1, ack_data)
/* 尝试读出数据 */
recv = ikcp_recv(k2)
/* 验证接收数据和发送数据一致 */
assert(recv_data == send_data)
サンプル コードでは、作成された KCP オブジェクトはkcp.output
この関数は、KCP オブジェクトの出力データの動作を定義するために使用されます。 kcp.writelog
、印刷のデバッグのために kcp_user_writelog 関数にもバインドされています。
さらに、 kcp.output
コールバックは他の ikcp_input を再帰的に呼び出すことができないため (最終的には独自のkcp.output
に再帰するため)、すべての出力データは中間の場所に格納され、 kcp.output
関数。これがサンプルコードで定義されている OUTPUT_CONTEXT 構造体の目的です。
サンプル コードを実行してみると、次の出力が得られます (# 記号が追加された内容は説明です)。
# k1.output 被调用,输出 1400 字节
k1 [RO] 1400 bytes
# k2 被调用 ikcp_input 输入数据
k2 [RI] 1400 bytes
# psh 数据推送分支处理
k2 input psh: sn=0 ts=0
# k2.output 被调用,输出确认包,数据长度24字节
k2 [RO] 24 bytes
# k1 被调用 ikcp_input 输入数据
k1 [RI] 24 bytes
# 序号 sn=0 被确认
k1 input ack: sn=0 rtt=0 rto=100
k1 [RO] 1400 bytes
k1 [RO] 1368 bytes
k2 [RI] 1400 bytes
k2 input psh: sn=1 ts=0
k2 [RI] 1368 bytes
k2 input psh: sn=2 ts=0
k2 [RO] 48 bytes
k1 [RI] 48 bytes
k1 input ack: sn=1 rtt=0 rto=100
k1 input ack: sn=2 rtt=0 rto=100
# k2 被调用 kcp_recv 取出数据
k2 recv sn=0
k2 recv sn=1
k2 recv sn=2
出力内容は、KCP コードに組み込まれて出力されるデバッグ情報であり、区別として k1/k2 行プレフィックスが kcp_user_writelog によって追加されます。
このコードの送信確認プロセスの完全な概略図は次のように説明されます (サイズは 2 倍)。
k1 で ikcp_send を呼び出します: (図ステップ 1-1)
長さ 4096 のデータが送信者に書き込まれます。 kcp.mss
長さ 1376/1376/1344 の 3 つのパケットに分割されており、各パケットのseg.frg
フラグメンテーション マークはそれぞれ 2/1/0 です。
kcp.mtu
最大送信単位は、 ikcp.output
コールバックによって毎回受信される最大データ長を定義します。デフォルトは 1400 です。
回路図では、ikcp_output メソッドは最終的に
ikcp.output
関数ポインターを呼び出します。 (ikcp.c:212)
kcp.mss
の最大メッセージ長は、 kcp.mtu
からプロトコル オーバーヘッド (24 バイト) を減算して計算されます。デフォルトは 1376 です。
現時点ではkcp.output
コールバックは実行されず、すべてのシャード データが IKCPSEG 構造に割り当てられて記録され、 kcp.snd_queue
キュー (ikcp.c:528) に追加されます。
このとき、k1のkcp.snd_queue
キュー長は3、 kcp.snd_buf
キュー長は0です。
k1 で ikcp_flush を呼び出します: (図のステップ 1-2)
ここでは、ウィンドウの特定の計算プロセスは無視されます。k1 が初めて ikcp_flush を呼び出したときに、輻輳ウィンドウ
kcp.cwnd
の値が 1 であることだけを知っておく必要があります。
輻輳ウィンドウの制限により、最初に送信できるパケットは 1 つだけです。 kcp.snd_queue
キューの最初のデータ長を持つ IKCPSEG オブジェクトがkcp.snd_buf
キュー (ikcp.c:1028) に移動され、 kcp.snd_nxt
に従って割り当てられたシーケンス番号seg.sn
の値が 0 (ikcp .c:1036)、 seg.cmd
フィールドは IKCP_CMD_PUSH、データプッシュパッケージを表します。
このとき、k1のkcp.snd_queue
キュー長は2、 kcp.snd_buf
キュー長は1となります。
ステップ 1-3 では、最初に送信されたデータに対して ikcp_output 呼び出し (ikcp.c:1113) を実行して、データ パケット[PSH sn=0 frg=2 len=1376]
を送信します。
データ コマンド タイプは 4 つだけです: IKCP_CMD_PUSH (データ プッシュ) IKCP_CMD_ACK (確認) IKCP_CMD_WASK (ウィンドウ検出) IKCP_CMD_WINS (ウィンドウ応答)、ikcp.c:29 で定義
k2 で ikcp_input を呼び出します: (図ステップ 2-1)
データパケット[PSH sn=0 frg=2 len=1376]
を入力し、パケットヘッダを解析して有効性を確認します。 (ikcp.c:769)
データパケットの種類を解析し、データプッシュ分岐処理に入る。 (ikcp.c:822)
データ パケットのseq.sn
値とseq.ts
値を確認リストkcp.acklist
(ikcp.c:828) に記録します。注意してください: この例のseq.ts
の値は常に 0 です。
受信したパケットをkcp.rcv_buf
キューに追加します。 (ikcp:709)
kcp.rcv_buf
キュー内の最初のデータ パケットが使用可能かどうかを確認し、使用可能なデータ パケットである場合は、 kcp.rcv_queue
キューに移動します。 (ikcp.c:726)
kcp.rcv_buf
で使用可能なデータ パケットは、受信が予想される次のデータ シーケンス番号 ( kcp.rcv_nxt
から取得され、次のデータ シーケンス番号はseg.sn
== 0 である必要があります) およびkcp.rcv_queue
キューが受信したウィンドウ サイズより小さいです。
このステップでは、 kcp.rcv_buf
キュー内の唯一のデータ パケットがkcp.rcv_queue
キューに直接移動されます。
このとき、k2のkcp.>rcv_queue
キュー長は1、 kcp.snd_buf
キュー長は0となります。次の受信データ通番kcp.rcv_nxt
の値が 0 から 1 に更新されます。
k2 で ikcp_flush を呼び出します: (図 ステップ 2-2)
k2 の最初の ikcp_flush 呼び出し内。確認リストkcp.acklist
にデータがあるため、確認パケットはエンコードされて送信されます (ikcp.c:958)。
確認パケットのseg.una
値にはkcp.rcv_nxt
=1 が割り当てられます。
このパケットは[ACK sn=0 una=1]
と記録されます。これは、ack 確認においてパケットシーケンス番号 0 が確認されたことを意味します。 una 確認では、パケット番号 1 より前のすべてのパケットが確認されます。
ステップ 2-3 では、 kcp.output
が呼び出され、データ パケットが送信されます。
k2 で ikcp_recv を呼び出します: (図ステップ 2-4)
kcp.rcv_queue
キューseg.frp
値が 0 のパケット (ikcp.c:459) が含まれているかどうかを確認し、このパケットが含まれている場合は、 seg.frp
最初のパケットとその前のパケットのデータを記録します。このパケットの合計長が戻り値として返されます。そうでない場合、この関数は失敗値 -1 を返します。
現時点では、 kcp.rcv_queue
はパッケージ[PSH sn=0 frg=2 len=1376]
しかないため、読み取りの試行は失敗しました。
ストリーム モード (kcp.stream != 0) の場合、すべてのパケットは
seg.frg=0
としてマークされます。この時点で、kcp.rcv_queue
キュー内のパケットはすべて正常に読み取られます。
k1 で ikcp_input: を呼び出します (図のステップ 3-1)
入力パケット[ACK sn=0 una=1]
。
UNAは次のように認めています:
受信したパッケージはすべて、最初に UNA 確認を試みます (ikcp.c:789)
パケットのseg.una
値を確認することで、 kcp.snd_buf
キュー内のseg.sn
値が una 値 (ikcp:599) より小さいすべてのパケットを確認して削除しました。
[PSH sn=0 frg=2 len=1376]
が確認され、k1 のkcp.snd_buf
キューから削除されます。
ACK確認:
データパケットの種類を解析し、確認分岐処理に入る。 (ikcp.c:792)
確認パケットのシーケンス番号を照合し、対応するパケットを削除します。 (ikcp.c:581)
ステップ 3-1 で ACK 確認を実行すると、パケット[PSH sn=0 frg=2 len=1376]
のみが事前に UNA によって確認されているため、 kcp.snd_buf
キューはすでに空になっています。
kcp.snd_buf
キュー ヘッダー データが確認されると ( kcp.snd_una
が変更されます)、輻輳ウィンドウ サイズの cwnd 値が再計算され、2 (ikcp.c:876) に更新されます。
UNA / ACK 確認図。この図は、プロセス図内のマークされていないkcp.snd_una
のステータスをさらに記録します。
ACK 確認応答は、連続して到着する確認応答パケットに対しては機能しません。順序どおりに到着しないパケットの場合、パケットは ACK による確認後に個別に削除されます。
k1 で ikcp_flush を呼び出します: (図ステップ 3-2)
ステップ 1-2 と同様に、新しい輻輳ウィンドウkcp.cwnd
の値が 2 に更新され、今度は残りの 2 つのパケットが送信されます。 [PSH sn=1 frg=1 len=1376]
[PSH sn=2 frg=0 len=1344]
。
ステップ 3-3 では、 kcp.output
実際に 2 回呼び出され、それぞれデータ パケットが送信されます。
k2 で ikcp_input: を呼び出します (図のステップ 4-1)
入力パケット[PSH sn=1 frg=1 len=1376]
および[PSH sn=2 frg=0 len=1344]
。
各パケットはkcp.rcv_buf
キューに追加され、利用可能になり、最終的にはすべてkcp.rcv_queue
キューに移動されます。
このとき、k2のkcp.rcv_queue
キュー長は3、 kcp.snd_buf
長は0です。次に受信が予想されるパケットのkcp.rcv_nxt
の値が 1 から 3 に更新されます。
k2 で ikcp_flush を呼び出します: (図ステップ 4-2)
kcp.acklist
内の確認応答情報は、パケット[ACK sn=1 una=3]
および[ACK sn=2 una=3]
にエンコードされ、ステップ 4-3 で送信されます。
実際、これら 2 つのパケットはバッファに書き込まれ、 kcp.output
呼び出しが行われます。
k2 で ikcp_recv を呼び出します: (図ステップ 4-4)
現在、 kcp.rcv_queue
には 3 つの未読パケットがあります: [PSH sn=0 frg=2 len=1376]
[PSH sn=1 frg=1 len=1376]
および[PSH sn=2 frg=0 len=1344]
このとき、 seg.frg
値が 0 のパケットが読み取られ、読み取れる合計長は 4096 と計算されます。その後、3 つのパケット内のすべてのデータが読み取られ、読み取りバッファーに書き込まれ、成功が返されます。
別の状況に注意する必要がありますkcp.rcv_queue
キューにseg.frg
値が 2/1/0/2/1/0 のユーザー送信パケットが 2 つ含まれており、6 つのデータ パケットに断片化されている場合、対応するデータ パケットは受信した完全なデータをすべて読み出すために、ikcp_recv を 2 回呼び出す必要もあります。
k1 で ikcp_input: を呼び出します (図のステップ 5-1)
確認応答パケット[ACK sn=1 una=3]
および[ACK sn=2 una=3]
を入力し、 seg.una
=3 に解析します。パッケージ[PSH sn=1 frg=1 len=1376]
[PSH sn=2 frg=0 len=1344]
が確認され、una を通じてkcp.snd_buf
キューから削除されます。
送信されたすべてのデータは確認されました。
ウィンドウはフロー制御に使用されます。これはキューの論理範囲をマークします。実際のデータの処理により、キューの位置はシーケンス番号の上位に移動し続けます。論理的には、このウィンドウは移動し続け、同時に拡大および縮小するため、スライディング ウィンドウとも呼ばれます。
この概略図は、「基本的なデータ送受信プロセス」セクションのフロー図のステップ 3-1 から 4-1 を別の図で表したものです。ステップ範囲外の操作として、データの方向を半透明の矢印で示します。
すべてのデータは矢印で示された関数によって処理され、新しい場所 (画像の 2 倍のサイズ) に移動されます。
送信側のikcp_send関数で渡されたデータは、データスライス処理後、そのままkcp.snd_queue
送信キューに格納されます。
ikcp_flushが呼び出されるたびに。この送信のウィンドウ サイズは、送信ウィンドウ サイズkcp.snd_wnd
、リモート ウィンドウ サイズkcp.rmt_wnd
、および輻輳ウィンドウ サイズkcp.cwnd
値は、min( kcp.snd_wnd
, kcp.rmt_wnd
、 kcp.cwd
) (ikcp.c:1017)。
ikcp_nolay関数によって nc パラメータが 1 に設定され、フロー制御モードがオフになっている場合、輻輳ウィンドウ値の計算は無視されます。送信ウィンドウの計算結果は min( kcp.snd_wnd
, kcp.rmt_wnd
) (ikcp.c:1018) となります。
フロー制御モードのみをオフにするデフォルト設定では、初めて送信できるデータ パケットの数はkcp.snd_wnd
のデフォルト サイズ値の 32 です。これは、フロー制御がデフォルトで有効になっているため、最初に 1 つのパケットしか送信できない基本的な送受信プロセスの例とは異なります。
新しく送信されたデータ パケットはkcp.snd_buf
キューに移動されます。
ikcp_send データの場合、スライス制限は 127 のみです (つまり、127*
kcp.mss
= 174752 バイト)。送信キュー内のパケットの総数に制限はありません。参照: キャッシュ蓄積遅延を回避する方法
kcp.snd_buf
送信バッファには、送信されるデータ、または送信されたデータが格納されます。
ikcp_flush
が呼び出されるたびに、送信ウィンドウが計算され、データ パケットがkcp.snd_queue
から現在のキューに移動されます。現在のキュー内のすべてのデータ パケットは、次の 3 つの状況で処理されます。
1. 最初のデータ送信 (ikcp.c:1053)
パケットの送信回数はseg.xmit
に記録されます。初回送信の処理は比較的単純で、再送タイムアウトの一部のパラメータseg.rto
/ seg.resendts
が初期化されます。
2. データタイムアウト (ikcp.c:1058)
内部に記録された時間kcp.current
パケット自体のタイムアウト期間seg.resendts
を超えると、タイムアウト再送が発生します。
3. データ交差の確認 (ikcp.c:1072)
データエンドがスパンされており、スパン数seg.fastack
スパン再送信設定kcp.fastresend
を超えると、スパン再送信が発生します。 ( kcp.fastresend
デフォルトは 0 で、0 の場合は UINT32_MAX として計算され、スパン再送信は行われません。) タイムアウト再送信後、現在のパケットseg.fastack
0 にリセットされます。
確認応答リストは、元々はパケットを受信した順序でシーケンス番号とタイムスタンプ ( seg.sn
/ seg.ts
) を記録する単純な記録リストです。
したがって、この記事の概略図では、
kcp.ack_list
には空白要素の位置は描画されません。これは論理的に順序付けされたキューではないためです (同様に、snd_queue
キュー内のパケットにはまだシーケンス番号が割り当てられていませんが、論理シーケンス番号は決定されています)。
受信側は、一時的に処理できないデータ パケットをバッファリングします。
ikcp_input から受信するすべてのデータ パケットは最初にこのキューに到着し、情報は元の到着順でkcp.ack_list
に記録されます。
このキューにデータが残る状況は 2 つだけです。
ここでは、パケット[PSH sn=0]
が最初に受信され、利用可能なパケットの条件を満たし、 kcp.rev_queue
に移動します。
次に、パケット[PSH sn=2]
が受信されましたが、これは受信が期待されていた次のパケット ( seg.sn
== kcp.rcv_nxt
) ではなかったため、このパケットはkcp.rcv_buf
に留まりました。
パケット[PSH sn=1]
を受信した後、スタックしている 2 つのパケット[sn=1]
[sn=2]
をkcp.rcv_queue
に移動します。
kcp.rcv_queue
受信キューの長さが受信ウィンドウ サイズkcp.rcv_wnd
に達しました (ikcp_recv が時間内に呼び出されませんでした)。受信側は、上位層が読み取ることができるデータを保存します。
ストリーミング モードでは、利用可能なすべてのパケットが読み取られ、非ストリーミング モードでは、断片化されたデータ セグメントが読み取られて、完全な生データに組み立てられます。
読み取りが完了すると、 kcp.rcv_buf
からこのキューへのデータの移動が試行されます (おそらく、受信ウィンドウが完全な状態から回復するため)。
送信ウィンドウのkcp.snd_wnd
値は構成された値であり、デフォルトは 32 です。
リモート ウィンドウkcp.rmt_wnd
は、送信者が受信者からパケット (単なる確認パケットではなく) を受信したときに更新される値です。現在のデータ パケットが受信側によって送信されるときに、受信側の受信キューkcp.rcv_queue
の使用可能な長さ (ikcp.c:1086) が記録されます。初期値は 128 です。
輻輳ウィンドウは、ikcp_input 経由でデータが受信されるたびにアルゴリズム的に増加する計算値です。
データ ikcp_flush をフラッシュするときにパケット損失と高速再送信が検出された場合、アルゴリズムに従って再計算されます。
kcp.cwnd
が 1 に初期化される位置は、ikcp_flush への最初の ikcp_update 呼び出し内です。
受信ウィンドウのkcp.rcv_wnd
値は構成された値であり、デフォルトは 128 です。これは、受信キューkcp.rcv_queue
の最大長を制限します。
このセクションでは、「基本的なデータ送受信」セクションのサンプル コードに基づいた kcp_optional.c の改良版が提供されています。マクロ定義を変更することで、プロトコルの動作をさらに観察できます。
サンプルコードでは、指定した数の固定長データを k1 に書き込み、k2 にすべて読み込むことを指定して処理を終了します。
指定された機能を制御するためにマクロが提供されています。
輻輳ウィンドウはkcp.cwnd
およびkcp.incr
の値を通じて記録されます。 kcp.cwnd
で記録される単位はパケットであるため、バイト長単位で表現される輻輳ウィンドウを記録するには追加のkcp.incr
が必要です。
TCP と同様に、KCP 輻輳制御も、スロー スタートと輻輳回避の 2 つの段階に分かれています。
輻輳ウィンドウは、 kcp.snd_buf
キュー ヘッダー データが確認されるたびに、データ パケットを確認する過程で増加します (有効な UNA 確認、 kcp.snd_una
が変化します)。また、輻輳ウィンドウが記録されたリモート ウィンドウkcp.rmt_wnd
より小さい場合、輻輳ウィンドウは増加します。 (ikcp:875)
1. 輻輳ウィンドウがスロー スタートしきい値kcp.ssthresh
より小さい場合、スロー スタート段階にあり、この時点で輻輳ウィンドウの増加は比較的積極的です。輻輳ウィンドウは 1 単位ずつ増加します。
2. 輻輳ウィンドウがスロー スタートしきい値以上の場合、輻輳回避段階にあり、輻輳ウィンドウの増加は比較的控えめです。 kcp.incr
毎回 mss/16 を増加させる場合、ユニット輻輳ウィンドウが増加する前に 16 回の有効な UNA 確認が必要になります。実際の輻輳回避フェーズのウィンドウの拡大は次のとおりです。
(mss * mss) / incr + (mss / 16)
incr=cwnd*mss は次のようになります。
((mss * mss) / (cwnd * mss)) + (mss / 16)
以下と同等:
(mss / cwnd) + (mss / 16)
輻輳ウィンドウは、cwnd ごと、および 16 個の有効な UNA 確認応答ごとに段階的に増加します。
輻輳ウィンドウの削減: ikcp_flush 関数が再送信またはタイムアウトにわたるパケット損失を検出すると、輻輳ウィンドウが削減されます。
1. スパン再送信が発生すると、スロー スタートしきい値kcp.ssthresh
未確認のシーケンス番号スパンの半分に設定されます。輻輳ウィンドウ サイズは、スロー スタートしきい値に高速再送信設定値kcp.resend
(ikcp:1117) を加えたものです。
ssthresh = (snd_nxt - snd_una) / 2
cwnd = ssthresh + resend
2. パケット損失タイムアウトが検出されると、スロー スタートしきい値が現在の輻輳ウィンドウの半分に設定されます。輻輳ウィンドウを 1 に設定 (ikcp:1126):
ssthresh = cwnd / 2
cwnd = 1
スロー スタート 1 を観察する: デフォルトの構成でサンプル コードを実行すると、スロー スタート プロセスがすぐにスキップされることがわかります。これは、デフォルトのスロー スタートしきい値が 2 であるためです。
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
# 收到一个确认包且拥塞窗口小于慢启动阈值,incr 增加一个 mss
t=100 n=1 una=1 nxt=2 cwnd=2|2 ssthresh=2 incr=2752
# 开始拥塞避免
t=200 n=1 una=2 nxt=3 cwnd=2|2 ssthresh=2 incr=3526
t=300 n=1 una=3 nxt=4 cwnd=4|4 ssthresh=2 incr=4148
t=400 n=1 una=4 nxt=5 cwnd=4|4 ssthresh=2 incr=4690
...
出力内容の t は論理時間、n はサイクル内で k1 がデータを送信する回数、cwnd=1|1 の値は、縦棒記号の前の 1 が ikcp_flush のときに計算されるウィンドウであることを示します。は、min(kcp. フロー制御モードでは.snd_wnd, kcp.rmt_wnd, kcp.cwnd)、次の 1 の値がkcp.cwnd
です。
デフォルト設定での輻輳ウィンドウの増加をグラフで観察します。輻輳回避フェーズでは、輻輳ウィンドウが大きくなるほど、輻輳ウィンドウの増加はスムーズになります。
スロー スタート 2 を観察します。サンプル構成のスロー スタートしきい値の初期値KCP_THRESH_INITを 16 に調整します。
#define KCP_THRESH_INIT 16
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=16 incr=1376
t=100 n=1 una=1 nxt=2 cwnd=2|2 ssthresh=16 incr=2752
t=200 n=1 una=2 nxt=3 cwnd=3|3 ssthresh=16 incr=4128
t=300 n=1 una=3 nxt=4 cwnd=4|4 ssthresh=16 incr=5504
...
t=1300 n=1 una=13 nxt=14 cwnd=14|14 ssthresh=16 incr=19264
t=1400 n=1 una=14 nxt=15 cwnd=15|15 ssthresh=16 incr=20640
t=1500 n=1 una=15 nxt=16 cwnd=16|16 ssthresh=16 incr=22016
# 开始拥塞避免
t=1600 n=1 una=16 nxt=17 cwnd=16|16 ssthresh=16 incr=22188
t=1700 n=1 una=17 nxt=18 cwnd=16|16 ssthresh=16 incr=22359
...
送信ラウンド前の短い期間をインターセプトするだけで、スロー スタートもデフォルトで直線的に増加することがわかります。
遅延確認応答をオフにすることを観察します。できるだけ多くのデータを送信し、遅延送信オプションACK_DELAY_FLUSH をオフにして、パケット損失をシミュレートします。
#define KCP_WND 256, 256
#define KCP_THRESH_INIT 32
#define SEND_DATA_SIZE (KCP_MSS*64)
#define SEND_STEP 16
#define K1_DROP_SN 384
//#define ACK_DELAY_FLUSH
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=32 incr=1376
t=100 n=2 una=1 nxt=3 cwnd=2|2 ssthresh=32 incr=2752
t=200 n=4 una=3 nxt=7 cwnd=4|4 ssthresh=32 incr=5504
t=300 n=8 una=7 nxt=15 cwnd=8|8 ssthresh=32 incr=11008
t=400 n=16 una=15 nxt=31 cwnd=16|16 ssthresh=32 incr=22016
...
t=1100 n=52 una=269 nxt=321 cwnd=52|52 ssthresh=32 incr=72252
t=1200 n=56 una=321 nxt=377 cwnd=56|56 ssthresh=32 incr=78010
t=1300 n=62 una=377 nxt=439 cwnd=62|62 ssthresh=32 incr=84107
t=1400 n=7 una=384 nxt=446 cwnd=62|62 ssthresh=32 incr=84863
t=1500 n=1 una=384 nxt=446 cwnd=1|1 ssthresh=31 incr=1376
t=1600 n=2 una=446 nxt=448 cwnd=2|2 ssthresh=31 incr=2752
t=1700 n=4 una=448 nxt=452 cwnd=4|4 ssthresh=31 incr=5504
t=1800 n=8 una=452 nxt=460 cwnd=8|8 ssthresh=31 incr=11008
t=1900 n=16 una=460 nxt=476 cwnd=16|16 ssthresh=31 incr=22016
...
この場合、スロー スタートと輻輳回避の典型的なグラフが得られます。 15 回目の送信ラウンド (t=1500) でパケット損失が検出されました。
遅延送信オプションをオフにすると、受信データ パーティは ikcp_input の実行直後に ikcp_flush を呼び出して確認パケットを送り返すことになります。
このときのスロー スタート プロセスは、 RTT (Round-Trip Time、往復時間) ごとに指数関数的に増加します。これは、各確認パケットが個別に送信されるため、送信側の輻輳ウィンドウが増大し、輻輳ウィンドウが増大するためパケット数が増加するためです。各RTT内で送信されるパックは2倍になります。
確認が遅れた場合は確認書も同送させていただきます。輻輳ウィンドウを増やすプロセスは、ikcp_input 関数が呼び出されるたびに 1 回だけ実行されるため、受信した複数の確認パケットをマージしても輻輳ウィンドウを複数回増やす効果はありません。
クロック サイクル間隔が RTT より大きい場合、間隔ごとに指数関数的に増加します。次のような状況が考えられます。
指数関数的な増加の前提は、次回送信されるデータが前回のデータ パケット数の 2 倍を満たすことができるということです。送信側に書き込まれるデータが不十分な場合、指数関数的な増加は達成されません。
サンプル コードが即時確認を送信する場合でも、影響を受けるのは受信側での確認の送信方法のみであることに注意してください。送信者は、これらの確認パケットを処理する前に、次のサイクルを待つ必要もあります。したがって、ここでの時間 t は参考用です。受信したパケットがすぐに処理されて実際のネットワーク トランシーバー コードに格納されない限り、応答する前に更新サイクルまで待つ必要があります。
KCP の特性に基づいて、輻輳ウィンドウを大きくし、フロー制御を直接オフにしてより積極的な送信を実現する、より直接的な方法が必要です。
#define KCP_NODELAY 0, 100, 0, 1
#define SEND_DATA_SIZE (KCP_MSS*127)
#define SEND_STEP 1
t=0 n=32 una=0 nxt=32 cwnd=32|1 ssthresh=2 incr=1376
t=100 n=32 una=32 nxt=64 cwnd=32|2 ssthresh=2 incr=2752
t=200 n=32 una=64 nxt=96 cwnd=32|2 ssthresh=2 incr=3526
t=300 n=31 una=96 nxt=127 cwnd=32|4 ssthresh=2 incr=4148
必要に応じて、輻輳ウィンドウの値を直接変更することもできます。
リモート ウィンドウがいっぱいであることを確認します。送信されたデータの長さがデフォルトのリモート ウィンドウ サイズに近く、受信側がそれを時間内に読み出せない場合、データを送信できない期間が発生します (注意してください)。サンプル コードでは、受信側は最初に確認パケットを送信し、その後コンテンツを再度読み取ります)。
#define KCP_NODELAY 0, 100, 0, 1
#define SEND_DATA_SIZE (KCP_MSS*127)
#define SEND_STEP 2
t=0 n=32 una=0 nxt=32 cwnd=32|1 ssthresh=2 incr=1376
t=100 n=32 una=32 nxt=64 cwnd=32|2 ssthresh=2 incr=2752
t=200 n=32 una=64 nxt=96 cwnd=32|2 ssthresh=2 incr=3526
t=300 n=32 una=96 nxt=128 cwnd=32|4 ssthresh=2 incr=4148
t=400 n=0 una=128 nxt=128 cwnd=0|4 ssthresh=2 incr=4148
t=500 n=32 una=128 nxt=160 cwnd=32|4 ssthresh=2 incr=4148
t=600 n=32 una=160 nxt=192 cwnd=32|4 ssthresh=2 incr=4690
t=700 n=32 una=192 nxt=224 cwnd=32|4 ssthresh=2 incr=5179
t=800 n=30 una=224 nxt=254 cwnd=31|4 ssthresh=2 incr=5630
受信機に記録されたリモートウィンドウサイズkcp.rmt_wnd
が 0 の場合、ikcp_flush でプローブ待機フェーズ (プローブ待機、ikcp.c:973) が開始されます。 kcp.ts_probe
、最初に 7000 ミリ秒の時間を記録します ( kcp->probe_wait
に記録)。
時間になると、IKCP_CMD_WASK タイプのパケットをさらにエンコードして受信側 (ikcp.c:994) に送信し、 kcp.rmt_wnd
更新するために IKCP_CMD_WINS タイプのパケットを返送するようリモート エンドに要求します。
リモート ウィンドウ サイズが常に 0 の場合、 kcp->probe_wait
毎回現在の値の半分ずつ増加し、待機時間を更新します。最大待機時間は 120000 ミリ秒 (120 秒) です。
リモートウィンドウサイズが0以外の場合、上記の検出ステータスはクリアされます。
この例では、記録されたリモート ウィンドウ サイズを復元する前に、最初の 7 秒間待機しません。データを読み取る受信側の ikcp_recv の操作では、データを読み取る前にkcp.rcv_queue
キューの長さが受信ウィンドウkcp.rcv_wnd
以上である場合 (読み取りウィンドウがいっぱいである場合)、フラグ ビットが返されるためです。 (ikcp .c:431)、次回 ikcp_flush でタイプ IKCP_CMD_WINS (ikcp.c:1005) のパケットを送信します。最新のリモート ウィンドウ サイズを更新するように送信側に通知します。
この問題を回避するには、受信側でデータをタイムリーに読み出す必要がありますが、リモート ウィンドウが小さくなっても、送信側の送信ウィンドウは小さくなり、追加の遅延が発生します。同時に、受信側の受信ウィンドウを拡大することも必要である。
RECV_TIME値を比較的大きな値 (たとえば、300 秒) に変更して、IKCP_CMD_WASK パケットの送信を観察してみてください。
kcp.snd_buf
キューの説明で説明されているように、ikcp_flush を呼び出すと、パケットが初めて送信されない限り、キュー内のすべてのパケットが走査されます。次に、パケットが指定された回数だけ確認応答を受信したかどうか、またはタイムアウト期間に達したかどうかを確認します。
往復時間とタイムアウトの計算に関連するフィールドは次のとおりです。
kcp.rx_rttval
: スムーズなネットワーク ジッター時間kcp.rx_srtt
: スムーズな往復時間kcp.rx_rto
(受信再送信タイムアウト): 再送信タイムアウト、初期値 200kcp.rx_minrto
: 最小再送信タイムアウト、初期値 100kcp.xmit
: グローバル再送信回数seg.resendts
: 再送タイムスタンプseg.rto
: 再送信タイムアウトseg.xmit
: 再送回数パッケージがタイムアウトを計算する方法について説明する前に、まずラウンドトリップ時間とタイムアウトの関連フィールドがどのように計算されるかを見てみましょう。
往復時間記録: ACK 確認パケットが処理されるたびに、確認パケットにはシーケンス番号と、シーケンス番号が送信者に送信された時刻 (正当な場合はseg.sn
/ seg.ts
) が含まれます。時刻更新処理が実行されます。
rtt の値は 1 つのパケットの往復時間、つまり rtt= kcp.current
- seg.ts
です。
滑らかな往復時間kcp.rx_srtt
が0の場合、初期化が実行されることを意味します。KCP.RX_SRTTはRTTとして直接記録されkcp.rx_rttval
kcp.rx_srtt
RTTの半分として記録されます。
非独立化プロセスでは、このRTTの変動値と記録されたkcp.rx_srtt
(IKCP.C:550)を表すデルタ値が計算されます。
delta = abs(rtt - rx_srtt)
新しいkcp.rx_rttval
は、古いkcp.rx_rttval
とdeltaの加重値によって更新されます。
rx_rttval = (3 * rx_rttval + delta) / 4
新しいkcp.rx_srtt
、古いkcp.rx_srtt
とRTTの加重値によって更新され、1未満になりません。
rx_srtt = (7 * rx_srtt + rtt) / 8
rx_srtt = max(1, rx_srtt)
新しいrx_rto
、滑らかな往復時間kcp.rx_srtt
の最小値で更新され、クロックサイクルkcp.interval
およびrx_rttval
4倍に更新され、範囲は[ kcp.rx_minrto
、60000]に限定されています。
rto = rx_srtt + max(interval, 4 * rttval)
rx_rto = min(max(`kcp.rx_minrto`, rto), 60000)
理想的には、ネットワークに固定された遅延とジッターのみがある場合、 kcp.rx_rttval
の値は0に近づき、 kcp.rx_rto
の値はスムーズな往復時間とクロックサイクルによって決定されます。
スムーズな往復時間の計算図:
最初の契約配達時間(IKCP.C:1052):
seg.rto
kcp.rx_rto
ステータスを記録し、データパケットの最初のタイムアウト時間はseg.rto
+ rtominミリ秒です。
rtominは、nodelayモードが有効になっている場合、 kcp.rx_rto
によって計算されます。 rtominは0です。そうでなければkcp.rx_rto
/8です。
Nodelayのタイムアウトが有効になっていない:
resendts = current + rx_rto + rx_rto / 8
Nodelay Timeoutを有効にします:
resendts = current + rx_rto
タイムアウト再送信(IKCP.C:1058):
内部時間がデータパケットのタイムアウト時間seg.resendts
に到達すると、このシーケンス番号のパケットが再送信されます。
Nodelayモードが有効になっていない場合、 seg.rto
の増分はmax( seg.rto
、 kcp.rx_rto
)(二重成長)です。
rto += max(rto, rx_rto)
Nodelayが有効になり、Nodelayが1の場合、 seg.rto
毎回増やします(1.5倍の増加):
rto += rto / 2
Nodelayが有効になり、Nodelayが2の場合、 kcp.rx_rto
毎回半分増加します(1.5倍の増加):
rto += rx_rto / 2
新しいタイムアウトは、 seg.rto
ミリ秒後です。
resendts = current + rx_rto
時間を越えて再送信(IKCP.C:1072):
データパケットが指定された回数を越えた場合、交差の再送信がトリガーされます。
seg.rto
、時間の経過とともに再送信されたときに更新されず、次のタイムアウト再送信時間は直接再計算されます。
resendts = current + rto
デフォルトのタイムアウトを観察します
パケットを1つだけ送信し、4回ドロップし、タイムアウトと再送信時間を観察します。
デフォルトの構成では、 kcp.rx_rto
の初期値は200ミリ秒であり、最初のタイムアウト時間は225ミリ秒です。 。
#define SEND_STEP 1
#define K1_DROP_SN 0,0,0,0
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=300 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=700 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=1500 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=3100 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
パッケージ自体に基づいてRTOの1.5倍の増加を観察する
#define KCP_NODELAY 1, 100, 0, 0
#define SEND_STEP 1
#define K1_DROP_SN 0,0,0,0
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=200 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=500 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=1000 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=1700 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
RTOに基づいて1.5倍の成長を見てください
#define KCP_NODELAY 2, 100, 0, 0
#define SEND_STEP 1
#define K1_DROP_SN 0,0,0,0
t=0 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=200 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=500 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=900 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
...
t=1400 n=1 una=0 nxt=1 cwnd=1|1 ssthresh=2 incr=1376
スパンの再送信を観察します
マージして処理された確認パケットは、パケット[sn=0]
とパケット[sn=1]
[sn=2]
の確認をトリガーしません。最終的には、タイムアウトを通じて再送信されました。
#define KCP_NODELAY 0, 100, 2, 1
#define SEND_DATA_SIZE (KCP_MSS*3)
#define SEND_STEP 1
#define K1_DROP_SN 0
t=0 n=3 una=0 nxt=3 cwnd=32|1 ssthresh=2 incr=1376
t=100 n=0 una=0 nxt=3 cwnd=32|1 ssthresh=2 incr=1376
t=200 n=0 una=0 nxt=3 cwnd=32|1 ssthresh=2 incr=1376
t=300 n=1 una=0 nxt=3 cwnd=32|1 ssthresh=16 incr=1376
2つのステップでデータパケットを送信すると、IKCP_INPUT確認の実行時に2番目のグループのパケットが2回交差し、 [sn=0]
次のIKCP_Flushで蓄積および再送信されます。
#define KCP_NODELAY 0, 100, 2, 1
#define SEND_DATA_SIZE (KCP_MSS*2)
#define SEND_STEP 2
#define K1_DROP_SN 0
t=0 n=2 una=0 nxt=2 cwnd=32|1 ssthresh=2 incr=1376
t=100 n=2 una=0 nxt=4 cwnd=32|1 ssthresh=2 incr=1376
t=200 n=1 una=0 nxt=4 cwnd=32|4 ssthresh=2 incr=5504
t=300 n=0 una=4 nxt=4 cwnd=32|4 ssthresh=2 incr=5934
この記事は、Creative Commons Attribution-NonCommercial-Noderivs 4.0国際ライセンスの下でライセンスされています。
プロジェクトのコードは、MITライセンスを使用したオープンソースです。
画像フォントについて:noto sans sc