ในแอปพลิเคชันบางตัว การใช้ TCP อย่างเดียวไม่สามารถตอบสนองความต้องการได้ การใช้ดาตาแกรม UDP โดยตรงไม่สามารถรับประกันความน่าเชื่อถือของข้อมูลได้ และบ่อยครั้งจำเป็นต้องปรับใช้โปรโตคอลการส่งข้อมูลที่เชื่อถือได้โดยอิงตาม UDP ที่เลเยอร์แอปพลิเคชัน
การใช้โปรโตคอล KCP โดยตรงคือทางเลือกหนึ่ง ซึ่งใช้โปรโตคอลการส่งข้อมูลอัตโนมัติที่มีประสิทธิภาพ และให้การปรับพารามิเตอร์ฟรีเพิ่มเติม ปรับให้เข้ากับความต้องการของสถานการณ์ที่แตกต่างกันผ่านพารามิเตอร์การกำหนดค่าและวิธีการโทรที่เหมาะสม
รู้เบื้องต้นเกี่ยวกับ KCP:
KCP เป็นโปรโตคอลที่รวดเร็วและเชื่อถือได้ซึ่งสามารถลดความล่าช้าโดยเฉลี่ยได้ 30%-40% และลดความล่าช้าสูงสุดได้ 3 เท่า โดยมีต้นทุนแบนด์วิธมากกว่า TCP 10%-20% การใช้อัลกอริธึมที่แท้จริงจะไม่รับผิดชอบต่อการส่งและรับโปรโตคอลพื้นฐาน (เช่น 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 มีเพียง 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;
/* ... */
}
เมื่อใช้เป็นองค์ประกอบคิว องค์ประกอบก่อนหน้า (ก่อนหน้า) และองค์ประกอบถัดไป (ถัดไป) ในคิวที่มีองค์ประกอบปัจจุบันอยู่จะถูกบันทึก
เมื่อก่อนหน้าชี้ไปที่คิว หมายความว่าองค์ประกอบปัจจุบันอยู่ที่จุดเริ่มต้นของคิว และเมื่อชี้ถัดไปไปที่คิว หมายความว่าองค์ประกอบปัจจุบันอยู่ที่จุดสิ้นสุดของคิว
การดำเนินการคิวทั้งหมดจัดทำไว้เป็นมาโคร
วิธีการกำหนดค่าที่ 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
ในแต่ละครั้ง ค่าดีฟอลต์คือ 1400
ในแผนภาพ วิธีการ ikcp_output จะเรียกตัวชี้ฟังก์ชัน
ikcp.output
ในที่สุด (ikcp.c:212)
ความยาวข้อความสูงสุดของ kcp.mss
คำนวณโดยการลบโอเวอร์เฮดของโปรโตคอล (24 ไบต์) ออกจาก kcp.mtu
ค่าดีฟอลต์คือ 1376
จะไม่มีการดำเนินการเรียกกลับ 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: (รูปที่ step 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: (รูปที่ Step 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 (รูปที่ step 3-1)
อินพุตแพ็กเก็ต [ACK sn=0 una=1]
UNA ยืนยันว่า :
พัสดุใดๆ ที่ได้รับจะพยายามยืนยัน 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 (รูปที่ step 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
มี 2 แพ็กเก็ตที่ผู้ใช้ส่งโดยมีค่า 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
เกินการกำหนดค่าการส่งข้อมูลซ้ำของ span kcp.fastresend
การส่งข้อมูลซ้ำของ span จะเกิดขึ้น ( kcp.fastresend
มีค่าเริ่มต้นเป็น 0 และเมื่อเป็น 0 จะถูกคำนวณเป็น UINT32_MAX และการส่งข้อมูลแบบ span จะไม่เกิดขึ้น) หลังจากการส่งสัญญาณซ้ำแบบหมดเวลา แพ็กเก็ตปัจจุบัน 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_Option.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 ในแต่ละครั้ง จำเป็นต้องมีการยืนยัน UNA ที่ถูกต้อง 16 ครั้งก่อนที่หน้าต่างความแออัดของหน่วยจะเพิ่มขึ้น การเติบโตของกรอบเวลาการหลีกเลี่ยงความแออัดที่แท้จริงคือ:
(mss * mss) / incr + (mss / 16)
เนื่องจาก incr=cwnd*mss คือ:
((mss * mss) / (cwnd * mss)) + (mss / 16)
เทียบเท่ากับ:
(mss / cwnd) + (mss / 16)
กรอบเวลาความแออัดจะเพิ่มขึ้นเรื่อยๆ สำหรับทุก ๆ cwnd และทุกๆ 16 การยอมรับ UNA ที่ถูกต้อง
การลดหน้าต่างความแออัด : เมื่อฟังก์ชัน ikcp_flush ตรวจพบการสูญเสียแพ็กเก็ตระหว่างการส่งสัญญาณซ้ำหรือการหมดเวลา หน้าต่างความแออัดจะลดลง
1. เมื่อการส่งสัญญาณ span เกิดขึ้น เกณฑ์การเริ่มช้า 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_flush ทันทีหลังจากดำเนินการ ikcp_input แต่ละครั้งเพื่อส่งแพ็กเก็ตการยืนยันกลับมา
กระบวนการเริ่มต้นที่ช้าในเวลานี้จะเพิ่มขึ้นแบบทวีคูณทุกๆ RTT (Round-Trip Time, Round trip time) เนื่องจากแต่ละแพ็กเก็ตการตอบรับจะถูกส่งแยกกัน ทำให้หน้าต่างความแออัดของผู้ส่งเพิ่มขึ้น และเนื่องจากหน้าต่างความแออัดเพิ่มขึ้น จำนวนแพ็กเก็ตที่เพิ่มมากขึ้น ส่งภายในแต่ละ RTT จะเพิ่มขึ้นสองเท่า
หากการยืนยันล่าช้า พัสดุยืนยันจะถูกส่งไปพร้อมกัน กระบวนการเพิ่มหน้าต่างความแออัดจะดำเนินการเพียงครั้งเดียวในแต่ละครั้งที่มีการเรียกใช้ฟังก์ชัน ikcp_input ดังนั้นการรวมแพ็กเก็ตการรับทราบหลายรายการที่ได้รับจะไม่ส่งผลต่อการเพิ่มหน้าต่างความแออัดหลายครั้ง
หากช่วงรอบสัญญาณนาฬิกามากกว่า RTT จะเพิ่มขึ้นแบบทวีคูณทุกช่วงเวลา แผนภาพแสดงสถานการณ์ที่เป็นไปได้:
สมมติฐานของการเติบโตแบบเอ็กซ์โปเนนเชียลคือข้อมูลที่ส่งในครั้งต่อไปสามารถตอบสนองจำนวนแพ็คเก็ตข้อมูลเป็นสองเท่าในครั้งล่าสุด หากข้อมูลที่เขียนไปยังจุดสิ้นสุดการส่งไม่เพียงพอ การเติบโตแบบเอ็กซ์โพเนนเชียลจะไม่เกิดขึ้น
ควรสังเกตว่าแม้ว่าโค้ดตัวอย่างจะส่งการยืนยันทันที แต่จะส่งผลต่อวิธีที่ผู้รับส่งการยืนยันเท่านั้น ผู้ส่งยังต้องรอรอบถัดไปก่อนที่จะประมวลผลแพ็คเก็ตการยืนยันเหล่านี้ ดังนั้นเวลาที่อยู่ที่นี่มีไว้เพื่อการอ้างอิงเท่านั้น เว้นแต่แพ็กเก็ตที่ได้รับจะไม่ได้รับการประมวลผลและจัดเก็บไว้ในโค้ดตัวรับส่งสัญญาณเครือข่ายจริงทันที จะต้องรอจนกว่าจะถึงรอบการอัพเดตก่อนจึงจะตอบสนอง
ขึ้นอยู่กับคุณลักษณะของ 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
+ RTOMON MILLISONDS
RTomin คำนวณโดย kcp.rx_rto
หากเปิดใช้งานโหมด Nodelay rtomin คือ 0 มิฉะนั้น kcp.rx_rto
/8
หมดเวลาสำหรับ Nodelay ที่ไม่ได้เปิดใช้งาน:
resendts = current + rx_rto + rx_rto / 8
เปิดใช้งาน Nodelay Timeout:
resendts = current + rx_rto
Timeout retransmission (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
retransmission ข้ามเวลา (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.5 เท่าของ 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=0]
ประมวลผลโดยการรวม [sn=1]
ไม่กระตุ้นการส่งสัญญาณซ้ำ ในที่สุดมันก็ถูกส่งผ่านหมดเวลา
#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
รหัสในโครงการเป็นโอเพ่นซอร์สโดยใช้ใบอนุญาต MIT
เกี่ยวกับตัวอักษรภาพ: noto sans sc