kcp源码剖析 (1)

kcp怎么学习,按照学习技术的三部曲

  1. 会用
  2. 会看
  3. 会改

那就开始第一步会用,下载kcp源码
https://github.com/skywind3000/kcp

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ../
make

kcp的代码很简单就一个ikcp.c 和一个ikcp.h
官方提提供了一个测试文件test.cpp

//=====================================================================
//
// test.cpp - kcp 测试用例
//
// 说明:
// gcc test.cpp -o test -lstdc++
//
//=====================================================================

#include <stdio.h>
#include <stdlib.h>

#include "test.h"
#include "ikcp.c"


// 模拟网络
LatencySimulator *vnet;

// 模拟网络:模拟发送一个 udp包
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
	union { int id; void *ptr; } parameter;
	parameter.ptr = user;
	vnet->send(parameter.id, buf, len);
	return 0;
}

// 测试用例
void test(int mode)
{
	// 创建模拟网络:丢包率10%,Rtt 60ms~125ms
	vnet = new LatencySimulator(10, 60, 125);

	// 创建两个端点的 kcp对象,第一个参数 conv是会话编号,同一个会话需要相同
	// 最后一个是 user参数,用来传递标识
	ikcpcb *kcp1 = ikcp_create(0x11223344, (void*)0);
	ikcpcb *kcp2 = ikcp_create(0x11223344, (void*)1);

	// 设置kcp的下层输出,这里为 udp_output,模拟udp网络输出函数
	kcp1->output = udp_output;
	kcp2->output = udp_output;

	IUINT32 current = iclock();
	IUINT32 slap = current + 20;
	IUINT32 index = 0;
	IUINT32 next = 0;
	IINT64 sumrtt = 0;
	int count = 0;
	int maxrtt = 0;

	// 配置窗口大小:平均延迟200ms,每20ms发送一个包,
	// 而考虑到丢包重发,设置最大收发窗口为128
	ikcp_wndsize(kcp1, 128, 128);
	ikcp_wndsize(kcp2, 128, 128);

	// 判断测试用例的模式
	if (mode == 0) {
		// 默认模式
		ikcp_nodelay(kcp1, 0, 10, 0, 0);
		ikcp_nodelay(kcp2, 0, 10, 0, 0);
	}
	else if (mode == 1) {
		// 普通模式,关闭流控等
		ikcp_nodelay(kcp1, 0, 10, 0, 1);
		ikcp_nodelay(kcp2, 0, 10, 0, 1);
	}	else {
		// 启动快速模式
		// 第二个参数 nodelay-启用以后若干常规加速将启动
		// 第三个参数 interval为内部处理时钟,默认设置为 10ms
		// 第四个参数 resend为快速重传指标,设置为2
		// 第五个参数 为是否禁用常规流控,这里禁止
		ikcp_nodelay(kcp1, 2, 10, 2, 1);
		ikcp_nodelay(kcp2, 2, 10, 2, 1);
		kcp1->rx_minrto = 10;
		kcp1->fastresend = 1;
	}


	char buffer[2000];
	int hr;

	IUINT32 ts1 = iclock();

	while (1) {
		isleep(1);
		current = iclock();
		ikcp_update(kcp1, iclock());
		ikcp_update(kcp2, iclock());

		// 每隔 20ms,kcp1发送数据
		for (; current >= slap; slap += 20) {
			((IUINT32*)buffer)[0] = index++;
			((IUINT32*)buffer)[1] = current;

			// 发送上层协议包
			ikcp_send(kcp1, buffer, 8);
		}

		// 处理虚拟网络:检测是否有udp包从p1->p2
		while (1) {
			hr = vnet->recv(1, buffer, 2000);
			if (hr < 0) break;
			// 如果 p2收到udp,则作为下层协议输入到kcp2
			ikcp_input(kcp2, buffer, hr);
		}

		// 处理虚拟网络:检测是否有udp包从p2->p1
		while (1) {
			hr = vnet->recv(0, buffer, 2000);
			if (hr < 0) break;
			// 如果 p1收到udp,则作为下层协议输入到kcp1
			ikcp_input(kcp1, buffer, hr);
		}

		// kcp2接收到任何包都返回回去
		while (1) {
			hr = ikcp_recv(kcp2, buffer, 10);
			// 没有收到包就退出
			if (hr < 0) break;
			// 如果收到包就回射
			ikcp_send(kcp2, buffer, hr);
		}

		// kcp1收到kcp2的回射数据
		while (1) {
			hr = ikcp_recv(kcp1, buffer, 10);
			// 没有收到包就退出
			if (hr < 0) break;
			IUINT32 sn = *(IUINT32*)(buffer + 0);
			IUINT32 ts = *(IUINT32*)(buffer + 4);
			IUINT32 rtt = current - ts;
			
			if (sn != next) {
				// 如果收到的包不连续
				printf("ERROR sn %d<->%d\n", (int)count, (int)next);
				return;
			}

			next++;
			sumrtt += rtt;
			count++;
			if (rtt > (IUINT32)maxrtt) maxrtt = rtt;

			printf("[RECV] mode=%d sn=%d rtt=%d\n", mode, (int)sn, (int)rtt);
		}
		if (next > 1000) break;
	}

	ts1 = iclock() - ts1;

	ikcp_release(kcp1);
	ikcp_release(kcp2);

	const char *names[3] = { "default", "normal", "fast" };
	printf("%s mode result (%dms):\n", names[mode], (int)ts1);
	printf("avgrtt=%d maxrtt=%d tx=%d\n", (int)(sumrtt / count), (int)maxrtt, (int)vnet->tx1);
	printf("press enter to next ...\n");
	char ch; scanf("%c", &ch);
}

int main()
{
	test(0);	// 默认模式,类似 TCP:正常模式,无快速重传,常规流控
	test(1);	// 普通模式,关闭流控等
	test(2);	// 快速模式,所有开关都打开,且关闭流控
	return 0;
}

/*
default mode result (20917ms):
avgrtt=740 maxrtt=1507

normal mode result (20131ms):
avgrtt=156 maxrtt=571

fast mode result (20207ms):
avgrtt=138 maxrtt=392
*/


kcp的工作流程是
在这里插入图片描述
kcp是不提供和网络层相关的代码的,是一个纯算法
所以第一步就是封装一个output,需要四个参数,buf len 显而易见是要发送的数据和数据长度

int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
	union { int id; void *ptr; } parameter;
	parameter.ptr = user;
	vnet->send(parameter.id, buf, len);
	return 0;
}

kcp参数基本上用不上,user是相当于上下文,下面代码就是将user传入了结构体,而结构体中保存了数据如:sockfd

// 模拟网络:模拟发送一个 udp包
int udpOutPut(const char *buf, int len, ikcpcb *kcp, void *user){
   
 //  printf("使用udpOutPut发送数据\n");
   
    kcpObj *send = (kcpObj *)user;

	//发送信息
    int n = sendto(send->sockfd, buf, len, 0,(struct sockaddr *) &send->addr,sizeof(struct sockaddr_in));//【】
    if (n >= 0) 
	{       
		//会重复发送,因此牺牲带宽
		//printf("udpOutPut-send: 字节 =%d bytes   内容=[%s]\n", n ,buf+24);//24字节的KCP头部
        return n;
    } 
	else 
	{
        printf("udpOutPut: %d bytes send, error\n", n);
        return -1;
    }
}

主要要使用的ikcp提供的函数有
ikcp_create 创建一个kcp
ikcp_nodelay 配置kcp参数,设置窗口大小,重传模式等
ikcp_update 循环的刷新kcp 这个是kcp的调度函数,不调用他,kcp就不会运作
ikcp_release 释放kcp
ikcp_input 将原始udp数据 写入到kcp中处理(打包头部)
ikcp_recv kcp收到数据 做重组分片,丢掉过去分片,处理ack等功能
ikcp_send kcp发送数据 里面不会调用前面hook的udpOutPut函数,只会处理数据,分片,真正的发udp包得要ikcp_update 调度

kcp源码中分片最大数量被限制为128

const IUINT32 IKCP_WND_RCV = 128;       // must >= max fragment size
if (count >= (int)IKCP_WND_RCV) return -2; //这个地方有问题 限制死了分片的数量

所以要更大的分片要么改代码,要么自己分段,但是自己分段需要自己重组顺序,具体方法可以参考kcp代码
上面的demo代码没有考虑重组
看看源码 在ikcp_recv中

// merge fragment 重组分片,把读出来的数据从seg中删除
	for (len = 0, p = kcp->rcv_queue.next; p != &kcp->rcv_queue; ) {
		int fragment;
		seg = iqueue_entry(p, IKCPSEG, node); 
		p = p->next; //循环遍历rcv_queue,应该是环形队列|?

		if (buffer) {
			memcpy(buffer, seg->data, seg->len);
			buffer += seg->len;
		}

		len += seg->len;
		fragment = seg->frg;

		if (ikcp_canlog(kcp, IKCP_LOG_RECV)) {
		//日志
			ikcp_log(kcp, IKCP_LOG_RECV, "recv sn=%lu", (unsigned long)seg->sn);
		}

		if (ispeek == 0) {
			iqueue_del(&seg->node);
			ikcp_segment_delete(kcp, seg);
			kcp->nrcv_que--;
		}

		if (fragment == 0) 
			break;
	}

这里并没有做排序的事情,看来入队列前排序就做好了,这就就是从队列取出数据拷贝到buffer然后删除队列中的分片


IUINT32 frg;      //分片编号 倒数第几个seg。主要就是用来合并一块被分段的数据。
if (fragment == 0) 
		break;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值