TCP拥塞管理

本次实验构建基于TCP/IP协议栈的点到点连接网络,对不同TCP拥塞控制变种下拥塞窗口变化进行跟踪、分析和可视化。介绍了NewReno、Cubic等算法,通过多个问题实验,分析慢启动、拥塞避免阶段,对比不同算法性能,还调研了BBR算法特点和原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实验二 TCP控制

实验概述

本次实验构建一个基于TCP/IP 协议栈的点到点连接网络。通过对不同TCP拥塞控制变种下拥塞窗口的变化进行跟踪、数据分析和可视化,加深对拥塞控制基本原理的理解,熟悉主要拥塞控制算法变种间的区别。同时,学习ns-3中跟踪变量(trace)的用法。

实验目标

  • 掌握TCP拥塞控制基本原理;
  • 了解ns-3中的跟踪变量,并掌握其查找和使用方法;
  • 了解ns-3模块默认参数的调整方法;
  • 掌握一种数据处理及可视化方法。

预备知识

传输控制协议(TCP)是TCP/IP协议栈传输层的核心协议,作用是维持可靠的端到端连接。TCP协议主要包含两大部分功能,一是拥塞控制,二是差错恢复。前者基于拥塞控制算法(Congestion Control Algorithm,CCA)调节拥塞窗口(Congestion Window,CWND),控制链路的在外(In-Flight)数据量,避免拥塞;后者基于反馈重传机制,对未确认分组进行重传,保证数据的一致性。本次实验关注TCP拥塞控制。

最早大规模部署的TCP拥塞控制算法称为NewReno。NewReno包含4个主要机制,即慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快速重传(Fast Retransmit )、快速恢复(Fast Recovery)。当前,大多数操作系统(Linux、Mac、MS Windows)的默认TCP拥塞控制算法则为Cubic,大约在2005年前后诞生。它在经典NewReno的基础上着重改进了慢启动和拥塞避免阶段CWND 的调节方式,使 TCP可以更好地适应高速链路、无线链路等场景。TCP拥塞控制算法长期以来是计算机网络领域的研究热点,近年来,仍有新的拥塞控制算法不断提出。代表性的包括谷歌公司提出的BBR 算法、Facebook 提出的Copa算法等。随着机器学习技术的火热应用,将其引入TCP拥塞控制算法设计也是热门的研究方向。

实验内容

问题2.1

代码分析

#include "tutorial-app.h" // 包含自定义应用程序的头文件
#include "ns3/applications-module.h" // 包含NS-3应用程序模块
#include "ns3/core-module.h" // 包含NS-3核心模块
#include "ns3/internet-module.h" // 包含NS-3网络模块
#include "ns3/network-module.h" // 包含NS-3网络模块
#include "ns3/point-to-point-module.h" // 包含NS-3点对点模块

#include <fstream> // 包含文件流用于输出
using namespace ns3; // 使用NS-3命名空间

NS_LOG_COMPONENT_DEFINE("FifthScriptExample"); // 定义此脚本的日志组件

// 拥塞窗口变化回调函数
static void
CwndChange(uint32_t oldCwnd, uint32_t newCwnd)
{
    NS_LOG_UNCOND(Simulator::Now().GetSeconds() << "\t" << newCwnd); // 输出新的拥塞窗口大小
}

// 接收丢包回调函数
static void
RxDrop(Ptr<const Packet> p)
{
    NS_LOG_UNCOND("RxDrop at " << Simulator::Now().GetSeconds()); // 输出丢包的时间
}

int
main(int argc, char* argv[])
{
    CommandLine cmd(__FILE__); // 创建命令行解析器
    cmd.Parse(argc, argv); // 解析命令行参数

    // 配置TCP参数:NewReno拥塞控制,初始拥塞窗口为1个数据包,使用经典的快速恢复算法
    Config::SetDefault("ns3::TcpL4Protocol::SocketType", StringValue("ns3::TcpNewReno"));
    Config::SetDefault("ns3::TcpSocket::InitialCwnd", UintegerValue(1));
    Config::SetDefault("ns3::TcpL4Protocol::RecoveryType",
                       TypeIdValue(TypeId::LookupByName("ns3::TcpClassicRecovery")));

    NodeContainer nodes; // 创建网络节点容器
    nodes.Create(2); // 创建两个节点

    PointToPointHelper pointToPoint; // 创建点对点连接助手
    pointToPoint.SetDeviceAttribute("DataRate", StringValue("5Mbps")); // 设置点对点链接的数据率
    pointToPoint.SetChannelAttribute("Delay", StringValue("2ms")); // 设置点对点链接的延迟

    NetDeviceContainer devices; // 创建网络设备容器
    devices = pointToPoint.Install(nodes); // 在节点上安装点对点设备

    Ptr<RateErrorModel> em = CreateObject<RateErrorModel>(); // 创建速率错误模型以模拟丢包
    em->SetAttribute("ErrorRate", DoubleValue(0.00001)); // 设置丢包率
    devices.Get(1)->SetAttribute("ReceiveErrorModel", PointerValue(em)); // 将错误模型附加到接收设备

    InternetStackHelper stack; // 创建互联网协议栈助手
    stack.Install(nodes); // 在节点上安装互联网协议栈

    Ipv4AddressHelper address; // 创建IPv4地址助手
    address.SetBase("10.1.1.0", "255.255.255.252"); // 设置基本网络地址和子网掩码
    Ipv4InterfaceContainer interfaces = address.Assign(devices); // 为设备分配IPv4地址

    uint16_t sinkPort = 8080; // 设置包接收应用程序的端口
    Address sinkAddress(InetSocketAddress(interfaces.GetAddress(1), sinkPort)); // 设置接收地址
    PacketSinkHelper packetSinkHelper("ns3::TcpSocketFactory",
                                      InetSocketAddress(Ipv4Address::GetAny(), sinkPort)); // 创建包接收助手
    ApplicationContainer sinkApps = packetSinkHelper.Install(nodes.Get(1)); // 在节点1上安装包接收应用程序
    sinkApps.Start(Seconds(0.)); // 启动包接收应用程序
    sinkApps.Stop(Seconds(20.)); // 20秒后停止包接收应用程序

    Ptr<Socket> ns3TcpSocket = Socket::CreateSocket(nodes.Get(0), TcpSocketFactory::GetTypeId()); // 在节点0上创建TCP套接字
    ns3TcpSocket->TraceConnectWithoutContext("CongestionWindow", MakeCallback(&CwndChange)); // 连接拥塞窗口变化跟踪

    Ptr<TutorialApp> app = CreateObject<TutorialApp>(); // 创建自定义应用程序
    app->Setup(ns3TcpSocket, sinkAddress, 1040, 1000, DataRate("1Mbps")); // 设置自定义应用程序参数
    nodes.Get(0)->AddApplication(app); // 将自定义应用程序添加到节点0
    app->SetStartTime(Seconds(1.)); // 设置自定义应用程序的启动时间
    app->SetStopTime(Seconds(20.)); // 设置自定义应用程序的停止时间

    devices.Get(1)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback(&RxDrop)); // 连接接收丢包跟踪

    Simulator::Stop(Seconds(20)); // 在20秒时停止模拟
    Simulator::Run(); // 运行模拟
    Simulator::Destroy(); // 销毁模拟器

    return 0; // 从main函数返回
}

代码分析
这个NS-3脚本设置了一个简单的网络拓扑,包括两个节点通过点对点连接。它配置了TCP参数,设置了数据丢失模拟,安装了包接收和自定义应用程序,并且在20秒内运行了模拟。自定义应用程序监视拥塞窗口的变化,同时也跟踪了数据包接收丢失。

部分运行结果

Consolidate compiler generated dependencies of target fifth
[  0%] Building CXX object examples/tutorial/CMakeFiles/fifth.dir/fifth.cc.o
[  0%] Building CXX object examples/tutorial/CMakeFiles/fifth.dir/tutorial-app.cc.o
[  0%] Linking CXX executable ../../../build/examples/tutorial/ns3.41-fifth-debug
1.00419	536
1.0093	1072
1.01528	1608
1.02167	2144
1.02999	2680
1.03831	3216
1.04663	3752
1.05495	4288
1.06327	4824
1.07159	5360
1.07991	5896
1.08823	6432
1.09655	6968
1.10487	7504
1.11319	8040
1.12151	8576
1.12983	9112
RxDrop at 1.13696
1.13815	9648
1.1548	1072
1.15978	1340
1.16476	1554
1.17232	1738
1.18064	1903
1.18896	2053

问题2.2

使用Python或 Matlab等工具处理q1.log,画出以时间为横轴,以CWND 为纵轴的曲线。结合仿真输出中 RxDrop(即丢包)出现的时间点,对该曲线特征进行简要描述。

python实现可视化分析

以下是处理数据并绘制曲线的 Python 代码:

import re
import matplotlib.pyplot as plt
filename = 'q1.txt'

time_cwnd = []
cwnd = []
time_ssthresh = []
ssthresh = []
time_rxdrop = []

with open(filename, 'r') as file:
    lines = file.readlines()

for line in lines:
    if line.startswith('RxDrop'):
        time_rxdrop.append(float(line.split()[2]))
    else:
        match = re.match(r'(\d+\.\d+)\s+(\d+)', line)
        if match:
            time, value = float(match.group(1)), int(match.group(2))
            if line.startswith('SsThreshChange'):
                time_ssthresh.append(time)
                ssthresh.append(value)
            else:
                time_cwnd.append(time)
                cwnd.append(value)

# 绘制曲线
plt.figure(figsize=(10, 5))
plt.plot(time_cwnd, cwnd, label='CWND')
plt.scatter(time_ssthresh, ssthresh, color='red', label='SsThreshChange')
plt.scatter(time_rxdrop, [0] * len(time_rxdrop), color='black', marker='x', label='RxDrop')

# 添加标签和标题
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('CWND and SsThreshChange over Time')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

图像分析

在传输初始阶段, 由于未知网络传输能力, 需要缓慢探测可用传输资源, 防止短时间内大量数据注入导致拥塞。慢启动算法正是针对这一问题而设计。在数据传输之初或者重传计时器检测到丢包后,需要执行慢启动。

cwnd会随着RTT呈指数增长。因此, 最终cwnd ( W也如此)会增至很大, 大量数据包的发送将导致网络瘫痪(TCP吞吐量与w/RTT成正比)。当发生上述情况时, cwnd将大幅度减小(减至原值一半)。这是TCP由慢启动阶段至拥塞避免阶段的转折点, 与cwnd和慢启动阈值相关。经典慢启动算法操作。在没有ACK延时情况下, 每接收到一个好的ACK就意味着发送方可以发送两个新的数据包(左)。这会使得发送方窗口随时间呈指数增长(右,上方曲线)。当发生ACK延时, 如每隔一个数据包生成一个ACK, cwnd仍以指数增长, 但增幅较小。

相关知识点

慢启动

当一个新的TCP连接建立或检测到由重传超时(RTO)导致的丢包时, 需要执行慢启动。TCP发送端长时间处于空闲状态也可能调用慢启动算法。慢启动的目的是, 使TCP在用拥塞避免探寻更多可用带宽之前得到cwnd值, 以及帮助TCP建立ACK时钟。通常, TCP在建立新连接时执行慢启动, 直至有丢包时, 执行拥塞避免算法进入稳定状态。

拥塞避免

一旦确立慢启动阈值, TCP会进人拥塞避免阶段, cwnd每次的增长值近似于成功传输的数据段大小。通常认为拥塞避免阶段的窗口随时间线性增长, 而慢启动阶段呈指数增长。这个函数也称为累加增长, 因为每成功接收到相应数据, cwnd就会增加一个特定值(这里大约是一个包大小)。

慢启动和拥塞避免的选择

慢启动阈值。这个值和cwnd的关系是:决定采用慢启动还是拥塞避免的界线。当cwnd < ssthresh, 使用慢启动算法; 当cwnd > ssthresh, 需要执行拥塞避免; 而当两者相等时, 任何一种算法都可以使用。慢启动阈值不是固定的, 而是随时间改变的。它的主要目的是, 在没有丢包发生的情况下, 记住上一次“最好的”操作窗口估计值。换言之, 它记录TCP最优窗口估计值的下界。

当有重传情况发生, 无论是超时重传还是快速重传, ssthresh会按下式改变:

ssthresh=max (在外数据值/2, 2*SMSS)

如果出现重传情况, TCP会认为操作窗口超出了网络传输能力范围。这时会将慢启动阈值(ssthresh)减小至当前窗口大小的一半(但不小于2*SMSS), 从而减小最优窗口估计值。这样通常会导致ssthresh减小, 但也有可能会使之增大。

问题2.3

加入以下代码

static void
SsThreshChange(uint32_t oldSsThresh, uint32_t newSsThresh)
{
NS_LOG_UNCOND("SsThreshChange at " << Simulator::Now().GetSeconds() << " to " <<
 newSsThresh);
}
 ns3TcpSocket->TraceConnectWithoutContext("SlowStartThreshold", MakeCallback(&
 SsThreshChange));

分析日志,指出慢启动门限何时发生了变化?此后,发生丢包时CWND的值为何总是回到1072?这时拥塞控制处于慢启动还是拥塞避免阶段?

import re
import matplotlib.pyplot as plt
filename = 'q2.txt'

time_cwnd = []
cwnd = []
time_ssthresh = []
ssthresh = []
time_rxdrop = []

with open(filename, 'r') as file:
    lines = file.readlines()

for line in lines:
    if line.startswith('RxDrop'):
        time_rxdrop.append(float(line.split()[2]))
    elif line.startswith('SsThreshChange'):
        match = re.match(r'SsThreshChange\s+at\s+(\d+\.\d+)\s+to\s+(\d+)', line)
        if match:
            time_ssthresh.append(float(match.group(1)))
            ssthresh.append(int(match.group(2)))
    else:
        match = re.match(r'(\d+\.\d+)\s+(\d+)', line)
        if match:
            time, value = float(match.group(1)), int(match.group(2))
            time_cwnd.append(time)
            cwnd.append(value)
# 绘制曲线
plt.figure(figsize=(10, 5))
plt.plot(time_cwnd, cwnd, label='CWND')
plt.scatter(time_ssthresh, ssthresh, color='red', label='SsThreshChange')  # 将 SsThreshChange 标记在曲线上
plt.scatter(time_rxdrop, [0] * len(time_rxdrop), color='black', marker='x', label='RxDrop')

# 添加标签和标题
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('CWND and SsThreshChange over Time')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

慢启动门限合适发生了变化,查看日志SsThreshChange at 1.1548 to 1072,由上图分析出慢启动门限在1.1548时变为1072,此后发生丢包,门限值未发送改变,此时拥塞控制处于拥塞避免阶段。

问题2.4

将上述误比特率改为10−4,并参考上述连接、打印跟踪变量的方法,为仿真脚本添加打印在外字节数跟踪变量(BytesInFlight)的代码,重新运行仿真脚本。分析日志,说明慢启动门限变化前在外字节数的值与慢启动门限新值之间的数值关系。

import re
import matplotlib.pyplot as plt
filename = 'q3.txt'

time_cwnd = []
cwnd = []
time_ssthresh = []
ssthresh = []
time_rxdrop = []

with open(filename, 'r') as file:
    lines = file.readlines()

for line in lines:
    if line.startswith('RxDrop'):
        time_rxdrop.append(float(line.split()[2]))
    elif line.startswith('SsThreshChange'):
        match = re.match(r'SsThreshChange\s+at\s+(\d+\.\d+)\s+to\s+(\d+)', line)
        if match:
            time_ssthresh.append(float(match.group(1)))
            ssthresh.append(int(match.group(2)))
    else:
        match = re.match(r'(\d+\.\d+)\s+(\d+)', line)
        if match:
            time, value = float(match.group(1)), int(match.group(2))
            time_cwnd.append(time)
            cwnd.append(value)
# 绘制曲线
plt.figure(figsize=(10, 5))
plt.plot(time_cwnd, cwnd, label='CWND')
plt.scatter(time_ssthresh, ssthresh, color='red', label='SsThreshChange')  # 将 SsThreshChange 标记在曲线上
plt.scatter(time_rxdrop, [0] * len(time_rxdrop), color='black', marker='x', label='RxDrop')

# 添加标签和标题
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('CWND and SsThreshChange over Time')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

这个值和cwnd的关系是:决定采用慢启动还是拥塞避免的界线。当cwnd < ssthresh, 使用慢启动算法; 当cwnd > ssthresh, 需要执行拥塞避免; 而当两者相等时, 任何一种算法都可以使用。

问题2.5

进一步将误比特率改为5×10−4,重新运行仿真脚本。分析日志,指出为何此后丢包时CWND的值经常回到536(ns-3中TCP的默认TCP最大报文段长度(MSS))?对比问题2.2和2.4中的 CWND 数据,试说明随机丢包增多对 TCP 的吞吐量性能(带宽利用率)造成了什么样的影响?

import re
import matplotlib.pyplot as plt
filename = 'q4.txt'

time_cwnd = []
cwnd = []
time_ssthresh = []
ssthresh = []
time_rxdrop = []

with open(filename, 'r') as file:
    lines = file.readlines()

for line in lines:
    if line.startswith('RxDrop'):
        time_rxdrop.append(float(line.split()[2]))
    elif line.startswith('SsThreshChange'):
        match = re.match(r'SsThreshChange\s+at\s+(\d+\.\d+)\s+to\s+(\d+)', line)
        if match:
            time_ssthresh.append(float(match.group(1)))
            ssthresh.append(int(match.group(2)))
    else:
        match = re.match(r'(\d+\.\d+)\s+(\d+)', line)
        if match:
            time, value = float(match.group(1)), int(match.group(2))
            time_cwnd.append(time)
            cwnd.append(value)
# 绘制曲线
plt.figure(figsize=(10, 5))
plt.plot(time_cwnd, cwnd, label='CWND')
plt.scatter(time_ssthresh, ssthresh, color='red', label='SsThreshChange')  # 将 SsThreshChange 标记在曲线上
plt.scatter(time_rxdrop, [0] * len(time_rxdrop), color='black', marker='x', label='RxDrop')

# 添加标签和标题
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('CWND and SsThreshChange over Time')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

丢包导致congestion window(cwnd)很小,导致无法链路的速率极低。对 TCP 的吞吐量性能造成极大影响,较低数据传输的速度和效率

问题2.6

修改代码,将TCP拥塞控制变种由ns3::TcpNewReno 替换为 ns3::TcpCubic。在5×10−4 误比特率设置下重新运行仿真脚本,重新画出问题2.2中所述的曲线。对照之前的曲线,说明异同?

import re
import matplotlib.pyplot as plt
filename = 'q5.txt'

time_cwnd = []
cwnd = []
time_ssthresh = []
ssthresh = []
time_rxdrop = []

with open(filename, 'r') as file:
    lines = file.readlines()

for line in lines:
    if line.startswith('RxDrop'):
        time_rxdrop.append(float(line.split()[2]))
    elif line.startswith('SsThreshChange'):
        match = re.match(r'SsThreshChange\s+at\s+(\d+\.\d+)\s+to\s+(\d+)', line)
        if match:
            time_ssthresh.append(float(match.group(1)))
            ssthresh.append(int(match.group(2)))
    else:
        match = re.match(r'(\d+\.\d+)\s+(\d+)', line)
        if match:
            time, value = float(match.group(1)), int(match.group(2))
            time_cwnd.append(time)
            cwnd.append(value)
# 绘制曲线
plt.figure(figsize=(10, 5))
plt.plot(time_cwnd, cwnd, label='CWND')
plt.scatter(time_ssthresh, ssthresh, color='red', label='SsThreshChange')  # 将 SsThreshChange 标记在曲线上
plt.scatter(time_rxdrop, [0] * len(time_rxdrop), color='black', marker='x', label='RxDrop')

# 添加标签和标题
plt.xlabel('Time')
plt.ylabel('Value')
plt.title('CWND and SsThreshChange over Time')
plt.legend()
plt.grid(True)
plt.show()

在这里插入图片描述

Cubic控制算法在发生丢包时,能够很快调整cwdn大小,从而提高数据传输速度和效率

问题2.7

调研文献,简要说明NewReno变种和Cubic变种的差异

NewReno是基于Reno算法的改进版本,通过在TCP流中引入快速重传和快速恢复机制来减少网络拥塞的影响。当有数据包在网络上丢失时,NewReno可以快速重传丢失的数据包,并通过快速恢复机制重新调整拥塞窗口大小,从而提高数据传输的效率。
Cubic是一种基于拥塞窗口大小的拥塞控制算法,它使用了一个具有sigmoid形状的函数来调整拥塞窗口大小,从而使网络拥塞的概率更小。Cubic有更好的网络拥塞控制能力,能够更快地调整拥塞窗口大小,从而提高数据传输的速度和效率。

问题2.8

TCP BBR 是谷歌于 2016 年前后提出的一种新的拥塞控制算法。以 “BBR”、“缓冲膨胀”为搜索引擎关键词开展调研,简述BBR的基本特点和原理。

BBR的基本特点和原理
1 网络路径模型
BBR是基于模型的拥塞控制算法:其行为方式是对传输流通过的网络路径的确切模型的表现。BBR模型包括两个估算参数:

BBR.BtlBw:估计的传输通道的瓶颈带宽,估算自滑动窗口的最大传发送速率样本。
BBR.RTprop:估计的该路径的双向往返传播延时,估算自滑动窗口的最小往返延时样本。

2 目标操作点
BBR使用该模型来探求高吞吐量低延时的工作区。为了在最优点(即达到最大吞吐量并且维持最小延时 [K79] [GK81])附近运作,该系统需要维护两个条件:

速率平衡:数据包的到达速率等于传输流的瓶颈带宽。
充盈管道:路径中处于传输状态的数据量等于BDP。
BBR使用该模型去维护网络的最佳工作区。为了达到速率平衡,BBR使用pacing让数据包以接近BBR.BtlBw的速率进行发送。为了达到充盈管道,BBR对pacing rate进行调制,来保持传输中的数据量接近估算得到的带宽延时积(BDP),或BBR.BtlBw * BBR.RTprop。

3 控制参数
BBR使用该模型去控制网络的发送行为,让其保持在目标工作区附近。与Reno和CUBIC不同的是(他们使用单一控制变量,如使用cwnd来限制传输中的数据量),BBR使用三个独特的控制参数:
pacing rate:BBR发送数据的速率。
send quantum:发送端为了均衡单包传输开销而规划的,可发送的单个包的最大集合大小,。
cwnd:在任意时刻,BBR允许网络中处于传输状态的数据量的最大值。

4 状态机设计概览
BBR使用一个清晰明了的状态机来维护三个控制参数。该状态机通过交替循环探测BBR.BtlBw和BBR.RTprop,来实现高吞吐量、低延时、近似公平的带宽共享。
BBR起始于Startup状态,该状态下会迅速提升发送速率。当预估到网络管道被填满时,则进入Drain状态,开始排放管道队列。已处于稳态的BBR流将只使用ProbeBW状态,去周期性地短暂地提升传输中的数据量,来探测更高的BBR.BtlBw样本。ProbeRTT状态下(如果需要的话),会短暂地降低传输中的数据量,去探测更低的BBR.RTprop样本。

缓冲膨胀(Bufferbloat)
深缓存:在有着深缓存的瓶颈链路中,拥塞往往发送在丢包之前。在现今的的边缘网络中,基于丢包的拥塞控制算法对众多最后几英里的设备,进行了深缓存的反复填充,引发了不必要的数秒级的排队延时,也就是“缓冲膨胀”的问题

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值