في بعض التطبيقات، لا يمكن أن يؤدي استخدام TCP ببساطة إلى تلبية الاحتياجات. لا يمكن أن يضمن الاستخدام المباشر لمخططات بيانات UDP موثوقية البيانات، وغالبًا ما يكون من الضروري تنفيذ بروتوكول نقل موثوق يعتمد على UDP في طبقة التطبيق.
يعد استخدام بروتوكول KCP مباشرة أحد الخيارات، والذي ينفذ بروتوكول إعادة إرسال تلقائي قوي ويوفر تعديلًا مجانيًا للمعلمات فوقه. التكيف مع احتياجات السيناريوهات المختلفة من خلال معلمات التكوين وطرق الاتصال المناسبة.
مقدمة إلى KCP:
KCP هو بروتوكول سريع وموثوق يمكنه تقليل متوسط التأخير بنسبة 30%-40% وتقليل الحد الأقصى للتأخير بمقدار ثلاث مرات بتكلفة عرض نطاق ترددي أكبر بنسبة 10%-20% من TCP. إن تطبيق الخوارزمية البحتة ليس مسؤولاً عن إرسال واستقبال البروتوكولات الأساسية (مثل UDP). يحتاج المستخدمون إلى تحديد طريقة إرسال حزم البيانات ذات الطبقة السفلية وتقديمها إلى KCP في شكل رد اتصال. حتى الساعة تحتاج إلى تمريرها خارجيًا، ولن يكون هناك أي استدعاء للنظام داخليًا. يحتوي البروتوكول بأكمله على ملفين مصدريين فقط، ikcp.h وikcp.c، ويمكن دمجهما بسهولة في مكدس البروتوكول الخاص بالمستخدم. ربما قمت بتنفيذ بروتوكول P2P أو بروتوكول يستند إلى UDP ولكنك تفتقر إلى التنفيذ الكامل والموثوق لبروتوكول ARQ، ثم قم ببساطة بنسخ هذين الملفين إلى المشروع الحالي، وكتابة بضعة أسطر من التعليمات البرمجية، ويمكنك استخدامه.
تقدم هذه المقالة بإيجاز عملية الإرسال والاستقبال الأساسية، ونافذة الازدحام، وخوارزمية المهلة لبروتوكول 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، ويتم إجراء اتصال البروتوكول عن طريق إنشاء كائنين 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:
IQUEUEHEAD عبارة عن قائمة بسيطة مرتبطة بشكل مزدوج تشير إلى عناصر البداية (السابقة) والأخيرة (التالية) في قائمة الانتظار:
struct IQUEUEHEAD {
/*
next:
作为队列时: 队列的首元素 (head)
作为元素时: 当前元素所在队列的下一个节点
prev:
作为队列时: 队列的末元素 (last)
作为元素时: 当前元素所在队列的前一个节点
*/
struct IQUEUEHEAD *next, *prev;
};
typedef struct IQUEUEHEAD iqueue_head;
عندما تكون قائمة الانتظار فارغة، سيشير التالي/السابق إلى قائمة الانتظار نفسها، وليس إلى NULL.
رأس بنية IKCPSEG كعنصر قائمة انتظار يعيد أيضًا استخدام بنية IQUEUEHEAD:
struct IKCPSEG
{
struct IQUEUEHEAD node;
/* ... */
}
عند استخدامه كعنصر قائمة انتظار، يتم تسجيل العنصر السابق (السابق) والعنصر التالي (التالي) في قائمة الانتظار حيث يوجد العنصر الحالي.
عندما يشير السابق إلى قائمة الانتظار، فهذا يعني أن العنصر الحالي موجود في بداية قائمة الانتظار، وعندما يشير التالي إلى قائمة الانتظار، فهذا يعني أن العنصر الحالي موجود في نهاية قائمة الانتظار.
يتم توفير جميع عمليات قائمة الانتظار كوحدات ماكرو.
طرق التكوين التي يوفرها 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_basic.c الذي يمكنه بشكل أساسي تشغيل KCP. يعد نموذج التعليمات البرمجية الذي يتكون من أقل من 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
: رقم القطعةقم بإنشاء بنية سياق KCP IKCPCB من خلال وظيفة ikcp_create.
يقوم IKCPCB داخليًا بإنشاء بنية IKCPSEG المقابلة لتخزين البيانات والحالة عن طريق الاتصال خارجيًا بـ ikcp_send (إدخال المستخدم إلى المرسل) وبيانات إدخال ikcp_input (إدخال المرسل إلى جهاز الاستقبال).
بالإضافة إلى ذلك، ستتم إزالة بنية IKCPSEG من خلال ikcp_recv (التي تمت إزالتها بواسطة المستخدم من الطرف المتلقي) وبيانات تأكيد ikcp_input (المستلمة من الطرف المرسل من الطرف المتلقي).
للحصول على اتجاه تدفق البيانات التفصيلي، راجع قسم قائمة الانتظار والنافذة .
يحدث إنشاء وتدمير IKCPSEG بشكل رئيسي في المواقف الأربعة المذكورة أعلاه، والبعض الآخر شائع في التنقل بين قوائم الانتظار الداخلية والتحسينات الأخرى.
في المقالات التالية، سيتم تمييز كافة حقول بنية IKCPCB وIKCPSEG التي تظهر بواسطة
标记
(تصفح تخفيض السعر فقط، وقد لا يراها الآخرون). سيتم بدء جميع حقول بنية IKCPCB بـkcp.
، وستكون جميع حقول بنية IKCPSEG مسبوقة بـseg.
. عادةً ما يكون اسم المتغير المطابق أو اسم معلمة الوظيفة في الكود المصدري هوkcp
أوseg
أيضًا.
يقوم هذا الرمز ببساطة بكتابة طول البيانات المحدد إلى كائن KCP المسمى k1 وقراءة البيانات من كائن 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_user_output بالإضافة إلى kcp.output
والذي يُستخدم لتحديد سلوك بيانات إخراج كائن KCP. يرتبط kcp.writelog
أيضًا بوظيفة kcp_user_writelog لتصحيح أخطاء الطباعة.
بالإضافة إلى ذلك، نظرًا لأن رد الاتصال kcp.output
لا يمكنه استدعاء ikcp_input آخر بشكل متكرر (لأنه سيعود في النهاية إلى kcp.output
الخاص به)، فيجب تخزين جميع بيانات الإخراج في موقع وسيط، ثم إدخالها في k2 بعد الخروج من 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 كتمييز.
يوصف الرسم التخطيطي الكامل لعملية تأكيد إرسال هذا الرمز بأنه (ضعف الحجم):
اتصل بـ ikcp_send على k1: (الشكل 1-1)
تتم كتابة البيانات بطول 4096 إلى المرسل. وفقًا لـ kcp.mss
يتم تقطيعه إلى ثلاث حزم بطول 1376/1376/1344، وعلامات التجزئة seg.frg
لكل حزمة هي 2/1/0 على التوالي.
تحدد وحدة الإرسال القصوى kcp.mtu
الحد الأقصى لطول البيانات التي يتلقاها رد الاتصال ikcp.output
في كل مرة.
في الرسم التخطيطي، ستستدعي الطريقة ikcp_output في النهاية مؤشر الدالة
ikcp.output
. (ikcp.c:212)
يتم حساب الحد الأقصى لطول رسالة kcp.mss
عن طريق طرح حمل البروتوكول (24 بايت) من kcp.mtu
.
لن يتم تنفيذ رد اتصال kcp.output
في هذا الوقت، وسيتم تخصيص جميع بيانات الجزء وتسجيلها في بنية IKCPSEG وإلحاقها بقائمة انتظار kcp.snd_queue
(ikcp.c:528).
في هذا الوقت، يبلغ طول قائمة الانتظار kcp.snd_queue
k1 3، وطول قائمة الانتظار kcp.snd_buf
هو 0.
اتصل بـ ikcp_flush على k1: (الشكل 1-2)
يتم هنا تجاهل عملية الحساب المحددة للنافذة. ما عليك سوى معرفة أن قيمة نافذة الازدحام
kcp.cwnd
هي 1 عندما يستدعي k1 ikcp_flush لأول مرة.
وبسبب حد نافذة الازدحام، يمكن إرسال حزمة واحدة فقط للمرة الأولى. يتم نقل كائن IKCPSEG بطول البيانات الأول لقائمة الانتظار kcp.snd_queue
إلى قائمة الانتظار kcp.snd_buf
(ikcp.c:1028)، وتكون قيمة الرقم التسلسلي seg.sn
المعين وفقًا لـ kcp.snd_nxt
هي 0 (ikcp .c:1036)، الحقل seg.cmd
هو IKCP_CMD_PUSH، يمثل حزمة دفع البيانات.
في هذا الوقت، طول قائمة الانتظار kcp.snd_queue
k1 هو 2، وطول قائمة الانتظار kcp.snd_buf
هو 1.
في الخطوة 1-3، قم بتنفيذ استدعاء ikcp_output (ikcp.c:1113) على البيانات المرسلة لأول مرة لإرسال حزمة البيانات [PSH sn=0 frg=2 len=1376]
.
لا يوجد سوى أربعة أنواع من أوامر البيانات: IKCP_CMD_PUSH (دفع البيانات) IKCP_CMD_ACK (التأكيد) IKCP_CMD_WASK (اكتشاف النافذة) IKCP_CMD_WINS (استجابة النافذة)، المعرفة في ikcp.c:29
اتصل بـ ikcp_input على k2: (الشكل 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
. (آيكب:709)
تحقق من توفر حزمة البيانات الأولى في قائمة الانتظار kcp.rcv_buf
إذا كانت حزمة بيانات متاحة، فسيتم نقلها إلى قائمة الانتظار kcp.rcv_queue
. (ikcp.c:726)
يتم تعريف حزم البيانات المتوفرة في kcp.rcv_buf
على النحو التالي: رقم تسلسل البيانات التالي المتوقع استلامه (مأخوذ من kcp.rcv_nxt
، حيث يجب أن يكون رقم تسلسل البيانات التالي seg.sn
== 0) وطول kcp.rcv_queue
قائمة الانتظار kcp.rcv_queue
أقل من حجم النافذة المستلمة.
في هذه الخطوة، يتم نقل حزمة البيانات الوحيدة الموجودة في قائمة الانتظار kcp.rcv_buf
مباشرةً إلى قائمة الانتظار kcp.rcv_queue
.
في هذا الوقت، يكون طول قائمة الانتظار kcp.>rcv_queue
k2 هو 1، وطول قائمة الانتظار kcp.snd_buf
هو 0. يتم تحديث قيمة الرقم التسلسلي للبيانات المستلمة التالي kcp.rcv_nxt
من 0 إلى 1.
اتصل بـ ikcp_flush على k2: (الشكل الخطوة 2-2)
في أول مكالمة ikcp_flush لـ k2. نظرًا لوجود بيانات في قائمة التأكيد kcp.acklist
، سيتم تشفير حزمة التأكيد وإرسالها (ikcp.c:958).
تم تعيين قيمة seg.una
في حزمة التأكيد kcp.rcv_nxt
=1.
يتم تسجيل هذه الحزمة كـ [ACK sn=0 una=1]
: وهذا يعني أنه في تأكيد ack، تم تأكيد رقم تسلسل الحزمة 0. في تأكيد واحد، يتم تأكيد جميع الحزم قبل الحزمة رقم 1.
في الخطوة 2-3، يتم استدعاء kcp.output
لإرسال حزمة البيانات.
اتصل بـ ikcp_recv على k2: (الشكل 2-4)
تحقق مما إذا كانت قائمة الانتظار kcp.rcv_queue
تحتوي على حزمة بقيمة seg.frp
0 (ikcp.c:459)، إذا كانت تحتوي على هذه الحزمة، فقم بتسجيل الحزمة الأولى seg.frp
== 0 وبيانات الحزمة قبل ذلك. يتم إرجاع الطول الإجمالي كقيمة الإرجاع. إذا لم يكن الأمر كذلك، تقوم هذه الدالة بإرجاع قيمة فشل تبلغ -1.
نظرًا لأن kcp.rcv_queue
يحتوي فقط على الحزمة [PSH sn=0 frg=2 len=1376]
في الوقت الحالي، فقد فشلت محاولة القراءة.
إذا كان في وضع الدفق (kcp.stream != 0)، فسيتم وضع علامة على جميع الحزم على أنها
seg.frg=0
. في هذا الوقت، سيتم قراءة أي حزم في قائمة الانتظارkcp.rcv_queue
بنجاح.
اتصل بـ ikcp_input: على k1 (الشكل الخطوة 3-1)
حزمة الإدخال [ACK sn=0 una=1]
.
أونا تؤكد :
أي طرد يتم استلامه سيحاول أولاً تأكيد UNA (ikcp.c:789)
تم تأكيد وإزالة كافة الحزم الموجودة في قائمة الانتظار kcp.snd_buf
seg.sn
أصغر من قيمة una (ikcp:599) عن طريق تأكيد قيمة seg.una
الخاصة بالحزمة.
تم تأكيد [PSH sn=0 frg=2 len=1376]
وإزالته من قائمة الانتظار kcp.snd_buf
لـ k1.
تأكيد الاستلام :
قم بتحليل نوع حزمة البيانات وأدخل معالجة فرع التأكيد. (ikcp.c:792)
قم بمطابقة الأرقام التسلسلية لحزم التأكيد وقم بإزالة الحزم المقابلة. (ikcp.c:581)
عند إجراء تأكيد ACK في الخطوة 3-1، تكون قائمة الانتظار kcp.snd_buf
فارغة بالفعل لأن الحزمة الوحيدة [PSH sn=0 frg=2 len=1376]
قد تم تأكيدها بواسطة UNA مسبقًا.
إذا تم تأكيد بيانات رأس قائمة الانتظار kcp.snd_buf
(تغيرات kcp.snd_una
)، فسيتم إعادة حساب قيمة cwnd لحجم نافذة الازدحام وتحديثها إلى 2 (ikcp.c:876).
مخطط تأكيد UNA / ACK، يسجل هذا المخطط بالإضافة إلى ذلك حالة kcp.snd_una
غير المميزة في مخطط العملية:
لن يعمل إقرار ACK مع حزم الإقرار التي تصل بشكل تسلسلي. بالنسبة للحزم التي تصل خارج الترتيب، تتم إزالة الحزمة بشكل فردي بعد التأكيد عبر ACK:
اتصل بـ ikcp_flush على k1: (الشكل 3-2)
تمامًا مثل الخطوة 1-2، تم تحديث قيمة نافذة الازدحام الجديدة kcp.cwnd
إلى 2، وسيتم إرسال الحزمتين المتبقيتين هذه المرة: [PSH sn=1 frg=1 len=1376]
[PSH sn=2 frg=0 len=1344]
.
في الخطوة 3-3، سيتم استدعاء kcp.output
مرتين لإرسال حزم البيانات على التوالي.
اتصل بـ ikcp_input: على k2 (الشكل 4-1)
حزم الإدخال [PSH sn=1 frg=1 len=1376]
و [PSH sn=2 frg=0 len=1344]
.
تتم إضافة كل حزمة إلى قائمة الانتظار kcp.rcv_buf
، وهي متاحة، وفي النهاية يتم نقلها كلها إلى قائمة الانتظار kcp.rcv_queue
.
في هذا الوقت، طول قائمة الانتظار kcp.rcv_queue
k2 هو 3، وطول kcp.snd_buf
هو 0. يتم تحديث قيمة kcp.rcv_nxt
للحزمة التالية المتوقع استلامها من 1 إلى 3.
اتصل بـ ikcp_flush على k2: (الشكل 4-2)
سيتم ترميز معلومات الإقرار في kcp.acklist
في حزم [ACK sn=1 una=3]
و [ACK sn=2 una=3]
وإرسالها في الخطوة 4-3.
في الواقع، سيتم كتابة هاتين الحزمتين في المخزن المؤقت وسيتم إجراء استدعاء kcp.output
.
اتصل بـ ikcp_recv على k2: (الشكل 4-4)
توجد الآن ثلاث حزم غير مقروءة في kcp.rcv_queue
: [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. ثم ستتم قراءة جميع البيانات الموجودة في الحزم الثلاث وكتابتها في المخزن المؤقت للقراءة وسيتم إرجاع النجاح.
يجب الانتباه إلى موقف آخر : إذا كانت قائمة الانتظار kcp.rcv_queue
تحتوي على حزمتين مرسلتين من قبل المستخدم بقيمة seg.frg
تبلغ 2/1/0/2/1/0 ومجزأة إلى 6 حزم بيانات، فإن المطابقة هي من الضروري أيضًا الاتصال بـ ikcp_recv مرتين لقراءة جميع البيانات الكاملة المستلمة.
اتصل بـ ikcp_input: على k1 (الشكل الخطوة 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]
وإزالتها من قائمة الانتظار kcp.snd_buf
من خلال una.
لقد تم الإقرار بجميع البيانات المرسلة.
يتم استخدام النافذة للتحكم في التدفق. إنه يمثل النطاق المنطقي لقائمة الانتظار. بسبب معالجة البيانات الفعلية، يستمر موضع قائمة الانتظار في الانتقال إلى الموضع العالي للرقم التسلسلي. ومن المنطقي أن تستمر هذه النافذة في التحرك والتوسع والانكماش في نفس الوقت، لذلك تسمى أيضًا بالنافذة المنزلقة .
يعد هذا الرسم التخطيطي تمثيلاً آخر للخطوات من 3-1 إلى 4-1 من مخطط التدفق في قسم "عملية إرسال واستقبال البيانات الأساسية". وكعمليات خارج نطاق الخطوات، تتم الإشارة إلى اتجاهات البيانات بأسهم شبه شفافة.
تتم معالجة جميع البيانات من خلال الوظيفة التي يشير إليها السهم ويتم نقلها إلى موقع جديد (ضعف حجم الصورة):
سيتم تخزين البيانات التي تم تمريرها بواسطة وظيفة 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).
إذا تم تعيين المعلمة nc على 1 من خلال وظيفة ikcp_nodelay وتم إيقاف تشغيل وضع التحكم في التدفق، فسيتم تجاهل حساب قيمة نافذة الازدحام. نتيجة الحساب لنافذة الإرسال هي min( kcp.snd_wnd
, kcp.rmt_wnd
) (ikcp.c:1018).
في التكوين الافتراضي لإيقاف تشغيل وضع التحكم في التدفق فقط، يكون عدد حزم البيانات التي يمكن إرسالها لأول مرة هو قيمة الحجم الافتراضية لـ kcp.snd_wnd
32. ويختلف هذا عن مثال عملية الإرسال والاستقبال الأساسية، حيث يمكن إرسال حزمة واحدة فقط للمرة الأولى نظرًا لتمكين التحكم في التدفق بشكل افتراضي.
سيتم نقل حزم البيانات المرسلة حديثًا إلى قائمة الانتظار kcp.snd_buf
.
بالنسبة لبيانات ikcp_send، لا يوجد سوى حد شريحة يبلغ 127 (أي 127*
kcp.mss
= 174752 بايت). لا يوجد حد لعدد الحزم الإجمالي في قائمة انتظار الإرسال. راجع: كيفية تجنب تأخير تراكم ذاكرة التخزين المؤقت
يقوم المخزن المؤقت للإرسال kcp.snd_buf
بتخزين البيانات التي سيتم إرسالها أو التي تم إرسالها.
في كل مرة يتم فيها استدعاء ikcp_flush
، يتم حساب نافذة الإرسال ويتم نقل حزمة البيانات من kcp.snd_queue
إلى قائمة الانتظار الحالية. ستتم معالجة جميع حزم البيانات الموجودة في قائمة الانتظار الحالية في ثلاث حالات:
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
بالترتيب الأصلي للوصول.
هناك حالتان فقط حيث ستظل البيانات في قائمة الانتظار هذه:
هنا، يتم استقبال الحزمة [PSH sn=0]
أولاً، والتي تلبي شروط الحزم المتوفرة وتنتقل إلى kcp.rev_queue
.
ثم تم استلام الحزمة [PSH sn=2]
، والتي لم تكن الحزمة التالية المتوقع استلامها ( seg.sn
== kcp.rcv_nxt
)، مما تسبب في بقاء هذه الحزمة في kcp.rcv_buf
.
بعد استلام الحزمة [PSH sn=1]
، انقل الحزمتين العالقتين [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
هي القيمة التي سيتم تحديثها عندما يتلقى المرسل حزمة من جهاز الاستقبال (وليس مجرد حزمة إقرار). يقوم بتسجيل الطول المتاح (ikcp.c:1086) لقائمة انتظار الاستلام للطرف المتلقي kcp.rcv_queue
عندما يتم إرسال حزمة البيانات الحالية بواسطة الطرف المتلقي. القيمة الأولية هي 128.
نافذة الازدحام هي قيمة محسوبة تنمو خوارزميًا في كل مرة يتم فيها تلقي البيانات عبر ikcp_input.
إذا تم اكتشاف فقدان الحزمة وإعادة الإرسال السريع عند مسح البيانات ikcp_flush، فسيتم إعادة حسابها وفقًا للخوارزمية.
الموضع الذي تمت فيه تهيئة
kcp.cwnd
إلى 1 موجود في أول استدعاء ikcp_update إلى ikcp_flush.
قيمة kcp.rcv_wnd
لنافذة الاستقبال هي قيمة تم تكوينها، والقيمة الافتراضية هي 128. إنه يحدد الحد الأقصى لطول قائمة انتظار الاستلام kcp.rcv_queue
.
في هذا القسم، يتم توفير إصدار محسّن من kcp_Optional.c استنادًا إلى نموذج التعليمات البرمجية الموجود في قسم "إرسال واستقبال البيانات الأساسية". ويمكنك مراقبة سلوك البروتوكول بشكل أكبر عن طريق تعديل تعريف الماكرو.
ينهي نموذج التعليمات البرمجية العملية عن طريق تحديد كتابة عدد محدد من البيانات ذات الطول الثابت إلى k1 وقراءتها بالكامل في k2.
يتم توفير وحدات الماكرو للتحكم في الوظائف المحددة:
يتم تسجيل نافذة الازدحام من خلال قيم kcp.cwnd
و kcp.incr
. نظرًا لأن الوحدة المسجلة بواسطة kcp.cwnd
عبارة عن حزمة، يلزم وجود kcp.incr
إضافي لتسجيل نافذة الازدحام معبرًا عنها بوحدات طول البايت.
مثل TCP، ينقسم التحكم في الازدحام KCP أيضًا إلى مرحلتين: البداية البطيئة وتجنب الازدحام:
تنمو نافذة الازدحام ؛ أثناء عملية تأكيد حزمة البيانات، في كل مرة يتم فيها تأكيد بيانات رأس قائمة الانتظار kcp.snd_buf
(تأكيد UNA الفعال، يتغير kcp.snd_una
). وعندما تكون نافذة الازدحام أصغر من النافذة البعيدة المسجلة kcp.rmt_wnd
، تزداد نافذة الازدحام. (آيكب:875)
1. إذا كانت نافذة الازدحام أصغر من عتبة البداية البطيئة kcp.ssthresh
، فهي في مرحلة البداية البطيئة ، ويكون نمو نافذة الازدحام قويًا نسبيًا في هذا الوقت. نافذة الازدحام تنمو بمقدار وحدة واحدة.
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
...
في هذه الحالة، يتم الحصول على رسم بياني كلاسيكي للبداية البطيئة وتجنب الازدحام. تم اكتشاف فقدان الحزمة في جولة الإرسال الخامسة عشرة (t=1500):
إن إيقاف تشغيل خيار الإرسال المؤجل يعني أن الطرف المتلقي للبيانات سيتصل بـ ikcp_flush مباشرة بعد كل تنفيذ لـ ikcp_input لإرسال حزمة تأكيد مرة أخرى.
تزيد عملية البدء البطيء في هذا الوقت بشكل كبير كل RTT (وقت ذهاب وإياب، وقت ذهاب وإياب)، لأنه سيتم إرسال كل حزمة إقرار بشكل مستقل، مما يتسبب في زيادة نافذة ازدحام المرسل، ونظرًا لنمو نافذة الازدحام، فإن عدد الحزم ستتضاعف الحزمة المرسلة داخل كل RTT.
إذا تأخر التأكيد، فسيتم إرسال حزمة التأكيد معًا. سيتم تنفيذ عملية زيادة نافذة الازدحام مرة واحدة فقط في كل مرة يتم فيها استدعاء وظيفة ikcp_input، لذا فإن دمج حزم الإقرار المتعددة المستلمة لن يكون له تأثير على زيادة نافذة الازدحام عدة مرات.
إذا كان الفاصل الزمني لدورة الساعة أكبر من RTT، فسوف يزداد بشكل كبير في كل فاصل زمني. يوضح الرسم التخطيطي الموقف المحتمل:
فرضية النمو الأسي هي أن البيانات المرسلة في المرة القادمة يمكن أن تلبي ضعف عدد حزم البيانات في المرة الأخيرة. إذا كانت البيانات المكتوبة إلى الطرف المرسل غير كافية، فلن يتم تحقيق النمو الأسي.
وتجدر الإشارة إلى أنه حتى لو أرسل نموذج التعليمات البرمجية تأكيدًا فوريًا، فإنه يؤثر فقط على الطريقة التي يرسل بها الطرف المتلقي التأكيد. يحتاج المرسل أيضًا إلى انتظار الدورة التالية قبل معالجة حزم التأكيد هذه. ولذلك، فإن الوقت 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.c:973) في ikcp_flush. سيسجل kcp.ts_probe
وقتًا مبدئيًا قدره 7000 مللي ثانية (مسجل في kcp->probe_wait
).
عندما يحين الوقت، قم أيضًا بتشفير حزمة من النوع IKCP_CMD_WASK وإرسالها إلى الطرف المتلقي (ikcp.c:994) لمطالبة الطرف البعيد بإرسال حزمة من النوع IKCP_CMD_WINS لتحديث kcp.rmt_wnd
إذا كان حجم النافذة البعيدة دائمًا 0، kcp->probe_wait
يزيد بمقدار نصف القيمة الحالية في كل مرة، ثم يقوم بتحديث وقت الانتظار. الحد الأقصى لوقت الانتظار هو 120000 مللي ثانية (120 ثانية).
عندما لا يكون حجم النافذة البعيدة 0، يتم مسح حالة الكشف أعلاه.
في هذا المثال، لا ننتظر أول 7 ثوانٍ قبل استعادة حجم النافذة البعيدة المسجلة. لأنه أثناء تشغيل ikcp_recv على الطرف المتلقي لقراءة البيانات، عندما يكون طول قائمة انتظار kcp.rcv_queue
أكبر من أو يساوي نافذة الاستقبال kcp.rcv_wnd
قبل قراءة البيانات (نافذة القراءة ممتلئة)، بت إشارة (ikcp .c:431) وأرسل حزمة من النوع IKCP_CMD_WINS (ikcp.c:1005) في المرة التالية ikcp_flush لإعلام الطرف المرسل بتحديث أحدث حجم للنافذة البعيدة.
لتجنب هذه المشكلة، يجب قراءة البيانات عند الطرف المتلقي في الوقت المناسب، ومع ذلك، حتى إذا أصبحت النافذة البعيدة أصغر، فستصبح نافذة الإرسال الخاصة بالمرسل أصغر، مما يؤدي إلى تأخيرات إضافية. وفي الوقت نفسه، من الضروري أيضًا زيادة نافذة الاستقبال للطرف المتلقي.
حاول تعديل قيمة 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 هي الوقت المستدير للرحمة لحزمة واحدة ، أي RTT = kcp.current
- seg.ts
إذا كان وقت الرحلة المستديرة الملساء kcp.rx_srtt
هو 0 ، فهذا يعني أنه يتم تنفيذ التهيئة: تم تسجيل kcp.rx_srtt
مباشرة باسم RTT ، kcp.rx_rttval
على أنها نصف RTT.
في عملية عدم التخصيص ، يتم حساب قيمة دلتا ، والتي تمثل قيمة تقلبات هذا RTT و kcp.rx_srtt
المسجلة (IKCP.C: 550):
delta = abs(rtt - rx_srtt)
يتم تحديث kcp.rx_rttval
الجديد بالقيمة المرجحة لـ kcp.rx_rttval
القديم ودلتا:
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
و 4 مرات rx_rttval
، والمدى يقتصر على [ 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 milliseconds.
يتم حساب rtomin بواسطة kcp.rx_rto
، إذا تم تمكين وضع nodelay. rtomin هو 0 ، وإلا kcp.rx_rto
/8.
مهلة Nodelay غير ممكّن:
resendts = current + rx_rto + rx_rto / 8
تمكين مهلة Nodelay:
resendts = current + rx_rto
إعادة نقل المهلة (IKCP.C: 1058):
عندما يصل الوقت الداخلي إلى وقت المهلة seg.resendts
من حزمة البيانات ، يتم إعادة إرسال الحزمة التي تحمل رقم التسلسل هذا.
عندما لا يتم تمكين وضع Nodelay ، فإن زيادة seg.rto
هي الحد الأقصى ( 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
milliseconds:
resendts = current + rx_rto
إعادة الإرسال عبر الوقت (IKCP.C: 1072):
عندما يتم عبور حزمة البيانات عدد المرات المحددة ، يتم تراكم عدد عمليات إعادة التقاط.
لا يتم تحديث seg.rto
عند إعادة التقديم عبر الزمن ، ويتم إعادة حساب وقت إعادة إرسال الوقت التالي:
resendts = current + rto
مراقبة المهلة الافتراضية
أرسل فقط حزمة واحدة ، وإسقاطها أربع مرات ، ومراقبة وقت المهلة وإعادة الإرسال.
بالنسبة للتكوين الافتراضي ، تبلغ القيمة الأولية لـ 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
راقب زيادة 1.5x في RTO بناءً على الحزمة نفسها
#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
شاهد نمو 1.5x على أساس 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
مراقبة عمليات إعادة الإرسال
[sn=2]
[sn=1]
حزم [sn=0]
المعالجة عن طريق الدمج إلى إعادة الإرسال. في النهاية ، تم إعادة إرساله من خلال المهلة.
#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
عند إرسال حزم بيانات في خطوتين ، سيتم عبور المجموعة الثانية من الحزم مرتين عند إجراء تأكيد IKCP_Input ، وسيتم تراكم [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 Noncommercial-Noderivs 4.0 الدولي.
الكود في المشروع مفتوح المصدر باستخدام ترخيص معهد ماساتشوستس للتكنولوجيا.
حول خط الصورة: noto sans sc