In bestimmten Anwendungen kann die einfache Verwendung von TCP die Anforderungen nicht erfüllen. Die direkte Verwendung von UDP-Datagrammen kann die Datenzuverlässigkeit nicht garantieren und es ist häufig erforderlich, ein zuverlässiges Übertragungsprotokoll auf UDP-Basis auf der Anwendungsebene zu implementieren.
Eine direkte Nutzung des KCP-Protokolls ist eine Option, die ein robustes automatisches Neuübertragungsprotokoll implementiert und darüber hinaus eine kostenlose Parameteranpassung ermöglicht. Passen Sie sich durch Konfigurationsparameter und geeignete Aufrufmethoden an die Anforderungen verschiedener Szenarien an.
Einführung in KCP:
KCP ist ein schnelles und zuverlässiges Protokoll, das die durchschnittliche Verzögerung um 30–40 % und die maximale Verzögerung um das Dreifache reduzieren kann, allerdings auf Kosten von 10–20 % mehr Bandbreite als TCP. Die reine Algorithmusimplementierung ist nicht für das Senden und Empfangen zugrunde liegender Protokolle (z. B. UDP) verantwortlich. Benutzer müssen die Sendemethode für Datenpakete der unteren Ebene definieren und diese in Form eines Rückrufs an KCP bereitstellen. Sogar die Uhr muss extern übergeben werden und es erfolgt intern kein Systemaufruf. Das gesamte Protokoll verfügt nur über zwei Quelldateien, ikcp.h und ikcp.c, die problemlos in den eigenen Protokollstapel des Benutzers integriert werden können. Vielleicht haben Sie ein P2P- oder UDP-basiertes Protokoll implementiert, aber es fehlt Ihnen eine vollständige und zuverlässige ARQ-Protokollimplementierung. Dann kopieren Sie einfach diese beiden Dateien in das bestehende Projekt, schreiben Sie ein paar Codezeilen und Sie können es verwenden.
In diesem Artikel werden der grundlegende Sende- und Empfangsprozess, das Überlastungsfenster und der Timeout-Algorithmus des KCP-Protokolls kurz vorgestellt und außerdem Referenzbeispielcode bereitgestellt.
Die referenzierte KCP-Version ist zum Zeitpunkt des Schreibens die neueste Version. In diesem Artikel wird nicht der gesamte Quellcode von KCP vollständig eingefügt, sondern an wichtigen Stellen Links zu den entsprechenden Stellen des Quellcodes hinzugefügt.
Die IKCPSEG-Struktur wird zum Speichern des Status gesendeter und empfangener Datensegmente verwendet.
Beschreibung aller IKCPSEG-Felder:
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];
};
Das data
am Ende der Struktur wird zur Indizierung der Daten am Ende der Struktur verwendet. Der zusätzlich zugewiesene Speicher erweitert die tatsächliche Länge des Datenfeld-Arrays zur Laufzeit (ikcp.c:173).
Die IKCPSEG-Struktur dient nur dem Speicherstatus und nur einige Felder sind im Transportprotokoll codiert.
Die Funktion ikcp_encode_seg kodiert den Transportprotokoll-Header:
/* 协议头一共 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;
}
Die IKCPCB-Struktur speichert den gesamten Kontext des KCP-Protokolls, und die Protokollkommunikation erfolgt durch die Erstellung zweier IKCPCB-Objekte am anderen Ende.
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;
In KCP gibt es nur zwei Warteschlangenstrukturen:
IQUEUEHEAD ist eine einfache doppelt verknüpfte Liste, die auf das Startelement (vorheriges Element) und das letzte Element (nächstes Element) der Warteschlange zeigt:
struct IQUEUEHEAD {
/*
next:
作为队列时: 队列的首元素 (head)
作为元素时: 当前元素所在队列的下一个节点
prev:
作为队列时: 队列的末元素 (last)
作为元素时: 当前元素所在队列的前一个节点
*/
struct IQUEUEHEAD *next, *prev;
};
typedef struct IQUEUEHEAD iqueue_head;
Wenn die Warteschlange leer ist, zeigt next/prev auf die Warteschlange selbst und nicht auf NULL.
Der IKCPSEG-Strukturheader als Warteschlangenelement verwendet auch die IQUEUEHEAD-Struktur wieder:
struct IKCPSEG
{
struct IQUEUEHEAD node;
/* ... */
}
Bei Verwendung als Warteschlangenelement werden das vorherige (vorherige) Element und das nächste Element (nächste) in der Warteschlange, in der sich das aktuelle Element befindet, aufgezeichnet.
Wenn prev auf die Warteschlange zeigt, bedeutet dies, dass sich das aktuelle Element am Anfang der Warteschlange befindet. Wenn next auf die Warteschlange zeigt, bedeutet dies, dass sich das aktuelle Element am Ende der Warteschlange befindet.
Alle Warteschlangenoperationen werden als Makros bereitgestellt.
Die von KCP bereitgestellten Konfigurationsmethoden sind:
Arbeitsmodusoptionen :
int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
Maximale Fensteroptionen :
int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
Die Größe des Sendefensters sndwnd
muss größer als 0 sein und die Größe des Empfangsfensters rcvwnd
muss größer als 128 sein. Die Einheit sind Pakete, nicht Bytes.
Maximale Übertragungseinheit :
KCP ist nicht für die Erkennung von MTU verantwortlich. Der Standardwert beträgt 1400 Byte. Sie können diesen Wert mit ikcp_setmtu festlegen. Dieser Wert beeinflusst die maximale Übertragungseinheit beim Kombinieren und Fragmentieren von Datenpaketen. Eine kleinere MTU wirkt sich auf die Routing-Priorität aus.
Dieser Artikel stellt einen Code kcp_basic.c bereit, der KCP grundsätzlich ausführen kann. Der Beispielcode mit weniger als 100 Zeilen ist ein reiner Algorithmusaufruf an KCP und beinhaltet keine Netzwerkplanung. ( Wichtig : Folgen Sie dem Artikel, um das Problem zu beheben, und probieren Sie es aus!)
Sie können es verwenden, um sich ein erstes Verständnis der grundlegenden Strukturfelder in der IKCPCB-Struktur zu verschaffen:
kcp.snd_queue
: Sendewarteschlange ( kcp.nsnd_que
Datensatzlänge)kcp.snd_buf
: Sendepuffer ( kcp.nsnd_buf
Datensatzlänge)kcp.rcv_queue
: Empfangswarteschlange ( kcp.nrcv_que
Datensatzlänge)kcp.rcv_buf
: Empfangspuffer ( kcp.nrcv_buf
Datensatzlänge)kcp.mtu
: maximale Übertragungseinheitkcp.mss
: Maximale NachrichtenlängeUnd in der Struktur IKCPSEG:
seg.sn
: Seriennummerseg.frg
: SegmentnummerErstellen Sie die KCP-Kontextstruktur IKCPCB über die Funktion ikcp_create.
IKCPCB erstellt intern die entsprechende IKCPSEG-Struktur zum Speichern von Daten und Status, indem es extern ikcp_send (Benutzereingabe an den Absender) und ikcp_input-Eingabedaten (Absendereingabe an den Empfänger) aufruft.
Darüber hinaus wird die IKCPSEG-Struktur über ikcp_recv (vom Benutzer vom empfangenden Ende entfernt) und ikcp_input-Bestätigungsdaten (vom sendenden Ende vom empfangenden Ende empfangen) entfernt.
Eine detaillierte Datenflussrichtung finden Sie im Abschnitt „Warteschlange und Fenster“ .
Die Erstellung und Zerstörung von IKCPSEG erfolgt hauptsächlich in den oben genannten vier Situationen, andere treten häufig bei Bewegungen zwischen internen Warteschlangen und anderen Optimierungen auf.
In den folgenden Artikeln werden alle angezeigten IKCPCB- und IKCPSEG-Strukturfelder durch
标记
hervorgehoben (nur Markdown-Browsing, andere sehen es möglicherweise nicht). Allen Feldern der IKCPCB-Struktur wirdkcp.
vorangestellt, und allen Feldern der IKCPSEG-Struktur wirdseg.
vorangestellt. Normalerweise lautet der entsprechende Variablenname oder Funktionsparametername im Quellcode auchkcp
oderseg
.
Dieser Code schreibt einfach die angegebene Datenlänge in das KCP-Objekt namens k1 und liest die Daten aus dem k2-Objekt. KCP ist im Standardmodus konfiguriert.
Der grundlegende Prozess kann durch Pseudocode einfach wie folgt beschrieben werden:
/* 创建两个 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)
Im Beispielcode ist das erstellte KCP-Objekt zusätzlich zu kcp.output
die zum Definieren des Verhaltens der Ausgabedaten des KCP-Objekts verwendet wird. kcp.writelog
ist zum Debuggen des Druckens auch an die Funktion kcp_user_writelog gebunden.
Da kcp.output
-Rückruf außerdem keine anderen ikcp_input-Daten rekursiv aufrufen kann (da er schließlich auf seinen eigenen kcp.output
zurückgreift), müssen alle Ausgabedaten an einem Zwischenspeicherort gespeichert und dann nach dem Beenden von kcp.output
in k2 eingegeben werden Funktion. Dies ist der Zweck der im Beispielcode definierten OUTPUT_CONTEXT-Struktur.
Versuchen Sie, den Beispielcode auszuführen, und Sie erhalten die folgende Ausgabe (der mit dem #-Zeichen angehängte Inhalt ist eine Erklärung):
# 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
Der Ausgabeinhalt sind die in den KCP-Code integrierten Debugging-Informationen. Zur Unterscheidung wird zusätzlich das Zeilenpräfix k1/k2 über kcp_user_writelog angehängt.
Das vollständige schematische Diagramm des Sendebestätigungsprozesses dieses Codes wird wie folgt beschrieben (doppelt so groß):
Rufen Sie ikcp_send auf k1 auf: (Abbildung Schritt 1-1)
An den Absender werden Daten mit einer Länge von 4096 geschrieben. Laut kcp.mss
ist es in drei Pakete mit einer Länge von 1376/1376/1344 unterteilt, und seg.frg
Fragmentierungsmarkierungen jedes Pakets betragen jeweils 2/1/0.
Die maximale Übertragungseinheit kcp.mtu
definiert die maximale Datenlänge, die ikcp.output
Rückruf jedes Mal empfängt. Der Standardwert ist 1400.
Im schematischen Diagramm ruft die Methode ikcp_output schließlich den Funktionszeiger
ikcp.output
auf. (ikcp.c:212)
Die maximale Nachrichtenlänge von kcp.mss
wird durch Subtrahieren des Protokoll-Overheads (24 Bytes) von kcp.mtu
berechnet. Der Standardwert ist 1376.
Zu diesem Zeitpunkt wird kein kcp.output
Rückruf ausgeführt, und alle Shard-Daten werden zugewiesen und in der IKCPSEG-Struktur aufgezeichnet und an die kcp.snd_queue
Warteschlange (ikcp.c:528) angehängt.
Zu diesem Zeitpunkt beträgt die Warteschlangenlänge kcp.snd_queue
von k1 3 und die Warteschlangenlänge kcp.snd_buf
0.
Rufen Sie ikcp_flush auf k1 auf: (Abbildung Schritt 1-2)
Der spezifische Berechnungsprozess des Fensters wird hier ignoriert. Sie müssen nur wissen, dass der Wert des Überlastungsfensters
kcp.cwnd
1 ist, wenn k1 ikcp_flush zum ersten Mal aufruft.
Aufgrund der Überlastungsfensterbeschränkung kann beim ersten Mal nur ein Paket gesendet werden. Das IKCPSEG-Objekt mit der ersten Datenlänge der kcp.snd_queue
Warteschlange wird in die kcp.snd_buf
Warteschlange (ikcp.c:1028) verschoben, und der Wert der gemäß kcp.snd_nxt
zugewiesenen Sequenznummer seg.sn
ist 0 (ikcp .c:1036), das seg.cmd
Feld ist IKCP_CMD_PUSH, Stellt ein Daten-Push-Paket dar.
Zu diesem Zeitpunkt beträgt die Warteschlangenlänge kcp.snd_queue
von k1 2 und kcp.snd_buf
1.
Führen Sie in Schritt 1-3 den ikcp_output-Aufruf (ikcp.c:1113) für die ersten gesendeten Daten aus, um das Datenpaket [PSH sn=0 frg=2 len=1376]
zu versenden.
Es gibt nur vier Datenbefehlstypen: IKCP_CMD_PUSH (Daten-Push) IKCP_CMD_ACK (Bestätigung) IKCP_CMD_WASK (Fenstererkennung) IKCP_CMD_WINS (Fensterantwort), definiert in ikcp.c:29
Rufen Sie ikcp_input auf k2 auf: (Abbildung Schritt 2-1)
Geben Sie das Datenpaket ein [PSH sn=0 frg=2 len=1376]
, analysieren Sie den Paketheader und überprüfen Sie die Gültigkeit. (ikcp.c:769)
Analysieren Sie den Typ des Datenpakets und geben Sie die Daten-Push-Zweigverarbeitung ein. (ikcp.c:822)
Notieren Sie den seq.sn
Wert und seq.ts
-Wert des Datenpakets in der Bestätigungsliste kcp.acklist
(ikcp.c:828). Bitte beachten Sie : Der Wert von seq.ts
ist in diesem Beispiel immer 0.
Empfangene Pakete zur kcp.rcv_buf
Warteschlange hinzufügen. (ikcp:709)
Überprüfen Sie, ob das erste Datenpaket in kcp.rcv_buf
verfügbar ist. Wenn es sich um ein verfügbares Datenpaket handelt, wird es in kcp.rcv_queue
verschoben. (ikcp.c:726)
Die verfügbaren Datenpakete in kcp.rcv_buf
sind definiert als: die nächste erwartete Datensequenznummer (entnommen aus kcp.rcv_nxt
, wobei die nächste Datensequenznummer seg.sn
== 0 sein sollte) und die Länge des kcp.rcv_queue
Warteschlange ist kleiner als die empfangene Fenstergröße.
In diesem Schritt wird das einzige Datenpaket in der Warteschlange kcp.rcv_buf
direkt in die Warteschlange kcp.rcv_queue
verschoben.
Zu diesem Zeitpunkt beträgt kcp.>rcv_queue
von k2 1 und die Warteschlangenlänge kcp.snd_buf
0. Der Wert der nächsten empfangenen Datensequenznummer kcp.rcv_nxt
wird von 0 auf 1 aktualisiert.
Rufen Sie ikcp_flush auf k2 auf: (Abb. Schritt 2-2)
Im ersten ikcp_flush-Aufruf von k2. Da die Bestätigungsliste kcp.acklist
Daten enthält, wird das Bestätigungspaket codiert und gesendet (ikcp.c:958).
Dem seg.una
Wert im Bestätigungspaket wird kcp.rcv_nxt
=1 zugewiesen.
Dieses Paket wird als [ACK sn=0 una=1]
aufgezeichnet: Dies bedeutet, dass in der Ack-Bestätigung die Paketsequenznummer 0 bestätigt wird. Bei einer Bestätigung werden alle Pakete vor Paket Nummer 1 bestätigt.
In Schritt 2-3 wird kcp.output
aufgerufen, um das Datenpaket zu senden.
Rufen Sie ikcp_recv auf k2 auf: (Abbildung Schritt 2-4)
Überprüfen Sie, ob die kcp.rcv_queue
Warteschlange ein Paket mit seg.frp
Wert von 0 enthält (ikcp.c:459). Wenn sie dieses Paket enthält, zeichnen Sie das erste Paket seg.frp
== 0 und die Daten des vorherigen Pakets auf Als Rückgabewert wird die Gesamtlänge dieses Pakets zurückgegeben. Wenn nicht, gibt diese Funktion einen Fehlerwert von -1 zurück.
Da kcp.rcv_queue
derzeit nur über das Paket [PSH sn=0 frg=2 len=1376]
verfügt, ist der Leseversuch fehlgeschlagen.
Wenn es sich im Stream-Modus befindet (kcp.stream != 0), werden alle Pakete als
seg.frg=0
markiert. Zu diesem Zeitpunkt werden alle Pakete in der Warteschlangekcp.rcv_queue
erfolgreich gelesen.
Rufen Sie ikcp_input: auf k1 auf (Abbildung Schritt 3-1)
Eingabepaket [ACK sn=0 una=1]
.
UNA bestätigt :
Für jedes empfangene Paket wird zunächst eine UNA-Bestätigung versucht (ikcp.c:789).
Alle Pakete in der kcp.snd_buf
-Warteschlange, seg.sn
-Wert kleiner als der una-Wert (ikcp:599) ist, wurden bestätigt und entfernt, indem der seg.una
-Wert des Pakets bestätigt wurde.
[PSH sn=0 frg=2 len=1376]
wird bestätigt und aus der kcp.snd_buf
Warteschlange von k1 entfernt.
ACK-Bestätigung :
Analysieren Sie den Typ des Datenpakets und geben Sie die Bestätigungszweigverarbeitung ein. (ikcp.c:792)
Passen Sie die Sequenznummern der Bestätigungspakete an und entfernen Sie die entsprechenden Pakete. (ikcp.c:581)
Bei der Durchführung der ACK-Bestätigung in Schritt 3-1 ist die Warteschlange kcp.snd_buf
bereits leer, da das einzige Paket [PSH sn=0 frg=2 len=1376]
vorab von UNA bestätigt wurde.
Wenn kcp.snd_buf
Warteschlangenkopfdaten bestätigt werden ( kcp.snd_una
ändert sich), wird der cwnd-Wert der Überlastungsfenstergröße neu berechnet und auf 2 aktualisiert (ikcp.c:876).
UNA/ACK-Bestätigungsdiagramm. Dieses Diagramm zeichnet zusätzlich den Status von nicht markiertem kcp.snd_una
im Prozessdiagramm auf:
Die ACK-Bestätigung funktioniert nicht für nacheinander eingehende Bestätigungspakete. Bei Paketen, die nicht in der richtigen Reihenfolge ankommen, wird das Paket nach Bestätigung per ACK einzeln entfernt:
Rufen Sie ikcp_flush auf k1 auf: (Abbildung Schritt 3-2)
Genau wie in Schritt 1-2 wurde der Wert des neuen Überlastungsfensters kcp.cwnd
auf 2 aktualisiert und die verbleibenden zwei Pakete werden dieses Mal gesendet: [PSH sn=1 frg=1 len=1376]
[PSH sn=2 frg=0 len=1344]
.
In Schritt 3-3 wird kcp.output
tatsächlich zweimal aufgerufen, um jeweils Datenpakete zu versenden.
Rufen Sie ikcp_input: auf k2 auf (Abbildung Schritt 4-1)
Eingabepakete [PSH sn=1 frg=1 len=1376]
und [PSH sn=2 frg=0 len=1344]
.
Jedes Paket wird zur Warteschlange kcp.rcv_buf
hinzugefügt, ist verfügbar und schließlich wird alles in die Warteschlange kcp.rcv_queue
verschoben.
Zu diesem Zeitpunkt beträgt die Warteschlangenlänge kcp.rcv_queue
von k2 3 und kcp.snd_buf
0. Der Wert von kcp.rcv_nxt
für das nächste Paket, dessen Empfang erwartet wird, wird von 1 auf 3 aktualisiert.
Rufen Sie ikcp_flush auf k2 auf: (Abbildung Schritt 4-2)
Die Bestätigungsinformationen in kcp.acklist
werden in Pakete [ACK sn=1 una=3]
und [ACK sn=2 una=3]
codiert und in Schritt 4-3 gesendet.
Tatsächlich werden diese beiden Pakete in einen Puffer geschrieben und ein kcp.output
Aufruf durchgeführt.
Rufen Sie ikcp_recv auf k2 auf: (Abbildung Schritt 4-4)
Es gibt jetzt drei ungelesene Pakete in kcp.rcv_queue
: [PSH sn=0 frg=2 len=1376]
[PSH sn=1 frg=1 len=1376]
und [PSH sn=2 frg=0 len=1344]
Zu diesem Zeitpunkt wird ein Paket mit seg.frg
Wert von 0 gelesen und die gesamte lesbare Länge wird mit 4096 berechnet. Dann werden alle Daten in den drei Paketen gelesen und in den Lesepuffer geschrieben und eine Erfolgsmeldung zurückgegeben.
Es muss auf eine andere Situation geachtet werden : Wenn die Warteschlange kcp.rcv_queue
2 vom Benutzer gesendete Pakete mit seg.frg
Wert von 2/1/0/2/1/0 enthält und in 6 Datenpakete fragmentiert ist, ist dies der Fall Außerdem muss ikcp_recv zweimal aufgerufen werden, um alle empfangenen Daten vollständig auszulesen.
Rufen Sie ikcp_input: auf k1 auf (Abbildung Schritt 5-1)
Geben Sie die Bestätigungspakete [ACK sn=1 una=3]
und [ACK sn=2 una=3]
ein und analysieren Sie sie in seg.una
=3. Das Paket [PSH sn=1 frg=1 len=1376]
[PSH sn=2 frg=0 len=1344]
wird bestätigt und über una aus der kcp.snd_buf
Warteschlange entfernt.
Alle gesendeten Daten wurden bestätigt.
Fenster wird zur Flusskontrolle verwendet. Es markiert einen logischen Bereich der Warteschlange. Aufgrund der Verarbeitung tatsächlicher Daten verschiebt sich die Position der Warteschlange weiterhin an die obere Position der Sequenznummer. Logischerweise wird sich dieses Fenster gleichzeitig weiter bewegen und erweitern und verkleinern, weshalb es auch als Schiebefenster bezeichnet wird.
Dieses schematische Diagramm ist eine weitere Darstellung der Schritte 3-1 bis 4-1 des Flussdiagramms im Abschnitt „Grundlegender Datensende- und -empfangsprozess“. Da es sich um Vorgänge außerhalb des Schrittbereichs handelt, werden die Datenrichtungen durch halbtransparente Pfeile angezeigt.
Alle Daten werden durch die Funktion verarbeitet, auf die der Pfeil zeigt, und an einen neuen Ort verschoben (doppelt so groß wie das Bild):
Die von der Funktion ikcp_send auf der Sendeseite übergebenen Daten werden nach der Daten-Slicing-Verarbeitung direkt in der Sendewarteschlange kcp.snd_queue
gespeichert.
jedes Mal, wenn ikcp_flush aufgerufen wird. Die Fenstergröße für diese Übertragung wird basierend auf der Größe des Sendefensters kcp.snd_wnd
, der Größe des Remote-Fensters kcp.rmt_wnd
und der Größe des Überlastungsfensters kcp.cwnd
Der Wert ist das Minimum der drei: min( kcp.snd_wnd
, kcp.rmt_wnd
, kcp.cwd
) (ikcp.c:1017).
Wenn der Parameter nc über die Funktion ikcp_nodelay auf 1 gesetzt und der Flusskontrollmodus deaktiviert ist, wird die Berechnung des Überlastungsfensterwerts ignoriert. Das Berechnungsergebnis des Sendefensters ist min( kcp.snd_wnd
, kcp.rmt_wnd
) (ikcp.c:1018).
In der Standardkonfiguration, in der nur der Flusskontrollmodus deaktiviert ist, entspricht die Anzahl der Datenpakete, die zum ersten Mal gesendet werden können, dem Standardgrößenwert von kcp.snd_wnd
32. Dies unterscheidet sich vom Beispiel des grundlegenden Sende- und Empfangsprozesses, bei dem nur ein Paket zum ersten Mal gesendet werden kann, da die Flusskontrolle standardmäßig aktiviert ist.
Neu gesendete Datenpakete werden in die Warteschlange kcp.snd_buf
verschoben.
Für ikcp_send-Daten gibt es nur ein Slice-Limit von 127 (d. h. 127*
kcp.mss
=174752 Bytes). Es gibt keine Begrenzung für die Gesamtzahl der Pakete in der Sendewarteschlange. Siehe: So vermeiden Sie Verzögerungen bei der Cache-Akkumulation
kcp.snd_buf
speichert Daten, die gesendet werden oder gesendet wurden.
Bei jedem Aufruf von ikcp_flush
wird das Sendefenster berechnet und das Datenpaket von kcp.snd_queue
in die aktuelle Warteschlange verschoben. Alle Datenpakete in der aktuellen Warteschlange werden in drei Situationen verarbeitet:
1. Erster Datenversand (ikcp.c:1053)
Die Anzahl der gesendeten Pakete wird in seg.xmit
aufgezeichnet. Die Verarbeitung des ersten Versands ist relativ einfach, und einige Parameter seg.rto
/ seg.resendts
für das Zeitlimit für die erneute Übertragung werden initialisiert.
2. Daten-Timeout (ikcp.c:1058)
Wenn die intern aufgezeichnete Zeit kcp.current
den Timeout-Zeitraum seg.resendts
des Pakets selbst überschreitet, erfolgt eine Timeout-Neuübertragung.
3. Bestätigung der Datenübernahme (ikcp.c:1072)
Wenn das Datenende überspannt ist und die Anzahl der Spans seg.fastack
die Span-Neuübertragungskonfiguration kcp.fastresend
überschreitet, findet eine Span-Neuübertragung statt. ( kcp.fastresend
ist standardmäßig auf 0 eingestellt, und wenn es 0 ist, wird es als UINT32_MAX berechnet, und eine erneute Span-Übertragung findet nie statt.) Nach einer Timeout-Neuübertragung wird das aktuelle Paket seg.fastack
auf 0 zurückgesetzt.
Die Bestätigungsliste ist eine einfache Datensatzliste, die ursprünglich Sequenznummern und Zeitstempel ( seg.sn
/ seg.ts
) in der Reihenfolge aufzeichnet, in der Pakete empfangen werden.
Daher werden im schematischen Diagramm dieses Artikels
kcp.ack_list
keine leeren Elementpositionen gezeichnet. Da es sich nicht um eine logisch geordnete Warteschlange handelt (ebenso wurde dem Paket in der Warteschlangesnd_queue
noch keine Sequenznummer zugewiesen, seine logische Sequenznummer wurde jedoch bestimmt).
Der Empfänger puffert Datenpakete, die nicht verarbeitet werden können, vorübergehend.
Alle von ikcp_input eingehenden Datenpakete kommen zuerst in dieser Warteschlange an und die Informationen werden in der ursprünglichen Reihenfolge ihres Eintreffens in kcp.ack_list
aufgezeichnet.
Es gibt nur zwei Situationen, in denen Daten weiterhin in dieser Warteschlange verbleiben:
Hier wird zuerst das Paket [PSH sn=0]
empfangen, das die Bedingungen der verfügbaren Pakete erfüllt und zu kcp.rev_queue
verschoben wird.
Dann wurde das Paket [PSH sn=2]
empfangen, das nicht das nächste erwartete Paket war ( seg.sn
== kcp.rcv_nxt
), was dazu führte, dass dieses Paket in kcp.rcv_buf
verblieb.
Nachdem Sie das Paket [PSH sn=1]
empfangen haben, verschieben Sie die beiden hängengebliebenen Pakete [sn=1]
[sn=2]
nach kcp.rcv_queue
.
kcp.rcv_queue
erreicht die Empfangsfenstergröße kcp.rcv_wnd
(ikcp_recv wurde nicht rechtzeitig aufgerufen).Das empfangende Ende speichert Daten, die von der oberen Schicht gelesen werden können.
Im Streaming-Modus werden alle verfügbaren Pakete gelesen, im Nicht-Streaming-Modus werden fragmentierte Datensegmente gelesen und zu den kompletten Rohdaten zusammengesetzt.
Nachdem der Lesevorgang abgeschlossen ist, wird versucht, Daten von kcp.rcv_buf
in diese Warteschlange zu verschieben (möglicherweise, um den Zustand des vollständigen Empfangsfensters wiederherzustellen).
Der kcp.snd_wnd
-Wert des Sendefensters ist ein konfigurierter Wert und der Standardwert ist 32.
Das Remote-Fenster kcp.rmt_wnd
ist ein Wert, der aktualisiert wird, wenn der Sender ein Paket vom Empfänger empfängt (nicht nur ein Bestätigungspaket). Es zeichnet die verfügbare Länge (ikcp.c:1086) der Empfangswarteschlange kcp.rcv_queue
auf, wenn das aktuelle Datenpaket vom Empfänger gesendet wird. Der Anfangswert beträgt 128.
Das Überlastungsfenster ist ein berechneter Wert, der jedes Mal algorithmisch wächst, wenn Daten über ikcp_input empfangen werden.
Wenn beim Leeren der Daten ikcp_flush ein Paketverlust und eine schnelle Neuübertragung festgestellt werden, werden diese gemäß dem Algorithmus neu berechnet.
Die Position, an der
kcp.cwnd
auf 1 initialisiert wird, befindet sich im ersten ikcp_update-Aufruf von ikcp_flush.
Der kcp.rcv_wnd
-Wert des Empfangsfensters ist ein konfigurierter Wert und der Standardwert ist 128. Es begrenzt die maximale Länge der Empfangswarteschlange kcp.rcv_queue
.
In diesem Abschnitt wird eine verbesserte Version von kcp_optional.c bereitgestellt, die auf dem Beispielcode im Abschnitt „Grundlegendes Senden und Empfangen von Daten“ basiert. Sie können das Protokollverhalten weiter beobachten, indem Sie die Makrodefinition ändern.
Der Beispielcode beendet den Prozess, indem er angibt, dass eine bestimmte Anzahl von Daten fester Länge in k1 geschrieben und in k2 vollständig gelesen werden soll.
Zur Steuerung bestimmter Funktionen stehen Makros zur Verfügung:
Das Überlastungsfenster wird über die Werte von kcp.cwnd
und kcp.incr
aufgezeichnet. Da es sich bei der von kcp.cwnd
aufgezeichneten Einheit um ein Paket handelt, ist zusätzlich kcp.incr
erforderlich, um das in Byte-Längeneinheiten ausgedrückte Überlastungsfenster aufzuzeichnen.
Wie TCP ist auch die KCP-Überlastungskontrolle in zwei Phasen unterteilt: langsamer Start und Überlastungsvermeidung:
Das Überlastungsfenster wächst bei der Bestätigung des Datenpakets jedes Mal, wenn kcp.snd_buf
Warteschlange bestätigt werden (wirksame UNA-Bestätigung, kcp.snd_una
ändert sich). Und wenn das Überlastungsfenster kleiner ist als das aufgezeichnete Remote-Fenster kcp.rmt_wnd
, wird das Überlastungsfenster vergrößert. (ikcp:875)
1. Wenn das Überlastungsfenster kleiner als der Schwellenwert für langsamen Start kcp.ssthresh
ist, befindet es sich in der Phase für langsamen Start , und das Wachstum des Überlastungsfensters ist zu diesem Zeitpunkt relativ aggressiv. Das Überlastungsfenster wächst um eine Einheit.
2. Wenn das Überlastungsfenster größer oder gleich dem Schwellenwert für den langsamen Start ist, befindet es sich in der Phase der Überlastungsvermeidung und das Wachstum des Überlastungsfensters ist relativ konservativ. Wenn kcp.incr
jedes Mal mss/16 erhöht, sind 16 gültige UNA-Bestätigungen erforderlich, bevor ein Einheitenüberlastungsfenster vergrößert wird. Das tatsächliche Fensterwachstum in der Stauvermeidungsphase beträgt:
(mss * mss) / incr + (mss / 16)
Da incr=cwnd*mss gilt:
((mss * mss) / (cwnd * mss)) + (mss / 16)
Entspricht:
(mss / cwnd) + (mss / 16)
Das Überlastungsfenster wächst schrittweise für jeden cwnd und alle 16 gültigen UNA-Bestätigungen.
Reduzierung des Überlastungsfensters : Wenn die Funktion ikcp_flush einen Paketverlust bei erneuten Übertragungen oder Zeitüberschreitungen erkennt, wird das Überlastungsfenster reduziert.
1. Wenn eine Span-Neuübertragung erfolgt, wird der Schwellenwert für den langsamen Start kcp.ssthresh
auf die Hälfte der unbestätigten Sequenznummernspanne gesetzt. Die Größe des Überlastungsfensters ist der Schwellenwert für den langsamen Start plus den Konfigurationswert für die schnelle Neuübertragung kcp.resend
(ikcp:1117):
ssthresh = (snd_nxt - snd_una) / 2
cwnd = ssthresh + resend
2. Wenn ein Paketverlust-Timeout erkannt wird, wird der Schwellenwert für den langsamen Start auf die Hälfte des aktuellen Überlastungsfensters festgelegt. Überlastungsfenster auf 1 gesetzt (ikcp:1126):
ssthresh = cwnd / 2
cwnd = 1
Beobachten Sie den langsamen Start 1 : Führen Sie den Beispielcode mit der Standardkonfiguration aus und Sie werden feststellen, dass der langsame Startvorgang schnell übersprungen wird. Dies liegt daran, dass der Standardschwellenwert für den langsamen Start 2 ist:
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 im Ausgabeinhalt ist die logische Zeit, n ist die Häufigkeit, mit der k1 Daten im Zyklus sendet, und der Wert von cwnd = 1 | 1 zeigt an, dass die 1 vor dem vertikalen Balkensymbol das berechnete Fenster ist, wenn ikcp_flush das ist ist, min(kcp. im Flusskontrollmodus. snd_wnd, kcp.rmt_wnd, kcp.cwnd), der Wert der folgenden 1 ist kcp.cwnd
.
Beobachten Sie das Wachstum des Überlastungsfensters unter der Standardkonfiguration grafisch: Je größer das Überlastungsfenster in der Überlastungsvermeidungsphase ist, desto gleichmäßiger ist das Wachstum des Überlastungsfensters.
Beobachten Sie den langsamen Start 2 : Passen Sie den Anfangswert KCP_THRESH_INIT der Beispielkonfiguration für den langsamen Startschwellenwert auf 16 an:
#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
...
Durch das Abfangen nur einen kurzen Zeitraum vor der Übertragungsrunde lässt sich beobachten, dass auch der langsame Start standardmäßig linear zunimmt.
Beobachten Sie die Deaktivierung der verzögerten Bestätigung : Senden Sie so viele Daten wie möglich, deaktivieren Sie die Option ACK_DELAY_FLUSH für verzögertes Senden und simulieren Sie einen Paketverlust:
#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
...
In diesem Fall wird ein klassisches Diagramm des langsamen Starts und der Vermeidung von Staus erhalten. Bei der 15. Übertragungsrunde (t=1500) wurde ein Paketverlust festgestellt:
Das Deaktivieren der Option für verzögertes Senden bedeutet, dass die empfangende Datenpartei unmittelbar nach jeder Ausführung von ikcp_input ikcp_flush aufruft, um ein Bestätigungspaket zurückzusenden.
Der langsame Startprozess nimmt zu diesem Zeitpunkt exponentiell mit jeder RTT (Round-Trip Time, Rundreisezeit) zu, da jedes Bestätigungspaket unabhängig gesendet wird, wodurch das Überlastungsfenster des Absenders wächst und die Anzahl der Pakete zunimmt, da das Überlastungsfenster zunimmt Das innerhalb jedes RTT gesendete Paket wird verdoppelt.
Sollte sich die Bestätigung verzögern, wird ein Bestätigungspaket mitgeschickt. Der Vorgang zum Erhöhen des Überlastungsfensters wird bei jedem Aufruf der Funktion ikcp_input nur einmal ausgeführt, sodass das Zusammenführen mehrerer empfangener Bestätigungspakete nicht zu einer mehrfachen Vergrößerung des Überlastungsfensters führt.
Wenn das Taktzyklusintervall größer als RTT ist, erhöht es sich mit jedem Intervall exponentiell. Das schematische Diagramm zeigt eine mögliche Situation:
Die Voraussetzung für das exponentielle Wachstum besteht darin, dass die beim nächsten Mal gesendeten Daten die doppelte Anzahl an Datenpaketen beim letzten Mal erfüllen können. Wenn die auf das sendende Ende geschriebenen Daten nicht ausreichen, wird kein exponentielles Wachstum erreicht.
Es ist zu beachten, dass selbst wenn der Beispielcode eine sofortige Bestätigung sendet, dies nur Auswirkungen auf die Art und Weise hat, wie der Empfänger eine Bestätigung sendet. Der Absender muss außerdem auf den nächsten Zyklus warten, bevor er diese Bestätigungspakete verarbeitet. Daher dient die Zeit t hier nur als Referenz, sofern das empfangene Paket nicht sofort im eigentlichen Netzwerk-Transceiver-Code verarbeitet und gespeichert wird, muss es bis zum Aktualisierungszyklus warten, bevor es antwortet.
Basierend auf den Eigenschaften von KCP muss es eine direktere Möglichkeit geben, das Überlastungsfenster zu vergrößern und die Flusskontrolle direkt zu deaktivieren, um aggressiveres Senden zu erzielen:
#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
Sie können den Wert des Überlastungsfensters auch direkt nach Bedarf ändern.
Beachten Sie, dass das Remote-Fenster voll ist : Wenn die Länge der gesendeten Daten nahe an der Standardgröße des Remote-Fensters liegt und der Empfänger sie nicht rechtzeitig ausliest, wird es einen Zeitraum geben, in dem keine Daten gesendet werden können (beachten Sie, dass Im Beispielcode sendet der Empfänger zuerst das Bestätigungspaket und liest dann den Inhalt erneut):
#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
Wenn die im Empfänger aufgezeichnete Remote-Fenstergröße kcp.rmt_wnd
0 ist, wird die Probe-Wartephase (Probe-Wartezeit, ikcp.c:973) in ikcp_flush gestartet. kcp.ts_probe
zeichnet zunächst eine Zeit von 7000 Millisekunden auf (aufgezeichnet in kcp->probe_wait
).
Wenn es soweit ist, kodieren Sie zusätzlich ein Paket vom Typ IKCP_CMD_WASK und senden Sie es an das empfangende Ende (ikcp.c:994), um das entfernte Ende aufzufordern, ein Paket vom Typ IKCP_CMD_WINS zurückzusenden, um kcp.rmt_wnd
zu aktualisieren
Wenn die Größe des Remote-Fensters immer 0 ist, erhöht sich kcp->probe_wait
jedes Mal um die Hälfte des aktuellen Werts und aktualisiert dann die Wartezeit. Die maximale Wartezeit beträgt 120000 Millisekunden (120 Sekunden).
Wenn die Größe des Remote-Fensters nicht 0 ist, wird der obige Erkennungsstatus gelöscht.
In diesem Beispiel warten wir nicht die ersten 7 Sekunden, bevor wir die aufgezeichnete Remote-Fenstergröße wiederherstellen. Denn bei der Operation von ikcp_recv auf der Empfangsseite zum Lesen von Daten wird ein Flag-Bit angezeigt, wenn die Länge kcp.rcv_queue
Warteschlange größer oder gleich dem Empfangsfenster kcp.rcv_wnd
ist, bevor die Daten gelesen werden (das Lesefenster ist voll). (ikcp .c:431) und senden Sie beim nächsten Mal ein Paket vom Typ IKCP_CMD_WINS (ikcp.c:1005) ikcp_flush Um das sendende Ende zu benachrichtigen, die neueste Remote-Fenstergröße zu aktualisieren.
Um dieses Problem zu vermeiden, müssen die Daten rechtzeitig auf der Empfängerseite ausgelesen werden. Selbst wenn das Remote-Fenster kleiner wird, wird jedoch das Sendefenster des Absenders kleiner, was zu zusätzlichen Verzögerungen führt. Gleichzeitig ist es auch notwendig, das Empfangsfenster des Empfangsendes zu vergrößern.
Versuchen Sie, den RECV_TIME- Wert auf einen relativ großen Wert (z. B. 300 Sekunden) zu ändern, und beobachten Sie dann das Senden des IKCP_CMD_WASK-Pakets.
Wie in der Beschreibung der kcp.snd_buf
Warteschlange beschrieben, werden beim Aufruf von ikcp_flush alle Pakete in der Warteschlange durchlaufen, wenn das Paket nicht zum ersten Mal gesendet wird. Anschließend wird geprüft, ob das Paket die angegebene Anzahl von Malen bestätigt hat oder ob die Timeout-Zeitspanne erreicht wurde.
Felder im Zusammenhang mit der Berechnung der Umlaufzeit und Zeitüberschreitung sind:
kcp.rx_rttval
: Glatte Netzwerk-Jitter-Zeitkcp.rx_srtt
: Glatte Roundtrip-Zeitkcp.rx_rto
(Timeout für erneute Übertragung empfangen): Timeout für erneute Übertragung, Anfangswert 200kcp.rx_minrto
: Minimales Zeitlimit für die erneute Übertragung, Anfangswert 100kcp.xmit
: globale Neuübertragungsanzahlseg.resendts
: Zeitstempel für die erneute Übertragungseg.rto
: Zeitüberschreitung bei erneuter Übertragungseg.xmit
: Anzahl der erneuten ÜbertragungenBevor wir besprechen, wie das Paket Timeouts berechnet, schauen wir uns zunächst an, wie die zugehörigen Felder Roundtrip-Zeit und Timeout berechnet werden:
Aufzeichnung der Roundtrip-Zeit : Jedes Mal, wenn ein ACK-Bestätigungspaket verarbeitet wird, enthält das Bestätigungspaket die Sequenznummer und die Zeit, zu der die Sequenznummer an den Absender gesendet wurde ( seg.sn
/ seg.ts
). Wenn zulässig, der Roundtrip Zeitaktualisierungsprozess wird ausgeführt.
Der Wert von rtt ist die Umlaufzeit eines einzelnen Pakets, also rtt= kcp.current
– seg.ts
Wenn die geglättete Rundreisezeit kcp.rx_srtt
0 ist, bedeutet dies, dass die Initialisierung durchgeführt wird: kcp.rx_srtt
wird direkt als rtt aufgezeichnet, kcp.rx_rttval
wird als Hälfte von RTT aufgezeichnet.
Im Nichtinitialisierungsprozess wird ein Delta-Wert berechnet, der den Schwankungswert dieses RTT und die aufgezeichneten kcp.rx_srtt
(IKCP.C: 550) darstellt:
delta = abs(rtt - rx_srtt)
Das neue kcp.rx_rttval
wird durch den gewichteten Wert des alten kcp.rx_rttval
und Delta aktualisiert:
rx_rttval = (3 * rx_rttval + delta) / 4
Der neue kcp.rx_srtt
wird durch den gewichteten Wert des alten kcp.rx_srtt
und rtt aktualisiert und kann nicht weniger als 1 betragen:
rx_srtt = (7 * rx_srtt + rtt) / 8
rx_srtt = max(1, rx_srtt)
Das neue rx_rto
wird durch den Mindestwert der geglätteten Rundtrip-Zeit kcp.rx_srtt
plus des Taktzyklus kcp.interval
und 4-mal rx_rttval
aktualisiert, und der Bereich ist auf [ kcp.rx_minrto
, 60000] beschränkt:
rto = rx_srtt + max(interval, 4 * rttval)
rx_rto = min(max(`kcp.rx_minrto`, rto), 60000)
Im Idealfall nähert sich der Wert von kcp.rx_rttval
im Idealfall, wenn das Netzwerk nur die Verzögerung und keinen Jitter festgelegt hat, und der Wert von kcp.rx_rto
wird durch die reibungslose Roundtrip-Zeit und den Taktzyklus bestimmt.
Glatte Rundzeit -Berechnungsdiagramm:
Erste Vertragszustellzeit (IKCP.C: 1052):
seg.rto
erfasst den Status kcp.rx_rto
, und die erste Auszeit des Datenpakets ist seg.rto
+ rtomin Milliseconds.
RTIomin wird durch kcp.rx_rto
berechnet, wenn der Nodelay -Modus aktiviert ist. rtomin ist 0, ansonsten kcp.rx_rto
/8.
Timeout für Nodelay nicht aktiviert:
resendts = current + rx_rto + rx_rto / 8
Aktivieren Sie Nodelay Timeout:
resendts = current + rx_rto
Timeout -Übertragung (IKCP.C: 1058):
Wenn die interne Zeit die Zeitüberschreitungszeit erreicht, die seg.resendts
des Datenpakets ist, wird das Paket mit dieser Sequenznummer erneut übertragen.
Wenn der Nodelay -Modus nicht aktiviert ist, beträgt das Inkrement von seg.rto
max ( seg.rto
, kcp.rx_rto
) (Doppelwachstum):
rto += max(rto, rx_rto)
Wenn Nodelay aktiviert ist und Nodelay 1 ist, erhöhen Sie seg.rto
jedes Mal um die Hälfte (1,5 -facher Anstieg):
rto += rto / 2
Wenn Nodelay aktiviert ist und Nodelay 2 ist, wird kcp.rx_rto
jedes Mal um die Hälfte erhöht (1,5 -fach erhöht):
rto += rx_rto / 2
Die neue Auszeit ist nach seg.rto
Milliseconds:
resendts = current + rx_rto
Übermittlung über die Zeit (IKCP.C: 1072):
Wenn ein Datenpaket eine bestimmte Anzahl von Male überschritten wird, wird eine Übergangsübertragung ausgelöst.
seg.rto
wird bei der Übermittlung im Laufe der Zeit nicht aktualisiert, und die nächste Zeit -out -Übertragungszeit wird direkt neu berechnet:
resendts = current + rto
Beobachten Sie die Standardzeit
Senden Sie nur ein Paket, lassen Sie es viermal fallen und beobachten Sie die Zeitüberschreitungs- und Wiedervermietungszeit.
Für die Standardkonfiguration beträgt der Anfangswert von kcp.rx_rto
200 Millisekunden, und die erste Auszeit von 225 Millisekunden. .
#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
Beachten Sie eine 1,5 -fach -Erhöhung der RTO basierend auf dem Paket selbst
#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
Beobachten Sie das 1,5 -fache Wachstum basierend auf RTO
#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
Beobachten Sie die Spannweitenübertragungen
Die [sn=2]
[sn=0]
verarbeiteten Bestätigungspakete auslösen [sn=1]
die Übertragung der Spannweite. Am Ende wurde es durch Timeout erneut übertragen.
#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
Beim Senden von Datenpaketen in zwei Schritten wird die zweite Gruppe von Paketen bei der Durchführung von IKCP_Input -Bestätigung zweimal gekreuzt, und [sn=0]
wird während der nächsten IKCP_FLUSH gesammelt und erneut übertragen.
#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
Der Artikel ist unter einer Kreative Commons Attribution-Noncommercial-Noderivs 4.0 International Lizenz lizenziert.
Der Code im Projekt ist Open Source verwendet die MIT -Lizenz.
Über die Bildschrift: Noto sans sc