实现UDP可靠性传输(KCP介绍使用)

文章目录

  • 1、TCP协议介绍
    • 1.1、ARQ协议
    • 1.2、停等式
    • 1.3、回退n帧
    • 1.4、选择性重传
    • 1.5、RTT和RTO
    • 1.6、流量控制
    • 1.7、拥塞控制
  • 2、KCP
    • 2.1、KCP介绍
    • 2.2、TCP vs KCP
    • 2.3、KCP使用
    • 2.4、client端
    • 2.5、server端
    • 2.6、KCP协议介绍
    • 2.7、使用流程

1、TCP协议介绍

TCP协议是基于IP协议,面向连接,可靠基于字节流的传输层协议
1、基于IP协议:TCP协议是基于IP协议之上传输的,TCP协议报文中的源端口+IP协议报文中的源地址+TCP协议报文中的目标端口+IP协议报文中的目标地址,组合起来唯一确定一条TCP连接。
2、面向连接:与UDP不同,TCP在传输数据之前,需要进行三次握手,建立一条TCP连接,然后在进行数据传输,释放需要进行四次挥手。
3、基于字节流:流的含义是不间断的数据结构,这里指的是没有边界的报文结构,假如发送内容比较大,TCP协议栈会将数据切成一块一块放入内核中。

实现UDP可靠性传输(KCP介绍使用)_第1张图片
实现UDP可靠性传输(KCP介绍使用)_第2张图片

1.1、ARQ协议

TCP之所以能实现可靠的数据传输,正是因为基于ARQ协议,ARQ协议(Automatic Repeat-reQuest),即自动重传请求,是传输层的纠正协议,在不可靠的网络中实现可靠的信息传输。
ARQ主要有3种模式:
1、停等式
2、回退n帧
3、选择性重传

1.2、停等式

停等式协议工作原理如下:
1、发送方对接收方发送数据包,等待接收方回复ack,并且开始计时
2、在等待过程中发送方停止发送数据
3、当数据包没有成功被接收方接收,接收方是不会发生ack,等待一段时间后,发送方会重新发送数据包
4、反复这个过程直到接收到ack
缺点:较长的等待时间,使发送数据缓慢。

实现UDP可靠性传输(KCP介绍使用)_第3张图片

1.3、回退n帧

为了解决上面的长时间等待ack的缺陷,连续ARQ协议会,连续发送一组数据包,然后会等待这些数据包的ack。

什么是滑动窗口?

发送方和接收方都会维护一个数据帧序列,这个序列被称为窗口,发送方的窗口是由接受方确定的,目的是控制发送方的速度,避免接收方的缓存不够,而导致数据溢出,同时限制网络中的流量,避免网络阻塞,协议中规定,对于窗口内未经确定的分组进行重传。

回退n帧

回退n帧允许发送方在等待超时的间歇,可以继续发送分组,所有分组携带序列号,在GBN协议中,发送方需要响应以下三件事件:
1、上层的调用,上层调用相应send()时,发送方首先要检索发送窗口是否满
2、接收ack,在该协议中,对序号n的分组的确定采取累积确认的方式,表明接收方已正确接收n以前的的所有分组
3、超时,若出现超时,发送方将重传所有已发生但未被确定的分组
下图:序号为2的分组,丢失了,后面的所有分组都需要重新传
GBN采用的技术包括序号、累积确认、检验和以及计时/重传。

实现UDP可靠性传输(KCP介绍使用)_第4张图片

1.4、选择性重传

虽然GBN改善了停等式中等待时间过长的缺陷,但是依旧存在性能问题,而SR协议通过让发送方仅重传在接收时丢失的分组,从而避免不必要的重传。

发送方:

SR协议中发送方需要响应以下三件事:
1、从上层接收数据,当从上层接收数据后,发送方需检查下一个可用于该分组的序号,若序号在窗口中则发送数据。
2、接收ACK。若收到ACK,且该分组在窗口内,则发送方将那个被确认的分组标记为已接收。若该分组序号等于基序号,则窗口序号向前移动到具有最小序号的未确认分组处。若窗口移动后并且有序号落在窗口内的未发送分组,则发送这些分组。
3、超时。若出现超时,发送方将重传已发出但还未确认的分组。与GBN不同的是,
SR协议中的每个分组都有独立的计时器。

接收方:

在SR协议下,接收方需响应以下三种事件:
(假设接收窗口的基序号为4,分组长度也为4)
1、序号在[4,7]内的分组被正确接收。该情况下,收到的分组落在接收方的窗口内,一个ACK
将发送给发送方。若该分组是以前没收到的分组,则被缓存。若该分组的序号等于基序号4,则该分组以及以前缓存的序号连续的分组都交付给上层,然后,接收窗口将向前移动。
2、序号在[0,3]内的分组被正确接收。在该情况下,必须产生一个ACK,尽管该分组是接收方
以前已确认过的分组。若接收方不确认该分组,发送方窗口将不能向前移动。
3、其他情况。忽略该分组对于接收方来说,若一个分组正确接收而不管其是否按序,则接收方会为该分组返回一个ACK给发送方。失序的分组将被缓存,直到所有丢失的分组都被收到,这时才可以将一批分组按序交付给上层。

实现UDP可靠性传输(KCP介绍使用)_第5张图片

1.5、RTT和RTO

RTT

RTT是指数据包从发送端发送出去到接收端收到并发送确认回来所经过的时间。它表示了数据包在网络中传输的延迟,通常以毫秒(ms)为单位。RTT的测量通常通过发送方发送一个数据包,然后在接收到对应的确认回复时计算出来。

RTO

RTO是指在发送方发送数据包后,等待确认回复的超时时间。当发送方发送一个数据包后,它会启动一个定时器,如果在RTO时间内未收到对应的确认回复,发送方会认为数据包已丢失或损坏,并触发重传机制。RTO的值通常是根据过去的RTT值来动态调整的,以适应网络的变化。发送方会维护一个估计的往返时间(Estimated Round-Trip Time,ERTT),并根据ERTT计算出RTO的值。常见的算法是基于加权平均值,如Karn算法或Jacobson/Karels算法

1.6、流量控制

接收方

接收方每次收到数据包,可以在发送确定报文的时候,同时告诉发送方自己的缓存区还剩余多少是空闲的,我们也把缓存区的剩余大小称之为接收窗口大小,用变量win来表示接收窗口的大小。

发送方

发送方收到之后,便会调整自己的发送速率,也就是调整自己发送窗口的大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据,防止出现大量丢包情况的发生。

实现UDP可靠性传输(KCP介绍使用)_第6张图片
流量控制-发送方何时再继续发送数据?

当发送方停止发送数据后,该怎样才能知道自己可以继续发送数据?
1、当接收方处理好数据,接受窗口 win > 0 时,接收方发个通知报文去通知发送方,告诉他可以继续发送数据了。当发送方收到窗口大于0的报文时,就继续发送数据。
2、当发送方收到接受窗口 win = 0 时,这时发送方停止发送报文,并且同时开启一个定时器,每隔一段时间就发个测试报文去询问接收方,打听是否可以继续发送数据了,如果可以,接收方就告诉他此时接受窗口的大小;如果接受窗口大小还是为0,则发送方再次刷新启动定时器。

实现UDP可靠性传输(KCP介绍使用)_第7张图片
流量控制-小结

  1. 通信的双方都拥有两个滑动窗口,一个用于接受数据,称之为接收窗口;一个用于发送数据,称之为拥塞窗口(即发送窗口)。指出接受窗口大小的通知我们称之为窗口通告。
    2、接收窗口的大小固定吗?接受窗口的大小是根据某种算法动态调整的。
    3、接收窗口越大越好吗?当接收窗口达到某个值的时候,再增大的话也不怎么会减少丢包率的了,而且还会更加消耗内存。所以接收窗口的大小必须根据网络环境以及发送发的的拥塞窗口来动态调整。
    4、发送窗口和接受窗口相等吗?接收方在发送确认报文的时候,会告诉发送发自己的接收窗口大小,而发送方的发送窗口会据此来设置自己的发送窗口,但这并不意味着他们就会相等。首先接收方把确认报文发出去的那一刻,就已经在一边处理堆在自己缓存区的数据了,所以一般情况下接收窗口 >= 发送窗口。

1.7、拥塞控制

大家可能都听说过拥塞控制和流量控制,想必也有一些人可能还分不清拥塞控制和流量控制,进而把他们当作一回事。拥塞控制和流量控制虽然采取的动作很相似,但拥塞控制与网络的拥堵情况相关联,而流量控制与接收方的缓存状态相关联。也就是说,拥塞控制和流量控制是针对完全不同的问题而采取的措施。今天这篇文章,我们先来讲讲拥塞控制。

链接: 5分钟读懂拥塞控制

2、KCP

2.1、KCP介绍

KCP是一种网络传输协议(A Fast and Reliable ARQ Protocol),可以视它为TCP的代替品,但是它运行于用户空间,它不管底层的发送与接收,只是个纯算法实现可靠传输,它的特点是牺牲带宽来降低延迟。因为TCP协议的大公无私,经常牺牲自己速度来减少网络拥塞,它是从大局上考虑的。而KCP是自私的,它只顾自己的传输效率,从不管整个网络的拥塞情况。举个例子,TCP检测到丢包的时候,首先想到的是网络拥塞了,要放慢自己的速度别让网络更糟,而KCP想到的赶紧重传别耽误事。

TCP

TCP的特点是可靠传输(累积确认、超时重传、选择确认)、流量控制(滑动窗口)、拥塞控制(慢开始、拥塞避免、快重传、快恢复)、面向连接。KCP对这些参数基本都可配,也没用建立/关闭连接的过程

KCP

其实KCP并不神秘,因为TCP的高度自治(很多东西都不可配),满足不了如今各种速度需求。而KCP就是基于UDP协议,再将一些TCP经典的机制移植过来,变成参数可配。

2.2、TCP vs KCP

以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度
1、RTO翻倍vs不翻倍

TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,十分恐怖,而KCP启
动快速模式后不x2,只是x1.5(实验证明1.5这个值相对比较好),提高了传输速度。 200 300 450 675 – 200 400 800 1600

2、选择性重传 vs 全部重传

TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。

3、快速重传(跳过多少个包马上重传)(如果使用了快速重传,可以不考虑RTO))

发送端发送了1,2,3,4,5几个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号包,大大改善了丢包时的传输速度。 fastresend=2

4、延迟ACK vs 非延迟ACK

TCP为了充分利用带宽,延迟发送ACK(NODELAY都没用),这样超时计算会算出较大 RTT时间,延长了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。

5、UNA vs ACK+UNA

ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光用UNA将导致全部重传,光用ACK则丢失成本太高,以往协议都是二选其一,而 KCP协议中,除去单独的 ACK包外,所有包都有UNA信息。

6、非退让流控

KCP正常模式同TCP一样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的小数据时,可选择通过配置跳过后两步,仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。

2.3、KCP使用

1、创建KCP对象

// 初始化 kcp对象,conv为一个表示会话编号的整数,和tcp的 conv一样,通信双
// 方需保证 conv相同,相互的数据包才能够被认可,user是一个给回调函数的指针
ikcpcb *kcp = ikcp_create(conv, user);

2、设置回调函数

// KCP的下层协议输出函数,KCP需要发送数据时会调用它
// buf/len 表示缓存和长度
// user指针为 kcp对象创建时传入的值,用于区别多个 KCP对象
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{

}
// 设置回调函数
kcp->output = udp_output;

3、循环调用update

// 以一定频率调用 ikcp_update来更新 kcp状态,并且传入当前时钟(毫秒单位)
// 如 10ms调用一次,或用 ikcp_check确定下次调用 update的时间不必每次调用
ikcp_update(kcp, millisec);

4、输入一个下层数据包

// 收到一个下层数据包(比如UDP包)时需要调用:
ikcp_input(kcp, received_udp_packet, received_udp_size);
处理了下层协议的输出/输入后 KCP协议就可以正常工作了,使用 ikcp_send 来向 远端发送数据。而另一端使用 ikcp_recv(kcp, ptr, size)来接收数据。

协议配置
协议默认模式是一个标准的 ARQ,需要通过配置打开各项加速开关:
1、工作模式:

int ikcp_nodelay(ikcpcb *kcp, int nodelay, int interval, int resend, int nc)
nodelay :是否启用 nodelay模式,0不启用;1启用。
interval :协议内部工作的 interval,单位毫秒,比如 10ms或者 20ms
resend :快速重传模式,默认0关闭,可以设置2(2次ACK跨越将会直接重传)
nc :是否关闭流控,默认是0代表不关闭,1代表关闭。
普通模式: ikcp_nodelay(kcp, 0, 40, 0, 0);
极速模式: ikcp_nodelay(kcp, 1, 10, 2, 1);

2、最大窗口:

int ikcp_wndsize(ikcpcb *kcp, int sndwnd, int rcvwnd);
该调用将会设置协议的最大发送窗口和最大接收窗口大小,默认为32. 这个可以理解为 TCP的 SND_BUF 和 RCV_BUF,只不过单位不一样 SND/RCV_BUF 单位是字节,这个单位是包。

3、最大传输单元:

纯算法协议并不负责探测 MTU,默认 mtu是1400字节,可以使用ikcp_setmtu来设置该值。该值将会影响数据包归并及分片时候的最大传输单元。

4、最小RTO:

不管是 TCP还是 KCP计算 RTO时都有最小 RTO的限制,即便计算出来RTO为40ms,由于默认的 RTO是100ms,协议只有在100ms后才能检测到丢包,快速模式下为30ms,可以手动更改该值:
kcp->rx_minrto = 10;

2.4、client端

delay.h

#include 
#include 
#include 



#define DELAY_BODY_SIZE 1300
typedef struct delay_obj
{
    uint16_t seqno; // 序列号
    int64_t send_time;  // 发送时间
    int64_t recv_time;  // 回来时间
    uint8_t body[DELAY_BODY_SIZE];
}t_delay_obj;

int64_t iclock64();
uint32_t iclock();
t_delay_obj *delay_new();
void delay_set_seqno(t_delay_obj *obj, uint16_t seqno);
void delay_set_seqno_send_time(t_delay_obj *obj, uint16_t seqno);
void delay_set_send_time(t_delay_obj *obj);
void delay_set_recv_time(t_delay_obj *obj);
void delay_print_rtt_time(t_delay_obj *objs, int num);


delay.c

#include "delay.h"
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 

/* get system time */
void itimeofday(long *sec, long *usec)
{
	#if defined(__unix)
	struct timeval time;
	gettimeofday(&time, NULL);
	if (sec) *sec = time.tv_sec;
	if (usec) *usec = time.tv_usec;
	#else
	static long mode = 0, addsec = 0;
	int retval;
	static int64_t freq = 1;
	int64_t qpc;
	if (mode == 0) {
		retval = QueryPerformanceFrequency((LARGE_INTEGER*)&freq);
		freq = (freq == 0)? 1 : freq;
		retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
		addsec = (long)time(NULL);
		addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
		mode = 1;
	}
	retval = QueryPerformanceCounter((LARGE_INTEGER*)&qpc);
	retval = retval * 2;
	if (sec) *sec = (long)(qpc / freq) + addsec;
	if (usec) *usec = (long)((qpc % freq) * 1000000 / freq);
	#endif
}

/* get clock in millisecond 64 */
int64_t iclock64(void)
{
	long s, u;
	int64_t value;
	itimeofday(&s, &u);
	value = ((int64_t)s) * 1000 + (u / 1000);
	return value;
}

uint32_t iclock()
{
	return (uint32_t)(iclock64() & 0xfffffffful);
}


inline t_delay_obj *delay_new() {
    t_delay_obj *obj =  (t_delay_obj *)malloc(sizeof(t_delay_obj));
    if(!obj) {
        return NULL;
    }
    obj->seqno = 0;
    obj->send_time = 0;
    obj->recv_time = 0;
}

inline void delay_set_seqno(t_delay_obj *obj, uint16_t seqno) {
    obj->seqno = seqno;
}

inline void delay_set_seqno_send_time(t_delay_obj *obj, uint16_t seqno) {
    obj->seqno = seqno;
    obj->send_time = iclock64();
}

inline void delay_set_send_time(t_delay_obj *obj) {
    obj->send_time = iclock64();
}

inline void delay_set_recv_time(t_delay_obj *obj) {
    obj->recv_time = iclock64();
}

inline void delay_print_rtt_time(t_delay_obj *objs, int num) {
    for(int i = 0; i < num; i++) {
        t_delay_obj *obj = &(objs[i]);
        printf("%04d seqno:%d rtt  :%ldms\n", i, obj->seqno, obj->recv_time - obj->send_time);
		// printf("%04d seqno:%d snd_t:%ldms\n", i, obj->seqno, obj->send_time);
		// printf("%04d seqno:%d rcv_t:%ldms\n", i, obj->seqno, obj->recv_time);
    }
}


client.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "ikcp.h"

#include 
#include 
#include 


#include "delay.h"
#define DELAY_TEST2_N 1
#define UDP_RECV_BUF_SIZE 1500

// 编译:  gcc -o client client.c ikcp.c delay.c  -lpthread


 typedef struct {
	unsigned char *ipstr;
	int port;
	
	ikcpcb *pkcp;
	
	int sockfd;
	struct sockaddr_in addr;//存放服务器的结构体
	
	char buff[UDP_RECV_BUF_SIZE];//存放收发的消息
}kcpObj;


/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
	#ifdef __unix 	/* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
	#elif defined(_WIN32)
	Sleep(millisecond);
	#endif
}



int udp_output(const char *buf, int len, ikcpcb *kcp, void *user){
   
 //  printf("使用udp_output发送数据\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("send:%d bytes\n", n);//24字节的KCP头部
        return n;
    } 
	else 
	{
        printf("udp_output: %d bytes send, error\n", n);
        return -1;
    }
}


int init(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);
	
}

// 特别说明,当我们使用kcp测试rtt的时候,如果发现rtt过大,很大一种可能是分片数据没有及时发送出去,需要调用ikcp_flush更快速将分片发送出去。
void delay_test2(kcpObj *send) {
    // 初始化 100个 delay obj
    char buf[UDP_RECV_BUF_SIZE];
	unsigned int len = sizeof(struct sockaddr_in);

    size_t obj_size = sizeof(t_delay_obj);
    t_delay_obj *objs = malloc(DELAY_TEST2_N * sizeof(t_delay_obj));
	int ret = 0;

	int recv_objs = 0;
	//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
 	ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
    for(int i = 0; i < DELAY_TEST2_N; i++) {
		//  isleep(1);
		delay_set_seqno_send_time(&objs[i], i);  
		ret = ikcp_send(send->pkcp, (char *) &objs[i], obj_size); 
        if(ret < 0) {
            printf("send %d seqno:%u failed, ret:%d, obj_size:%ld\n", i, objs[i].seqno, ret, obj_size);
            return;
        } 
        // ikcp_flush(send->pkcp);		// 调用flush能更快速把分片发送出去
		//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
		
		int n = recvfrom(send->sockfd, buf, UDP_RECV_BUF_SIZE, MSG_DONTWAIT,(struct sockaddr *) &send->addr,&len);
		// printf("print recv1:%d\n", n);
		if(n < 0) {//检测是否有UDP数据包 
			// isleep(1);
			continue;
		}
		ret = ikcp_input(send->pkcp, buf, n);	// 从 linux api recvfrom先扔到kcp引擎
		if(ret < 0)//检测ikcp_input是否提取到真正的数据
		{
			//printf("ikcp_input ret = %d\n",ret);
			continue;			// 没有读取到数据
		}	
		ret = ikcp_recv(send->pkcp, (char *)&objs[i], obj_size);		
		if(ret < 0)//检测ikcp_recv提取到的数据	
		{
			printf("ikcp_recv1 ret = %d\n",ret);
			continue;
		}
		delay_set_recv_time(&objs[recv_objs]);
		recv_objs++;
		printf("recv1 %d seqno:%d, ret:%d\n", recv_objs, objs[i].seqno, ret);
        if(ret != obj_size) {
            printf("recv1 %d seqno:%d failed, size:%d\n", i, objs[i].seqno, ret);
            delay_print_rtt_time(objs, i);
            return;
        }
    }

	// 还有没有发送完毕的数据
	for(int i = recv_objs; i < DELAY_TEST2_N; ) {
		//  isleep(1);
		//ikcp_update包含ikcp_flush,ikcp_flush将发送队列中的数据通过下层协议UDP进行发送
		ikcp_update(send->pkcp,iclock());//不是调用一次两次就起作用,要loop调用
		//ikcp_flush(send->pkcp);		// 调用flush能更快速把分片发送出去  
		int n = recvfrom(send->sockfd, buf, UDP_RECV_BUF_SIZE, MSG_DONTWAIT,(struct sockaddr *) &send->addr,&len);
		// printf("recv2:%d\n", n);
		if(n < 0) {//检测是否有UDP数据包
			// printf("recv2:%d\n", n);
			isleep(1);
			continue;
		}
			
		ret = ikcp_input(send->pkcp, buf, n);	
		if(ret < 0)//检测ikcp_input是否提取到真正的数据
		{
			printf("ikcp_input2 ret = %d\n",ret);
			continue;			// 没有读取到数据
		}	
		ret = ikcp_recv(send->pkcp, (char *)&objs[i], obj_size);		
		if(ret < 0)//检测ikcp_recv提取到的数据	
		{
			// printf("ikcp_recv2 ret = %d\n",ret);
			continue;
		}
		printf("recv2 %d seqno:%d, ret:%d\n", recv_objs,  objs[i].seqno, ret);
		delay_set_recv_time(&objs[recv_objs]);
		recv_objs++;
		i++;
        if(ret != obj_size) {
            printf("recv2 %d seqno:%d failed, size:%d\n", i, objs[i].seqno, ret);
            delay_print_rtt_time(objs, i);
            return;
        }
        
	}
	ikcp_flush(send->pkcp);

    delay_print_rtt_time(objs, DELAY_TEST2_N);
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n,ret;

	// while(1)
	{
		isleep(1);
		delay_test2(send);
	}
	printf("loop finish\n");
	close(send->sockfd);
	
}

int main(int argc,char *argv[])
{
	//printf("this is kcpClient,请输入服务器 ip地址和端口号:\n");
	if(argc != 3)
	{
		printf("请输入服务器ip地址和端口号\n");
		return -1;
	}
	printf("this is kcpClient\n");
	int64_t cur =  iclock64();
    printf("main started t:%ld\n", cur); // prints Hello World!!!
	unsigned char *ipstr = (unsigned char *)argv[1];
	unsigned char *port  = (unsigned char *)argv[2];
	
	kcpObj send;
	send.ipstr = ipstr;
	send.port = atoi(argv[2]);
	
	init(&send);//初始化send,主要是设置与服务器通信的套接字对象
	
	bzero(send.buff,sizeof(send.buff));
	
	// 每个连接都是需要对应一个ikcpcb
	ikcpcb *kcp = ikcp_create(0x1, (void *)&send);//创建kcp对象把send传给kcp的user变量
	kcp->output = udp_output;//设置kcp对象的回调函数
	ikcp_nodelay(kcp,0, 10, 0, 0);//(kcp1, 0, 10, 0, 0); 1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);
	ikcp_setmtu(kcp, 1400);
	send.pkcp = kcp;	
	loop(&send);//循环处理
	ikcp_release(send.pkcp);
	printf("main finish t:%ldms\n", iclock64() - cur);
	return 0;	
}

2.5、server端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "ikcp.h"
#define RECV_BUF 1500

static int number = 0;

typedef struct
{
	unsigned char *ipstr;
	int port;

	ikcpcb *pkcp;

	int sockfd;

	struct sockaddr_in addr;	  //存放服务器信息的结构体
	struct sockaddr_in CientAddr; //存放客户机信息的结构体

	char buff[RECV_BUF]; //存放收发的消息

} kcpObj;
// 编译:  gcc -o server server.c ikcp.c  
// 特别需要注意,这里的服务器端也只能一次使用,即是等客户端退出后,服务端也要停止掉再启动
// 之所以是这样,主要是因为sn的问题,比如客户端第一次启动 sn 0~5, 第二次启动发送的sn还是0 ~5 如果服务器端不停止则自己以为0~5已经收到过了就不会回复。

// 在真正使用的时候,还需要另外的通道让客户端和服务器端之前重新创建ikcpcb,以匹配ikcpcb的conv
/* get system time */
void itimeofday(long *sec, long *usec)
{
#if defined(__unix)
	struct timeval time;
	gettimeofday(&time, NULL);
	if (sec)
		*sec = time.tv_sec;
	if (usec)
		*usec = time.tv_usec;
#else
	static long mode = 0, addsec = 0;
	BOOL retval;
	static IINT64 freq = 1;
	IINT64 qpc;
	if (mode == 0)
	{
		retval = QueryPerformanceFrequency((LARGE_INTEGER *)&freq);
		freq = (freq == 0) ? 1 : freq;
		retval = QueryPerformanceCounter((LARGE_INTEGER *)&qpc);
		addsec = (long)time(NULL);
		addsec = addsec - (long)((qpc / freq) & 0x7fffffff);
		mode = 1;
	}
	retval = QueryPerformanceCounter((LARGE_INTEGER *)&qpc);
	retval = retval * 2;
	if (sec)
		*sec = (long)(qpc / freq) + addsec;
	if (usec)
		*usec = (long)((qpc % freq) * 1000000 / freq);
#endif
}

/* get clock in millisecond 64 */
IINT64 iclock64(void)
{
	long s, u;
	IINT64 value;
	itimeofday(&s, &u);
	value = ((IINT64)s) * 1000 + (u / 1000);
	return value;
}

IUINT32 iclock()
{
	return (IUINT32)(iclock64() & 0xfffffffful);
}

int64_t first_recv_time = 0;
/* sleep in millisecond */
void isleep(unsigned long millisecond)
{
#ifdef __unix /* usleep( time * 1000 ); */
	struct timespec ts;
	ts.tv_sec = (time_t)(millisecond / 1000);
	ts.tv_nsec = (long)((millisecond % 1000) * 1000000);
	/*nanosleep(&ts, NULL);*/
	usleep((millisecond << 10) - (millisecond << 4) - (millisecond << 3));
#elif defined(_WIN32)
	Sleep(millisecond);
#endif
}

int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{

	kcpObj *send = (kcpObj *)user;

	//发送信息
	int n = sendto(send->sockfd, buf, len, 0, (struct sockaddr *)&send->CientAddr, sizeof(struct sockaddr_in));
	if (n >= 0)
	{
		//会重复发送,因此牺牲带宽
		printf("send: %d bytes, t:%lld\n", n, iclock64() - first_recv_time); //24字节的KCP头部
		return n;
	}
	else
	{
		printf("error: %d bytes send, error\n", n);
		return -1;
	}
}

int init(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);
	}
}

void loop(kcpObj *send)
{
	unsigned int len = sizeof(struct sockaddr_in);
	int n, ret;
	//接收到第一个包就开始循环处理
	int recv_count = 0;

	isleep(1);
	ikcp_update(send->pkcp, iclock());

	char buf[RECV_BUF] = {0};

	while (1)
	{
		isleep(1);
		ikcp_update(send->pkcp, iclock());
		//处理收消息
		n = recvfrom(send->sockfd, buf, RECV_BUF, MSG_DONTWAIT, (struct sockaddr *)&send->CientAddr, &len);
		if (n > 0)
		{
			printf("UDP recv[%d]  size= %d   \n", recv_count++, n);
			if (first_recv_time == 0)
			{
				first_recv_time = iclock64();
			}
			//预接收数据:调用ikcp_input将裸数据交给KCP,这些数据有可能是KCP控制报文,并不是我们要的数据。
			//kcp接收到下层协议UDP传进来的数据底层数据buffer转换成kcp的数据包格式
			ret = ikcp_input(send->pkcp, buf, n);
			if (ret < 0)
			{
				continue;
			}
			//kcp将接收到的kcp数据包还原成之前kcp发送的buffer数据
			ret = ikcp_recv(send->pkcp, buf, n); //从 buf中 提取真正数据,返回提取到的数据大小
			if (ret < 0)
			{ // 没有检测ikcp_recv提取到的数据
				isleep(1);
				continue;
			}
			int send_size = ret;
			//ikcp_send只是把数据存入发送队列,没有对数据加封kcp头部数据
			//应该是在kcp_update里面加封kcp头部数据
			//ikcp_send把要发送的buffer分片成KCP的数据包格式,插入待发送队列中。
			ret = ikcp_send(send->pkcp, buf, send_size);
			printf("Server reply ->  bytes[%d], ret = %d\n", send_size, ret);
			ikcp_flush(send->pkcp);	// 快速flush一次 以更快让客户端收到数据
			number++;
		}
		else if (n == 0)
		{
			printf("finish loop\n");
			break;
		}
		else
		{
			// printf("n:%d\n", n);
		}
	}
}

int main(int argc, char *argv[])
{
	printf("this is kcpServer\n");
	if (argc < 2)
	{
		printf("请输入服务器端口号\n");
		return -1;
	}

	kcpObj send;
	send.port = atoi(argv[1]);
	send.pkcp = NULL;

	bzero(send.buff, sizeof(send.buff));
	char Msg[] = "Server:Hello!"; //与客户机后续交互
	memcpy(send.buff, Msg, sizeof(Msg));

	ikcpcb *kcp = ikcp_create(0x1, (void *)&send); //创建kcp对象把send传给kcp的user变量
	ikcp_setmtu(kcp, 1400);
	kcp->output = udp_output;		//设置kcp对象的回调函数
	ikcp_nodelay(kcp, 0, 10, 0, 0); //1, 10, 2, 1
	ikcp_wndsize(kcp, 128, 128);

	send.pkcp = kcp;

	init(&send); //服务器初始化套接字
	loop(&send); //循环处理

	return 0;
}

2.6、KCP协议介绍

实现UDP可靠性传输(KCP介绍使用)_第8张图片

conv :连接号。UDP是⽆连接的,conv⽤于表示来⾃于哪个客户端。对连接的⼀种替代, 因为有 conv , 所
以KCP也是⽀持多路复⽤的。
cmd :命令类型,只有四种
frg :分⽚,⽤户数据可能会被分成多个KCP包,发送出去
在 xtaci/kcp-go 的实现中,这个字段始终为0,以及没有意义了, 详⻅issues/121
wnd :接收窗⼝⼤⼩,发送⽅的发送窗⼝不能超过接收⽅给出的数值, (其实是接收窗⼝的剩余⼤⼩,这个
⼤⼩是动态变化的)
ts : 时间序列
sn : 序列号
una :下⼀个可接收的序列号。其实就是确认号,收到sn=10的包,una为11
len :数据⻓度(DATA的⻓度)
data :⽤户数据

2、CMD四种类型

IKCP_CMD_PUSH 数据推送命令
IKCP_CMD_ACK 确认命令
IKCP_CMD_WASK 接收窗⼝⼤⼩询问命令
IKCP_CMD_WINS 接收窗⼝⼤⼩告知命令

IKCP_CMD_PUSH 和 IKCP_CMD_ACK 关联
IKCP_CMD_WASK 和 IKCP_CMD_WINS 关联

2.7、使用流程

实现UDP可靠性传输(KCP介绍使用)_第9张图片
实现UDP可靠性传输(KCP介绍使用)_第10张图片
实现UDP可靠性传输(KCP介绍使用)_第11张图片

  1. 创建 KCP对象:ikcpcb *kcp = ikcp_create(conv, user);
  2. 设置传输回调函数(如UDP的send函数):kcp->output = udp_output;
  3. 真正发送数据需要调用sendto
  4. 循环调用 update:ikcp_update(kcp, millisec);
  5. 输入一个应用层数据包(如UDP收到的数据包):
    ikcp_input(kcp,received_udp_packet,received_udp_size);
  6. 我们要使用recvfrom接收,然后扔到kcp里面做解析
  7. 发送数据:ikcp_send(kcp1, buffer, 8); 用户层接口
  8. 接收数据:hr = ikcp_recv(kcp2, buffer, 10);

你可能感兴趣的:(Linux,系统编程,udp,网络,php)