计算机网络实验3.3-基于UDP服务设计可靠传输协议拥塞控制

计算机网络实验3.3-基于UDP服务设计可靠传输协议(拥塞控制)

实验要求

  • 在实验3-2的基础上,选择实现一种拥塞控制 算法,也可以是改进的算法,完成给定测试文件的传输。
  • RENO算法;
  • 也可以自行设计协议或实现其他拥塞控制算法;
  • 给出实现的拥塞控制算法的原理说明;
  • 有必要日志输出(须显示窗口大小改变情况)。

程序流程展示

注:与实验3.1相同的部分仅作简要叙述,详细可以参见计算机网络实验实验3.1

协议设计

基于rdt3.0,本次实验在GBN的基础上实现了RENO拥塞控制算法,并通过令接收方缓存失序包验证了快速重传的正确性和有效性。

报文结构

如图所示,报文头长度共128Bits。下面介绍报文结构如下所示:

整个实验只使用一个序列号字段。对于发送端对应TCP中的seq,接收端对应TCP中的ack

下面是十六位校验和以及数据报字段长度,与TCP相同。

使用u_short来存放flag。其字段含义如下:

F:FIN

S:SYN

A:ACK

H:FILE_HEAD

FILE_HEAD用于指示接收端此报文包含文件信息的字段。

window_size存放接收端通告给发送端的窗口大小。

option为可选字段,在本次实验中暂时用于存放文件长度。

data的最大长度可以调节,本次实验定义为1024字节。

此部分代码段的定义可参阅源代码或3.1部分的报告。

建连和断连

建连和断连过程与3.1无太大变化。主要是在建连过程中增加了接收方初始窗口大小的通告。

程序代码解释

文件发送过程

发送端

在本次实验中发送端实现的是基础的RENO算法.状态机如下所示:

下面将结合代码进行拥塞控制算法具体的原理及实现。

发送分组

首先由于拥塞控制算法中窗口大小计算是以字节为单位的,因此本次实验中计算和展示窗口大小时也改为字节计数(而不是按分组计数),如下图所示:

在发送时,只需要添加"实际发送窗口取决于接收通告窗口和拥塞控制窗口中较小值"对应的代码即可,对应于第一行:

C++ 复制代码
//send packets
window_size = min(cwnd, advertised_window_size);
if ((LastByteSent - LastByteAcked < window_size) && (LastByteSent < file_len)) {
    pkt_data_size = min(MAX_SIZE, file_len - nextseqnum * MAX_SIZE);
    sndpkts[nextseqnum] = make_pkt(DATA, nextseqnum, pkt_data_size, file_data + nextseqnum * MAX_SIZE);
    udt_send(sndpkts[nextseqnum]);
    cout << "Sent packet " + to_string(nextseqnum) + " ";
    if (base == nextseqnum) {
        timer.start_timer();
    }
    nextseqnum++;
    LastByteSent += pkt_data_size;
    print_window();
}

拥塞控制相关的逻辑主要在于接收ACK和超时时的窗口变化。下面将着重讲解。

接收ACK

首先我们先来看处理正常收到ACK的情况:

上一个实验中提到,实际网络环境中ACK未必是按序到达的,因此将base的移动改为判断按照按序接收到ACK为标准进行滑动,如下所示:

C++ 复制代码
//        base = get_ack_num(rcvpkt) + 1;
        acked[get_ack_num(rcvpkt)] = true;
        while (acked[base]) {
            base++;
        }

但是,实际上注意到,当接收方发出一个更大的ACK,说明之前的ACK事实上已经按序收到。当发送方收到一个更大的ACK时,不管之前的ACK有没有返回到发送方,发送方仅通过这个信息就已经可以确信接收方按序收到了之前的ACK了。因此其实是可以放心的进行base = get_ack_num(rcvpkt) + 1的。

之后,我们据此更新LastByteAcked,然后根据状态机对RENO算法的状态进行更新,最后更新窗口大小。据此,代码如下所示:

C++ 复制代码
        u_int ack_num = get_ack_num(rcvpkt);
		if (ack_num >= base) {
            u_int gap = ack_num - base + 1;
            //update the base and LastByteAcked
            for (int i = 0; i < gap; i++) {
                LastByteAcked += sndpkts[base + i].head.data_size;
            }
            base = ack_num + 1;
            switch (RENO_STATE) {
                case SLOW_START:
                    cwnd += gap * MSS;
                    dupACKcount = 0;
                    if (cwnd >= ssthresh) {
                        RENO_STATE = CONGESTION_AVOIDANCE;
                    }
                    break;
                case CONGESTION_AVOIDANCE:
                    cwnd += gap * MSS * MSS / cwnd;
                    dupACKcount = 0;
                    break;
                case FAST_RECOVERY:
                    cwnd = ssthresh;
                    RENO_STATE = CONGESTION_AVOIDANCE;
                    dupACKcount = 0;
                    break;
                default:
                    break;
            }
            window_size = min(cwnd, advertised_window_size);
        }

处于慢启动阶段时,窗口大小增大1MSS。每过一个RTT,cwnd翻倍,窗口大小呈指数增长;而处于拥塞避阶段时,免cwnd = cwnd + MSS*(MSS/cwnd)。相当于每过一个RTT,cwnd加1。

当收到冗余ACK时,我们需要进行快速重传,并进行窗口大小更新。

c++ 复制代码
            //duplicate ACK
            dupACKcount++;
            if (RENO_STATE == SLOW_START || RENO_STATE == CONGESTION_AVOIDANCE) {
                if (dupACKcount == 3) {
                    //fast retransmit
                    ssthresh = cwnd / 2;
                    cwnd = ssthresh + 3 * MSS;
                    window_size = min(cwnd, advertised_window_size);
                    RENO_STATE = FAST_RECOVERY;
                    print_message("Fast resend"+to_string(ack_num), WARNING);
                    //resend the packet
                    udt_send(sndpkts[ack_num + 1]);
                } else {
                    cwnd += MSS;
                }
            }
超时处理

注意到,不管什么状态下,超时后都需要恢复到慢启动状态,因此直接在超时事件上进行改动即可。

C++ 复制代码
//handle timeout
if (timer.timeout()) {
    print_message("Timeout, resend packets from " + to_string(base) + " to " + to_string(nextseqnum - 1),
                  WARNING);
    for (u_int i = base; i < nextseqnum; i++) {
        udt_send(sndpkts[i]);
    }
    ssthresh = cwnd / 2;
    cwnd = MSS;
    dupACKcount = 0;
    RENO_STATE = SLOW_START;
    timer.start_timer();
}

接收端

首先我们可以运行查看接收端不进行改变的情况:

可以看到,当接收方不缓存失序的包时,即使有了快速重传,由于之前发过的,对接收端来说失序的包没有进行缓存,仍旧相当于丢失了。因此快速重传当前期望的包只是解决了"眼前的问题",其余的包迟早还要超时重传,因此尽管进行了拥塞控制,但重传的行为在实际网络环境中事实上加剧了拥塞。

因此基于此可以对接收端的逻辑进行改进:

在前面提到,接收端必须是累计确认的(确认按序收到的最大序号),发送端才有理由在移动窗口时移动到当前ack+1的位置。同时我们还想让接收端缓存失序的包,以避免发送端进行已收到的包的重传而加剧拥塞。

因此我在上一次实验的SR的基础上修改发送ACK行为,使其从选择确认转变成累计确认,同时保留其缓存失序包行为即可解决这个问题。代码如下所示:

c++ 复制代码
               if (pkt_seq >= rcv_base && pkt_seq <= rcv_base + N - 1) {
                    //in the window
                    if (!acked[pkt_seq]) {
                        if (pkt_seq == rcv_base) {
                            //the first packet in the window
                            pkt_data_size = rcvpkt.head.data_size;
                            memcpy(file_buffer + pkt_seq * MAX_SIZE, rcvpkt.data, pkt_data_size);
                            acked[pkt_seq] = true;
                            print_message("Received packet " + to_string(pkt_seq), DEBUG);
                            //slide the window
                            while (acked[rcv_base]) {
                                rcv_base++;
                            }
                            packet sndpkt = make_pkt(ACK, rcv_base - 1);
                            udt_send(sndpkt);
                        } else {
                            //not the first packet in the window, cache it
                            pkt_data_size = rcvpkt.head.data_size;
                            memcpy(file_buffer + pkt_seq * MAX_SIZE, rcvpkt.data, pkt_data_size);
                            acked[pkt_seq] = true;
                            packet sndpkt = make_pkt(ACK, rcv_base - 1);
                            udt_send(sndpkt);
                            print_message("Received packet " + to_string(pkt_seq) + ", cached", DEBUG);
                        }
                    } else {
                        //already acked in the window, do not resend ack
                        print_message("Received packet " + to_string(pkt_seq) + " again", WARNING);
                          packet sndpkt = make_pkt(ACK, rcv_base - 1);
                          udt_send(sndpkt);
                    }
                }

当收到窗口内的包时,总是发送当前rcv_base - 1位置的ACK。但是如果收到的包恰巧在rcv_base上,那么窗口其实有潜力往前移动很多,以覆盖之前缓存过的包。移动完了之后再发送rcv_base - 1位置的ACK,让发送端知道在这之前的包都已经按序收到了。除此之外的情况都不发送ACK。

可以看到,修改后快速恢复起到了其应有的作用。

成功收取!

程序演示

建立连接

路由器设置:

增大通告窗口大小,观察慢启动和拥塞控制阶段窗口大小变化。慢启动阶段,cwnd的值迅速增大,当cwnd>=ssthresh后进入拥塞控制阶段,cwnd增速减缓。

快速重传的正确性上面分析的过程中也已经验证。

相关推荐
花椒技术10 小时前
从7S到4S,我们如何系统性降低直播播放延迟
性能优化·程序员
程序员cxuan13 小时前
vibe coding 凉了,wish coding 来了
人工智能·后端·程序员
JustTest18 小时前
Mac mini初始安装软件记录
程序员
SimonKing19 小时前
轻量级富文本编辑器Quill,保姆级教程,5分钟快速上手
java·后端·程序员
文心快码BaiduComate1 天前
Comate搭载Kimi K2.6,长程13h!
前端·后端·程序员
图图玩ai2 天前
SSH 命令管理工具怎么选?从命令收藏到批量执行一次讲清
linux·nginx·docker·ai·程序员·ssh·可视化·gmssh·批量命令执行
SamDeepThinking2 天前
程序员懂业务,到底要懂到什么程度
后端·程序员·团队管理
盖世英雄酱581362 天前
java技术博主停更3个月了???
程序员
DyLatte2 天前
我做了个AI项目后才发现:会做事的人,正在输给会讲故事的人
前端·后端·程序员
SimonKing2 天前
别让你的代码裸奔!Spring Boot混淆全攻略(附配置)
java·后端·程序员