【SZU计算机网络实验】从rdt到GBN,这实验居然实现了TCP的可靠数据传输机制?

前言

一个实验六个任务,实验文档一划划不到底。。看来老师们是真下功夫了啊

本文主要展示了作者在完成SZU计算机网络实验3的思路及过程,实验主要包括:

  1. 理解rdt2.1
  2. 实现rdt2.2
  3. 实现rdt3.0
  4. 实现回退N步(GBN)机制
  5. 实现面向无连接的可靠传输机制(GBN)
  6. 进行量化分析

文中出现的状态机演示图均基于mermaid。在本文中,过渡文字中第一行表示事件,第二行之后表示动作

参考资料:

实验文档:计算机网络课程综合实验平台 (snrc.site)

一、理解rdt2.1

0. 理解rdt1.0和rdt2.0

在理解rdt2.1之前,我们需要先了解rdt2.1出现的背景,才能知道其解决的问题

rdt全称reliable data transfer,即可靠数据传输

由于网络层是不可靠传输,而位于网络层之上的传输层中的TCP,试图为上层提供可靠的传输

1) rdt1.0

rdt1.0作为第一代的rdt模型,它假设底层信道(网络层及其以下层)是可靠的,即传输层的这一端到另一端之间,它们都是按序到达,且不会出现数据的损坏和丢包

因而rdt1.0的发送端和接收端的状态机的表示如下

发送端

rdt_send(data)
packet=make_pkt(data) udt_send(packet)
Wait for call from above

rdt_send(data)表示上层(应用层)发送数据到该层

packet=make_pkt(data)表示将数据封装成数据包,udt_send(packet)将数据包传递给下层

接收端

rdt_rcv(packet)
extract(packet, data) deliver_data(data)
Wait for call from below

rdt_rcv(packet)表示从下层接收到数据包

extract(packet, data)表示从数据包中提取出数据,deliver_data(data)将数据分发到上层

2) rdt2.0

rdt1.0过于理想,实际上发送的数据包很可能在传输过程中,出现比特差错

那么接收端就需要对数据包做差错检测。如果数据包没有受损,则反馈给发送端一个ACK;若数据包受损,则反馈给发送端一个NAK

而发送端在发送数据包后等待接收端的反馈,根据反馈是ACK还是NAK选择进入下一个数据包的发送,或是重传原来的数据包

rdt2.0的状态机如下:

发送端

rdt_send(data)
sndpkt=make_pkt(data, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && isACK(rcvpkt)
rdt_rcv(rcvpkt) && isNAK(rcvpkt)
udt_send(sndpkt)
Wait for call from above
Wait for ACK or NAK

可以发现在构造数据包时,多引入了一个变量checksum即校验码:sndpkt=make_pkt(data, checksum)

接收端

rdt_rcv(rcvpkt) && notcorrupt(rcvpkt)
extract(packet, data) deliver_data(data) sndpkt=make_pkt(ACK) udt_send(sndpkt)
rdt_rcv(rcvpkt) && corrupt(rcvpkt)
sndpkt=make_pkt(NAK) udt_send(sndpkt)
Wait for call from below

接收到数据包时,利用notcorrupt(rcvpkt)和corrupt(rcvpkt)判断数据包受损与否

若未受损,构造ACK数据包并反馈:sndpkt=make_pkt(ACK) udt_send(sndpkt)

若受损,构造NAK数据包并反馈:sndpkt=make_pkt(NAK) udt_send(sndpkt)

1. 引入rdt2.1

1) rdt2.1的状态转移

在rdt2.0中,通过引入ACK/NAK解决了发送的数据包可能存在比特差错的问题

但是,接收端反馈的ACK/NAK数据包也可能在传输过程中出现比特差错,这就需要在接收端构造ACK/NAK数据包时也使用checksum校验码,并且在发送端对ACK/NAK数据包进行校验。

这就导致了一种情况:当发送的数据包成功抵达接收方,而接收方发送的ACK出现比特差错时,发送方需要重传原来的数据包,而接收方无法识别这是新的数据包还是原来的数据包的重传

因此需要为每个数据包标号0/1:

当发送端发送数据包0,进入等待ACK/NAK的状态;数据包0成功抵达接收端,接收端发送一个ACK并进入等待数据包1的状态;但是该ACK出现比特差错,发送端需要重传数据包0,继续等待ACK/NAK,接收端接收到数据包0,知道是重复的数据包,返回一个ACK并继续等待数据包1

故rdt2.1的状态机如下以及:

发送端

rdt_send(data)
sndpkt=make_pkt(0, data, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isNAK(rcvpkt))
udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt)
rdt_send(data)
sndpkt=make_pkt(1, data, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isNAK(rcvpkt))
udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt)
Wait for call 0 from above
Wait for ACK or NAK 0
Wait for call 1 from above
Wait for ACK or NAK 1

与rdt2.0相比,rdt2.1在make_pkt中新增了一个参数,值为0/1:

sndpkt=make_pkt(0, data, checksum)
sndpkt=make_pkt(1, data, checksum)

在等待ACK/NAK的状态中,当接收包损坏或为NAK时重发,只有接收包未损坏且为ACK时才进入下一数据包的发送:

corrupt(rcvpkt) || isNAK(rcvpkt)
notcorrupt(rcvpkt) && isACK(rcvpkt)

接收端

rdt_rcv(rcvpkt) && corrupt(rcvpkt)
sndpkt=make_pkt(NAK, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq1(rcvpkt)
sndpkt=make_pkt(ACK, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq0(rcvpkt)
extract(packet, data) deliver_data(data)
sndpkt=make_pkt(ACK, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && corrupt(rcvpkt)
sndpkt=make_pkt(NAK, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq0(rcvpkt)
sndpkt=make_pkt(ACK, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq1(rcvpkt)
extract(packet, data) deliver_data(data)
sndpkt=make_pkt(ACK, checksum) udt_send(sndpkt)
Wait for 0 call from below
Wait for 1 call from below

与rdt2.0相比,rdt2.1在make_pkt中增加了一个checksum:

sndpkt=make_pkt(ACK, checksum)
sndpkt=make_pkt(NAK, checksum)

在Wait for 0 call from below状态中,只有接收到未损坏且序列号为0的数据包,才会提取并分发数据并发送一个ACK,跳转到Wait for 1 call from below状态;接收到未损坏且序列号为1的数据包时,认定其为重复,不提取分发,但是发送一个ACK;接收到损坏的数据包时,发送一个NAK

2) rdt2.1的代码实例

在c语言中利用一个while循环+switch分支模拟状态机的运行,代码如下:

发送端

void sending_packets()
{// 初始化状态Sender_State currentState = STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE;int seq = 0;char *data;Packet *rcvpkt;boolean finish_send = FALSE;// get start_timeunsigned long start_time = GetTickCount();while (!finish_send){switch (currentState){case STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE:printf("STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE\n");data = rdt_send(seq);sndpkt = make_pkt(seq, PACKET_TYPE_DATA, data);udt_send(sockfd, sndpkt, &client_addr);seq++;currentState = STATE_WAIT_ACK_NAK_EVEN;break;case STATE_WAIT_ACK_NAK_EVEN:printf("STATE_WAIT_ACK_NAK_EVEN\n");rcvpkt = rdt_rcv(sockfd, &client_addr);if (corrupt(rcvpkt) || isNAK(rcvpkt)){udt_send(sockfd, sndpkt, &client_addr);}else if (notcorrupt(rcvpkt) && isACK(rcvpkt)){free(sndpkt);currentState = STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE;}free(rcvpkt);break;case STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE:printf("STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE\n");data = rdt_send(seq);sndpkt = make_pkt(seq, PACKET_TYPE_DATA, data);udt_send(sockfd, sndpkt, &client_addr);seq++;currentState = STATE_WAIT_ACK_NAK_ODD;break;case STATE_WAIT_ACK_NAK_ODD:printf("STATE_WAIT_ACK_NAK_ODD\n");rcvpkt = rdt_rcv(sockfd, &client_addr);if (corrupt(rcvpkt) || isNAK(rcvpkt)){udt_send(sockfd, sndpkt, &client_addr);}else if (notcorrupt(rcvpkt) && isACK(rcvpkt)){free(sndpkt);currentState = STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE;if (seq == TOTAL_PACKETS)finish_send = TRUE;}free(rcvpkt);}}
}

接收端

void receiving_packets(){Packet *rcvpkt;Packet* sndpkt;int rcv_seq = -1;// 初始化状态Receiver_State currentState = STATE_WAIT_FOR_EVEN_FROM_BELOW; while (TRUE){switch (currentState){case STATE_WAIT_FOR_EVEN_FROM_BELOW:printf("STATE_WAIT_FOR_EVEN_FROM_BELOW\n");rcvpkt = rdt_rcv(sockfd, &server_addr);if (notcorrupt(rcvpkt) && is_seq_even(rcvpkt)){extract_data(rcvpkt);rcv_seq = rcvpkt->seq;sndpkt = make_pkt(rcv_seq, PACKET_TYPE_ACK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);currentState = STATE_WAIT_FOR_ODD_FROM_BELOW;}else if (corrupt(rcvpkt)){sndpkt = make_pkt(rcv_seq, PACKET_TYPE_NAK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);}else if (notcorrupt(rcvpkt) && is_seq_odd(rcvpkt)){sndpkt = make_pkt(rcv_seq, PACKET_TYPE_ACK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);}case STATE_WAIT_FOR_ODD_FROM_BELOW:printf("STATE_WAIT_FOR_ODD_FROM_BELOW\n");rcvpkt = rdt_rcv(sockfd, &server_addr);if (notcorrupt(rcvpkt) && is_seq_odd(rcvpkt)){extract_data(rcvpkt);rcv_seq = rcvpkt->seq;sndpkt = make_pkt(rcv_seq, PACKET_TYPE_ACK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);currentState = STATE_WAIT_FOR_EVEN_FROM_BELOW;}else if (corrupt(rcvpkt)){sndpkt = make_pkt(rcv_seq, PACKET_TYPE_NAK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);}else if (notcorrupt(rcvpkt) && is_seq_even(rcvpkt)){sndpkt = make_pkt(rcv_seq, PACKET_TYPE_ACK, NULL);udt_send(sockfd, sndpkt, &server_addr);free(sndpkt);}free(rcvpkt);break;}}

3) rdt2.1的测试

实验要求我们以 数据包错误频率(Tamper rate) 为自变量,改变其值

观察 数据包总数(OverHead)有效吞吐量(Goodput) 这两个因变量与之的关系

数据包总数(OverHead)= 发送端总发包量 + 接收端总发包量,即包括正常数据包、ACK/NAK包、重传包;

有效吞吐量(Goodput) = 有效数据包数量 × 数据包大小 ÷ 数据发送总时间,其中有效数据包不包括ACK/NAK和重传包;

为了统计数据包总数,我们在库文件中引入静态变量:

static unsigned long OverHead = 0;

在每次发送数据包,即调用udt_send时,将该变量+1:

void udt_send(SOCKET sockfd, Packet *packet, struct sockaddr_in *addr)
{char *buffer = (char *)malloc(sizeof(Packet));memcpy(buffer, packet, sizeof(Packet));// 发送通知if (sendto(sockfd, buffer, sizeof(Packet), 0, (struct sockaddr *)addr, sizeof(*addr)) == SOCKET_ERROR){printf("Error code : %d\n", WSAGetLastError());printf("Sendto failed.\n");}else{printf("Sent successfully. Sequence: %d; Type:%d.\n", packet->seq, packet->type);OverHead++; // 统计OverHead}free(buffer);
}

提供获取OverHead的函数:

unsigned long getOverHead(){printf("OverHead: %d\n", OverHead);return OverHead;
}

我们以相同的手段统计数据包损坏数,并计算数据包错误频率

数据包错误频率 = 数据包损坏数 / 数据包总数

static unsigned long corruptNum = 0;
boolean corrupt(Packet *rcvpkt)
{if (rcvpkt->checksum != calculate_checksum(rcvpkt)){corruptNum++;   // 统计数据包损坏数printf("Packet corrupted!\n");return TRUE;}else{return FALSE;}
}
float calculate_tamper_rate(){float tamper_rate = corruptNum * 1.0 / OverHead;printf("Tamper Rate: %f\n", tamper_rate);return tamper_rate;
}

而对于有效吞吐量,库文件中已经有提供函数给我们直接获取,需要我们传入程序运行的始末时间:

float calculate_goodput(unsigned long start_time, unsigned long end_time)
{unsigned long long total_bytes_received = MAX_PACKET_SIZE * TOTAL_PACKETS;              // 接收到的总字节数float goodput = (float)total_bytes_received / (float)(end_time - start_time) * 1000.0f; // bytes per secondprintf("Total time elapsed: %lu ms\n", (end_time - start_time));printf("Goodput: %f B/s\n", goodput);return goodput;
}

在server.c中调用以上函数如下:

    // get start_timeunsigned long start_time = GetTickCount();while (!finish_send){/**状态机**/ }// get end_timeunsigned long end_time = GetTickCount();// calculate goodputcalculate_goodput(start_time, end_time);// get overheadgetOverHead();// get tamper ratecalculate_tamper_rate();
}

由于默认的数据包数量是200,在该条件下,前后间隔时间太短接近于0,会导致计算吞吐量得到无穷大。因此在rdt.h中修改数据包数为20000:

#define TOTAL_PACKETS 20000

先运行接收端再运行客户端,分别将输出内容重定向到文本文件中:

root@Andrew:/mnt/d/.c/computernetwork/exp3-1# ./client.exe >client.txt
root@Andrew:/mnt/d/.c/computernetwork/exp3-1# ./server.exe > server.txt

在clumsy中设定Tamper = 0.3,测试结果如下:

Total time elapsed: 1531 ms
Goodput: 13376878.000000 B/s
OverHead: 20105
Tamper Rate: 0.002835

修改Tamper的值依次为0.3, 0.5, 1, 2, 3, 5, 10, 20, 30,统计汇总到excel表格中,并绘制曲线如下:

9912a0f3794c14949d973d45ca2ce9e.png

772527f5e6443b3755427a36ecf3500.png

根据图像可以看出,数据包总数 与 数据包错误频率 之间有呈非线性关系的趋势,而有效吞吐量 与 数据包错误频率 之间呈线性关系

二、实现rdt2.2

1. 理解rdt2.2

想要理解为什么引入rdt2.2,还是得看rdt2.1在哪种场景下会比较低效:

假设连续三个数据包满足以下条件:接收端正常接收数据包,接收端返回的ACK发生错误,发送端重传的数据包发生错误

如果是rdt2.1,之后会发生:接收端返回一个NAK,之后发送端再重传一个数据包

可以发现,在上述场景中,发送端重传的数据包无论是否发生错误,对接收端来说都是没有用的。而如果该数据包损坏,接收端就得返回一个NAK,导致发送端不得不再重传一次没有用的数据包

于是rdt2.2引入一种解决方案:

不使用NAK,而是将ACK编号:接收端只有在接收到当前期待的数据包(比如数据包0)时,才会返回该序号的ACK(比如ACK0),其他情况(接收到数据包1 或 接收到受损的数据包)都会返回另一序号的ACK(比如ACK1)

因此,rdt2.2的状态机如下:

发送端

rdt_send(data)
sndpkt=make_pkt(0, data, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isACK(rcvpkt, 1))
udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt, 0)
rdt_send(data)
sndpkt=make_pkt(1, data, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || isACK(rcvpkt, 0))
udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && isACK(rcvpkt, 1)
Wait for call 0 from above
Wait for ACK 0
Wait for call 1 from above
Wait for ACK 1

相比rdt2.1,rdt2.2检查ACK的函数多了一个参数,值为0/1,用于判断ACK的序号的奇偶性;并将isNAK替换为当前期待ACK序号奇偶性相反的isACK,比如,在状态Wait for ACK 0:

isACK(rcvpkt) 变为 isACK(rcvpkt, 0)
isNAK(rcvpkt) 变为 isACK(rcvpkt, 1)

接收端

rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || has_seq1(rcvpkt))
sndpkt=make_pkt(ACK, 1, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq0(rcvpkt)
extract(packet, data) deliver_data(data)
sndpkt=make_pkt(ACK, 0, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && (corrupt(rcvpkt) || has_seq0(rcvpkt))
sndpkt=make_pkt(ACK, 0, checksum) udt_send(sndpkt)
rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq1(rcvpkt)
extract(packet, data) deliver_data(data)
sndpkt=make_pkt(ACK, 1, checksum) udt_send(sndpkt)
Wait for 0 call from below
Wait for 1 call from below

相比rdt2.1,rdt2.2在状态Wait for 0 call from below时,将corrupt(rcvpkt) 和 notcorrupt(rcvpkt) && has_seq1(rcvpkt)合并为一种情况:

corrupt(rcvpkt) || has_seq1(rcvpkt)

并统一发送ACK1(make_pkt构造ACK数据包也增加了一个序号参数):

make_pkt(ACK, 1, checksum)

否则(正常接收到数据包0)发送ACK0:

make_pkt(ACK, 0, checksum)

2. rdt2.2代码实现

由于rdt2.2不需要NAK,并且将ACK编号为ACK0和ACK1

在库文件中将数据包类型(包括ACK, DATA, NAK)修改为(ACK0, DATA, ACK1),如下:

// 数据包定义
typedef enum
{// rdt 2.1// PACKET_TYPE_ACK = 1,// PACKET_TYPE_DATA = 0,// PACKET_TYPE_NAK = -1// rdt 2.2PACKET_TYPE_ACK_ODD = 1,PACKET_TYPE_DATA = 0,PACKET_TYPE_ACK_EVEN = -1
} Packet_Type;

那么就不需要原本的isACK以及isNAK,将其注释并引入isACKEven和isACKOdd判断数据包类型:

// rdt v2.1
// boolean isACK(Packet *rcvpkt)
// {
//     if (rcvpkt->type == PACKET_TYPE_ACK)
//     {
//         printf("Received ACK of %d\n", rcvpkt->seq);
//         return TRUE;
//     }
//     return FALSE;
// }
// boolean isNAK(Packet *rcvpkt)
// {
//     if (rcvpkt->type == PACKET_TYPE_NAK)
//     {
//         printf("Received NAK of %d\n", rcvpkt->seq);
//         return TRUE;
//     }//     return FALSE;
// }// rdt v2.2
boolean isACKOdd(Packet *rcvpkt)
{if (rcvpkt->type == PACKET_TYPE_ACK_ODD)  // key step{printf("Received ACK ODD of %d\n", rcvpkt->seq);return TRUE;}return FALSE;
}
boolean isACKEven(Packet *rcvpkt)
{if (rcvpkt->type == PACKET_TYPE_ACK_EVEN)  // key step{printf("Received ACK EVEN of %d\n", rcvpkt->seq);return TRUE;}return FALSE;
}

对于发送端,将状态 STATE_WAIT_ACK_NAK_EVENSTATE_WAIT_ACK_NAK_ODD 修改为 STATE_WAIT_ACK_EVENSTATE_WAIT_ACK_ODD,根据状态机模型合并两个分支(以下key step):

        switch (currentState){case STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE:printf("STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE\n");data = rdt_send(seq);sndpkt = make_pkt(seq, PACKET_TYPE_DATA, data);udt_send(sockfd, sndpkt, &client_addr);seq++;// rdt v2.1:// currentState = STATE_WAIT_ACK_NAK_EVEN;// rdt v2.2:currentState = STATE_WAIT_ACK_EVEN;break;// rdt v2.1:// case STATE_WAIT_ACK_NAK_EVEN://     printf("STATE_WAIT_ACK_NAK_EVEN\n");//     rcvpkt = rdt_rcv(sockfd, &client_addr);//     if (corrupt(rcvpkt) || isNAK(rcvpkt))//     {//         udt_send(sockfd, sndpkt, &client_addr);//     }//     else if (notcorrupt(rcvpkt) && isACK(rcvpkt))//     {//         free(sndpkt);//         currentState = STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE;//     }//     free(rcvpkt);//     break;// rdt v2.2:case STATE_WAIT_ACK_EVEN:printf("STATE_WAIT_ACK_EVEN\n");rcvpkt = rdt_rcv(sockfd, &client_addr);if (corrupt(rcvpkt) || isACKOdd(rcvpkt))          // key step{udt_send(sockfd, sndpkt, &client_addr);}else if (notcorrupt(rcvpkt) && isACKEven(rcvpkt))  // key step{free(sndpkt);currentState = STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE;}free(rcvpkt);break;case STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE:printf("STATE_WAIT_FOR_CALL_ODD_FROM_ABOVE\n");data = rdt_send(seq);sndpkt = make_pkt(seq, PACKET_TYPE_DATA, data);udt_send(sockfd, sndpkt, &client_addr);seq++;// rdt v2.1// currentState = STATE_WAIT_ACK_NAK_ODD;// rdt v2.2currentState = STATE_WAIT_ACK_ODD;break;// rdt v2.1// case STATE_WAIT_ACK_NAK_ODD://     printf("STATE_WAIT_ACK_NAK_ODD\n");//     rcvpkt = rdt_rcv(sockfd, &client_addr);//     if (corrupt(rcvpkt) || isNAK(rcvpkt))//     {//         udt_send(sockfd, sndpkt, &client_addr);//     }//     else if (notcorrupt(rcvpkt) && isACK(rcvpkt))//     {//         free(sndpkt);//         currentState = STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE;//         if (seq == TOTAL_PACKETS)//             finish_send = TRUE;//     }//     free(rcvpkt);// rdt v2.2case STATE_WAIT_ACK_ODD:printf("STATE_WAIT_ACK_ODD\n");rcvpkt = rdt_rcv(sockfd, &client_addr);if (corrupt(rcvpkt) || isACKEven(rcvpkt))          // key step{udt_send(sockfd, sndpkt, &client_addr);}else if (notcorrupt(rcvpkt) && isACKOdd(rcvpkt))  // key step{free(sndpkt);currentState = STATE_WAIT_FOR_CALL_EVEN_FROM_ABOVE;if (seq == TOTAL_PACKETS)finish_send = TRUE;}free(rcvpkt);}}

3. rdt2.2测试

以数据包错误率为自变量对rdt2.2进行与上述相同的测试,并与rdt2.1的结果进行比较,绘制曲线如下

c03952d33fe77a91e8c4bb899978bae.png

4e4a06d56d194518bbbd3292b6b94e4.png

可以看到,rdt2.2对比rdt2.1发送的数据包总量更少,且有效吞吐量更高,说明rdt2.2确实减少了冗余数据包的发送,相比rdt2.1更为高效

三、实现rdt3.0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/830325.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2000.1-2023.8中国经济政策不确定性指数数据(日度、月度)

2000.1-2023.8中国经济政策不确定性指数数据(日度、月度) 1、时间:日度:2001.1.1-2022.06.17,月度2000.1-2023.8 2、指标:CNEPU(经济政策不确定性指数) 3、来源:China…

Linux网络-文件传输协议之FTP服务(附带命令及截图)

目录 一.FTP简介 二.FTP的数据模式 1.主动模式 2.被动模式 3.两种模式比较 三.安装配置vsftpd 1.安装vsftpd 1.1.安装前关闭防火墙 1.2.安装vsftpd 1.3.查看 1.4.备份 2.配置 3.重启后生效 四.相关实验 1.以win为例 1.1.设置并测试测试连通性 1.2.在终端里创建…

Redis基本數據結構 ― List

Redis基本數據結構 ― List 介紹常用命令範例1. 將元素推入List中2. 取得List內容3. 彈出元素 介紹 Redis中的List結構是一個雙向鏈表。 LPUSH LPOP StackLPUSH RPOP QueueLPUSH BRPOP Queue(消息隊列) 常用命令 命令功能LPUSH將元素推入列表左端RPUSH將元素推入列表右…

ubuntu20.04安装RabbitMQ 3.11.19+Erlang 25.3.1

1、检查RabbitMQ、Erlang版本 Erlang Version Requirements | RabbitMQ 2、ubuntu20.04对应的是 focal 3、下载安装Erlang 下载地址:http://packages.erlang-solutions.com/erlang/debian/pool/ sudo dpkg -i esl-erlang_25.3-1~ubuntu~focal_amd64.deb sudo apt…

C++ 如何实现原子性

1.操作系统如何实现原子性 在单处理器,单核,运行多线程的情况下,我们不使用线程同步工具, 我们会出现,线程之间会互相抢夺,临界区的资源,造成数据不符合我们预期的结果, 后面再说解决办法,那么我们怎么帮助实现原子性 1 屏蔽中断,不让线程之间切换,让它完成再切换 2 底层硬…

栈与递归的关系

定义 特点 函数调用过程 具体实现过程与状态 小结 拓展 递归的分解 典型案例

第三弹:JavaScript 学习记录

目录 1.1. 了解 1.1.1. 为什么学习JavaScript 1.1.2. JavaScript简介 1.1.3. JavaScript / ECMAScript 1.1.4. JavaScript使用方式 1.1.5. JavaScript输出 1.1.6. JavaScript语句 1.1.7. JavaScript注释 1.1.8. JavaScript变量及常量 1.1.9. JavaScript数据类型 1.1.…

C++实战演练---负载均衡在线oj项目预热

顾得泉:个人主页 个人专栏:《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂,年薪百万! 前言 学习准备了快一年时间,心心念念的实战演练终于可以开始了,话不多说,直接进入主题…

React、React Router 和 Redux 常用Hooks 总结,提升您的开发效率!

Hooks 是 React 16.8 中引入的一种新特性,它使得函数组件可以使用 state 和其他 React 特性,从而大大提高了函数组件的灵活性和功能性。下面分别总结React、React Router 、Redux中常用的Hooks。 常用Hooks速记 React Hooks useState:用于…

ssm088基于JAVA的汽车售票网站abo+vue

汽车售票网站的设计与实现 摘 要 互联网发展至今,无论是其理论还是技术都已经成熟,而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对汽车售票信息管理混乱,出错率…

基于stm32的USB模拟UART的尝试F429

目录 基于stm32的USB模拟UART的尝试F429实验目的场景使用原理图USBX 组件移植USBX实现虚拟串口配置USB移植USBX源码工程中添加对应源码修改usb_otg.c创建 USBX 任务添加使用串口的代码上机现象本文中使用的测试工程 基于stm32的USB模拟UART的尝试F429 本文目标:基于…

uniapp-vue3-wechat:基于uniapp+vue3仿微信app聊天实例(H5+小程序+App端)

uni-vue3-wchat:基于uni-appvue3pinia2高仿微信app聊天模板。 原创基于最新跨端技术uni-appvue3.xpinia2vite4uv-ui构建三端仿微信app界面聊天实例。实现编辑框多行消息/emoj混合、长按触摸式仿微信语音面板、图片/视频预览、红包/朋友圈等功能。支持编译到H5小程序…

数据结构与算法-抽象数据类型ADT系列

以前在学习数据结构的时候做实验,老师要求用ADT抽象数据类型来写这些实现代码。后面也要复习数据结构,在这里就先放下链接。不过以前学习的时候使用的编程语言是C,里面会用到很多指针。现在编代码过程大多数时候都是用Java。不过思路应该还是…

keytool,openssl的使用

写在前面 在生成公钥私钥,配置https时经常需要用到keytool,openssl工具,本文就一起看下其是如何使用的。 keytool是jdk自带的工具,不需要额外下载,但openssl需要额外下载 。 1:使用keytool生成jks私钥文件…

WEB攻防-IIS中间件PUT漏洞

IIS6.0 server在web服务扩展中开启了WebDAV(Web-based Distributed Authoring and Versioning)。WebDAV是一种HTTP1.1的扩展协议。它扩展了HTTP 1.1,在GET、POST、HEAD等几个HTTP标准方法以外添加了一些新的方法,如PUT&#xff0c…

自动驾驶横向控制算法

本文内容来源是B站——忠厚老实的老王,侵删。 三个坐标系和一些有关的物理量 使用 frenet坐标系可以实现将车辆纵向控制和横向控制解耦,将其分开控制。使用右手系来进行学习。 一些有关物理量的基本概念: 运动学方程 建立微分方程 主要是弄…

Linux进程——进程的概念(PCB的理解)

前言:在了解完冯诺依曼体系结构和操作系统之后,我们进入了Linux的下一篇章Linux进程,但在学习Linux进程之前,一定要阅读理解上一篇内容,理解“先描述,再组织”才能更好的理解进程的含义。 Linux进程学习基…

Hadoop3:集群搭建及常用命令与shell脚本整理(入门篇,从零开始搭建)

一、集群环境说明 1、用VMware安装3台Centos7.9虚拟机 2、虚拟机配置:2C,2G内存,50G存储 3、集群架构 从表格中,可以看出,Hadoop集群,主要有2部分,一个是HDFS服务,一个是YARN服务 …

记一次内网渗透

环境搭建: 排错: 在搭建过程中发现报错,删除这部分内容就成功解决。 信息收集 端口扫描 使用namp -sn 探测存活IP 接着去查看服务 web服务 然后发现80端口。访问 发现有管理员接口,并泄露了默认用户名和密码。 弱口令登录 …

vue中配置 测试、准生产、生产环境

在package.json,scripts中配置 "dev": "vue-cli-service serve --open --mode dev",在项目根目录下配置 新建 .env.dev 和.env.development文件 //类似于title NODE_ENV "serve" //各环境API数据接口请求地址 VUE_APP_BASE_API "http:…