计算机网络实验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增速减缓。

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

相关推荐
百万蹄蹄向前冲1 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
陈哥聊测试1 天前
软件格局在变,谁能扛起国产替代的大旗?
安全·程序员·产品
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 天前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
少年姜太公2 天前
从零开始详解js中的this(下)
前端·javascript·程序员
凌虚2 天前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
小华同学ai2 天前
ShowDoc:Star12.3k,福利项目,个人小团队的在线文档“简单、易用、轻量化”还专门针对API文档、技术文档做了优化
前端·程序员·github
小青鱼4 天前
AI编程-Cursor从入门到精通系列之常用概念及解释(二)
人工智能·程序员
捡田螺的小男孩5 天前
参数校验的十个建议!收藏好,别再给测试机会提bug~
java·后端·程序员
哔哩哔哩技术5 天前
B站装机系统实践:从初创到规模化的演进
前端·程序员
程序员鱼皮5 天前
没事别想不开去创业!
计算机·面试·程序员·项目