kcp源码剖析+kcp应用层组包 (2)

上文说道分段保持有序,可以先联想一下tcp是怎么保存接受的包有序的
TCP 不同与UDP ,TCP 是有序的,那么是如何保证有序的,数据在发送后,可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同,后发先到是一件很平常的事,网络层是不会保证数据的有序,TCP 是传输层协议,tcp通过字节编号,每一个数据字节都会有一个编号,比如发送了三包,每包100字节,假设第一包首个字节标号是1,那么发送的三包的编号就是 1,101,201,三包数据,只有接收端收到连续的序号的包,才会将数据包提交到应用层例如收到1,201,101,是不会提交到上层应用层的,只有收到正确连续顺序才会提交,所以就保证了数据的有序性。
在ikcp_recv中只用sn是rcv_nxt需要的段序号的时候,才会入队,这就保证了有序性

if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) {
			iqueue_del(&seg->node);
			kcp->nrcv_buf--;
			iqueue_add_tail(&seg->node, &kcp->rcv_queue);
			kcp->nrcv_que++;
			kcp->rcv_nxt++;
		}

完整的ikcp_recv代码

int ikcp_recv(ikcpcb *kcp, char *buffer, int len)
{
	//这个函数是kcp内部的,即应该是要加好数据头的
	struct IQUEUEHEAD *p;
	int ispeek = (len < 0)? 1 : 0;
	int peeksize;
	int recover = 0;
	IKCPSEG *seg;
	assert(kcp);

	if (iqueue_is_empty(&kcp->rcv_queue)){
		// printf("iqueue_is_empty\n");
		return -1;
	}
		

	if (len < 0) len = -len; //为什么会出现负的
	/*	
	计算当前接收队列中的属于同一个消息的数据总长度,
	这个长度应该比参数中的Len小,如果大于,导致数据不能导出
	*/
	peeksize = ikcp_peeksize(kcp);

	if (peeksize < 0) 
		return -2;

	if (peeksize > len) 
		return -3;

	if (kcp->nrcv_que >= kcp->rcv_wnd)
		recover = 1;

	// 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;
	}

	assert(len == peeksize);

	// move available data from rcv_buf -> rcv_queue
	if(iqueue_is_empty(&kcp->rcv_buf)){
		// printf("iqueue_is_empty\n");
	}
	while (! iqueue_is_empty(&kcp->rcv_buf)) {
		seg = iqueue_entry(kcp->rcv_buf.next, IKCPSEG, node);
		if (seg->sn == kcp->rcv_nxt && kcp->nrcv_que < kcp->rcv_wnd) {
			iqueue_del(&seg->node);
			kcp->nrcv_buf--;
			// 1. 根据 sn 确保数据是按序转移到 rcv_queue 中
			// 2. 根据接收窗口大小来判断是否可以接收数据
			iqueue_add_tail(&seg->node, &kcp->rcv_queue);
			kcp->nrcv_que++;
			kcp->rcv_nxt++;
		}	else {
			break;
		}
	}

	// fast recover
	// 快恢复
	if (kcp->nrcv_que < kcp->rcv_wnd && recover) {
		// ready to send back IKCP_CMD_WINS in ikcp_flush
		// tell remote my window size
		kcp->probe |= IKCP_ASK_TELL;
	}

	return len;
}

kcp作者提出了因为一种破除分片数量限制的方式,就是使用stream流模式
应用层组包demo首先要自定义一个协议头,如是否分片,分片的个数
demo没有完成分片的排序,可以学习kcp的做法一个buf一个队列,一段时间变量buf取走需要的次序,因为kcp保证了可靠性,所以最后会收到完整的包。
可能会出现的问题:这个包还没处理完,下一个包已经发来了,可以设置一个编号? 不过如果没有啥实时性的要求,还是不要怎么麻烦用TCP就挺好

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

#include <sys/types.h>
#include <sys/socket.h>
// #include <pthread.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <memory>
#include <sys/time.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <string>
#include <iostream>
#include "test.h"
#include "ikcp.c"


typedef struct {
	const char *ipstr;
	int port;
	
	ikcpcb *pkcp;
	
	int sockfd;
	
	struct sockaddr_in addr;//存放服务器信息的结构体
	struct sockaddr_in CientAddr;//存放客户机信息的结构体
	
	char buff[488];//存放收发的消息
	
}kcpObj;

// 模拟网络
LatencySimulator *vnet;

// 模拟网络:模拟发送一个 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;
    }
}

int initClient(kcpObj *send)
{	
	send->sockfd = socket(AF_INET,SOCK_DGRAM,0);
	
	if(send->sockfd < 0)
	{
		perror("socket error!");
		exit(1);
	}
	
	bzero(&send->addr, sizeof(send->addr));
	
	//设置服务器ip、port
	send->addr.sin_family=AF_INET;
    send->addr.sin_addr.s_addr = inet_addr((char*)send->ipstr);
    send->addr.sin_port = htons(send->port);
	
	printf("sockfd = %d ip = %s  port = %d\n",send->sockfd,send->ipstr,send->port);
	return 1;
}

int initServer(kcpObj *send)
{	
	send->sockfd = socket(AF_INET,SOCK_DGRAM,0);
	
	if(send->sockfd<0)
	{
		perror("socket error!");
		exit(1);
	}
	
	bzero(&send->addr, sizeof(send->addr));
	
	send->addr.sin_family = AF_INET;
	send->addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY
	send->addr.sin_port = htons(send->port);
		
	printf("服务器socket: %d  port:%d\n",send->sockfd,send->port);
	
	if(send->sockfd<0){
		perror("socket error!");
		exit(1);
	}
	
	if(bind(send->sockfd,(struct sockaddr *)&(send->addr),sizeof(struct sockaddr_in))<0)
	{
		perror("bind");
		exit(1);
	}
	return 1;
	
}


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;
}

// 测试用例
// std::shared_ptr<char> my_segment_new(const char *buf, int len){

// }

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

	// 创建两个端点的 kcp对象,第一个参数 conv是会话编号,同一个会话需要相同
	// 最后一个是 user参数,用来传递标识
// -------------------client-----------------
	kcpObj send;
	send.ipstr = "127.0.0.1";
	send.port = 8888;
	
	initClient(&send);//初始化send,主要是设置与服务器通信的套接字对象
	
	bzero(send.buff,sizeof(send.buff));
	// char Msg[] = "Client:Hello!";//与服务器后续交互	
	// memcpy(send.buff,Msg,sizeof(Msg));
// -------------------server-----------------
	kcpObj send2;
	send2.port = 8888;
	send2.pkcp = NULL;
	initServer(&send2);
	
	// bzero(send2.buff,sizeof(send2.buff));
	// char Msg[] = "Server:Hello!";//与客户机后续交互	
	// memcpy(send2.buff,Msg,sizeof(Msg));

// ------------------------------------------

	ikcpcb *kcp1 = ikcp_create(0x11223344, (void *)&send);
	ikcpcb *kcp2 = ikcp_create(0x11223344, (void *)&send2);

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

	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, 129, 129);
	ikcp_wndsize(kcp2, 129, 129);

	//init socket
	

	// 判断测试用例的模式
	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[300000] = "自行准备长度为300000的数据";
		int hr;
	kcpObj *psend=&send;
	kcpObj *psend2=&send2;
	IUINT32 ts1 = iclock();
	std::string str;

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

		// 每隔 20ms,kcp1发送数据
		for (; current >= slap; slap += 10000) {
			int len = 300000;
			char seg[9126];
			char *buffer2 = buffer;
			if(len > 9000){
				int count = (len + 9000 - 1) / 9000;
				for(int i=count;i>=0;--i){
					int size = len > (int)9000 ? (int)9000 : len;
					memcpy(seg+8, buffer2, size);
					((IUINT32*)seg)[0] = 1;//代表分片 开始 结尾 标识
					((IUINT32*)seg)[1] = i;
					
					ikcp_send(kcp1, seg, size+8);
					buffer2 +=size;
					len -= size;
				}
			}else{
				// 发送上层协议包
				int err = ikcp_send(kcp1, buffer, len);
				assert(-2 != err);
			}
		}
		// 处理虚拟网络:检测是否有udp包从p1->p2
		
		unsigned int len = sizeof(struct sockaddr_in);
		
		while (1) {
			char buffer_test[300000];
			hr = recvfrom(psend->sockfd,buffer_test,2800,MSG_DONTWAIT,(struct sockaddr *) &psend->addr,&len);//1 3 9 9不行
			// hr = vnet->recv(1, buffer, 2000);
			if (hr < 0) break;
			// 如果 p2收到udp,则作为下层协议输入到kcp2
			ikcp_input(kcp2, buffer_test, hr);
		}

		// 处理虚拟网络:检测是否有udp包从p2->p1
		while (1) {
			char buffer_test[300000];
			hr = recvfrom(psend2->sockfd,buffer_test,2800,MSG_DONTWAIT,(struct sockaddr *)&psend2->CientAddr,&len);	
			// hr = vnet->recv(0, buffer, 2000);
			if (hr < 0) break;
			// 如果 p1收到udp,则作为下层协议输入到kcp1
			// printf("hr = %d",hr);
			ikcp_input(kcp1, buffer_test, hr);
		}

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

		// kcp1收到kcp2的回射数据
		
		while (1) {
			char buffer_test[300000];
			hr = ikcp_recv(kcp1, buffer_test, 300000);
			// 没有收到包就退出
			if (hr < 0) break;
			IUINT32 flag = *(IUINT32*)(buffer_test + 0);
			IUINT32 index = *(IUINT32*)(buffer_test + 4);
			if(flag == 1){
				//分片
				str.append(buffer_test+8,buffer_test+hr);
			}
			if(index == 0){
				std::cout<<str<<std::endl;
				std::cout<<"str.size()="<<str.size()<<std::endl;
				std::string().swap(str);
			}

		}
		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 \n", (int)(sumrtt / count), (int)maxrtt);
	printf("press enter to next ...\n");
	char ch; scanf("%c", &ch);
}

int main()
{
	// test(0);	// 默认模式,类似 TCP:正常模式,无快速重传,常规流控
	// test(1);	// 普通模式,关闭流控等
	test(2);	// 快速模式,所有开关都打开,且关闭流控
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是在 Unity 中使用 kcp 的示例代码: 首先,需要在 Unity 中导入 kcp 的 C# 实现代码。可以从以下链接下载: https://github.com/xtaci/kcp-csharp 将所有的 .cs 文件添加到 Unity 工程中。 接下来,可以编写一个简单的 kcp 客户端和服务器程序。以下是客户端代码: ```csharp using System; using System.Net; using System.Threading; using KcpClient; public class KcpClientTest : IDisposable { private KcpClient.KcpClient _client; public void Start() { _client = new KcpClient.KcpClient(); _client.Connect(IPAddress.Parse("127.0.0.1"), 12345); new Thread(() => { while (true) { if (_client.Connected) { var data = new byte[1024]; var length = _client.Receive(data, out var remote); if (length > 0) { var message = System.Text.Encoding.UTF8.GetString(data, 0, length); Console.WriteLine($"Received message: {message}"); } } Thread.Sleep(10); } }).Start(); } public void Send(string message) { var data = System.Text.Encoding.UTF8.GetBytes(message); _client.Send(data, data.Length); } public void Dispose() { _client.Dispose(); } } ``` 以下是服务器端代码: ```csharp using System; using System.Net; using System.Threading; using KcpServer; public class KcpServerTest : IDisposable { private KcpServer.KcpServer _server; public void Start() { _server = new KcpServer.KcpServer(); _server.Bind(IPAddress.Any, 12345); new Thread(() => { while (true) { if (_server.ConnectedClients.Count > 0) { foreach (var client in _server.ConnectedClients) { var data = new byte[1024]; var length = client.Receive(data, out var remote); if (length > 0) { var message = System.Text.Encoding.UTF8.GetString(data, 0, length); Console.WriteLine($"Received message: {message}"); client.Send($"Received message: {message}"); } } } Thread.Sleep(10); } }).Start(); } public void Dispose() { _server.Dispose(); } } ``` 在 Unity 中使用以上代码时,需要在场景中添加一个空物体,然后将客户端和服务器端代码分别添加到该物体的脚本组件中。在客户端脚本中,可以调用 `Start` 方法启动客户端,并使用 `Send` 方法发送消息。在服务器端脚本中,调用 `Start` 方法启动服务器端。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值