上文说道分段保持有序,可以先联想一下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;
}