# 实验四

> 本次实验要实现：拥塞控制（简化版或完整版）

实验文档给了两种状态机的选择，我选择实现了简化版的（但是所有状态迁移中CWND和ssthresh的变化还要遵循new reno的规则）。如果你前面实验完全正确，直接加一个控制函数改一点就可以了。

如果你和我一样实现简化版的状态机，你还是要看一下实验文档里的new reno基本知识。

简化版的状态机简单来说就是OPEN状态是慢启动和拥塞控制的集合状态，如果收到一个dupACK,进入Disorder状态，如果连续三个dupACK进入Recovery状态快速重传，如果超时强制进入LOSS状态，超时重传后回到OPEN状态，CWND和ssthresh的变化和new reno的要求要一致。

下面来说一些我遇到的要注意的细节和一些提示。

## 对之前实验代码的修改

这里可能是我的个例，之前实现的persist timer如果触发，它的probe报文会影响tcp\_process的逻辑，因为判断数据包是用pl\_len>0判断的，但是probe包的pl\_len=1,有可能client端会误处理。同理probe包回复的ACK也可能会被server端误处理，如果你发现打log,server端有时候调用了update window或者在处理ACK然后发了probe报文（这是不正常的），client端反而收到pl\_len=1的报文，那很可能是tcp\_process的条件控制有误。

<figure><img src="/files/Cw1Qm4uIfUEd7lwmjafq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/lLSSoj8mCMOxDLTaLAT2" alt=""><figcaption></figcaption></figure>

在抓包看来，probe报文是keep-alive报文，注意一下。

那么怎么解决这个问题呢？很简单：

1.检查你所有的可能被server端使用的tcp\_process的代码，不要让他们调用tcp\_update\_window,因为这个函数只是给client用的，你服务器又不用维护window大小。

2.用tsk->parent来判断是不是server端（因为你在进入LISTEN状态就应该设置了server的parent）,如果为true说明是server端。只要修改一下你的判断条件加上判断tsk->parent就能保证你不会错误执行某些代码。

尽量确认你的代码没有这个问题，否则你的拥塞窗口的结果可能会让你难以理解。

## 画图和数据获取

首先是数据的问题。如果你只是按照实验文档里的开个thread,那么你很可能发现你记录的CWND都是0.00000（即使你确实修改了CWND）。

这是因为实验框架里的CWND是U32类型的，我这里直接改成了float类型，要注意。

然后在 struct tcp\_sock里加上一个int c\_state表示拥塞控制的四个状态。

然后加一个enum:

```c
// 拥塞控制状态
enum tcp_congestion_state {
    TCP_CONG_OPEN,       // 正常状态(包含慢启动和拥塞避免)
    TCP_CONG_DISORDER,   // 失序状态
    TCP_CONG_RECOVERY,   // 快速恢复状态
    TCP_CONG_LOSS       // 超时状态
    };
```

如果你用实验文档的方法很可能不能捕捉所有的变化，这里你也可以选择每个ACK来一个记录，经过我的尝试，建议直接在拥塞控制函数

```c
void tcp_congestion_control(struct tcp_sock *tsk, struct tcp_cb *cb, int ack_valid) 
```

里这么写：

```c
static FILE *fp = NULL;
    static int time_us = 0;
    
    if (!fp) {
        fp = fopen("cwnd.txt", "w");
        if (!fp) {
            log(ERROR, "Failed to open cwnd.txt");
            return;
        }
    }
	if (less_or_equal_32b(tsk->snd_una, cb->ack) && less_or_equal_32b(cb->ack, tsk->snd_nxt)){
         // 记录当前拥塞控制参数
         time_us += 1000;  // 假设每个ACK间隔1ms
         fprintf(fp, "%d %f %u %u %d\n", time_us, tsk->cwnd, tsk->ssthresh, tsk->adv_wnd,tsk->c_state);
         fflush(fp); 
        }
```

这里我所谓的1ms就是一个ACK，注意一下。（我这里图方便当成时间了，但是实际上是ACK个数）

但是注意的就是以ACK记录和以时间记录最后生成的图是有区别的，具体区别在哪里后面说。

然后就是画图，这里贴一个我的py脚本（横坐标有误，应该叫ACK个数而不是时间，我这里认为1ms就是一个ACK）

```python
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams.update({'font.size': 14})

data = np.loadtxt('cwnd.txt')
time = data[:, 0] / 1000
cwnd = data[:, 1]
ssthresh = data[:, 2]
state = data[:, 4].astype(int)

interval = 1000
num_segments = int(np.ceil(time[-1] / interval))

# 修改颜色方案，使LOSS状态更明显
state_colors = {
    0: 'green',     # OPEN
    1: 'yellow',    # DISORDER 
    2: 'red',       # RECOVERY
    3: 'black'      # LOSS - 使用黑色
}

state_names = ["OPEN", "DISORDER", "RECOVERY", "LOSS"]
used_states = set()

for i in range(num_segments):
    # ...existing code...
    plt.figure(figsize=(12, 8))
    used_states.clear()
    
    # 绘制ssthresh
    s = ssthresh[i * interval:(i + 1) * interval]  # Define 's' as a segment of 'ssthresh'
    st = state[i * interval:(i + 1) * interval]  # Define 'st' as a segment of 'state'
    t = time[i * interval:(i + 1) * interval]   # Define 't' as a segment of 'time'
    c = cwnd[i * interval:(i + 1) * interval]   # Define 'c' as a segment of 'cwnd'
    plt.plot(t, s, 'r--', label='ssthresh', linewidth=2)
    
    # 标记LOSS状态发生的位置
    for j in range(1, len(st)):
        if st[j] == 3:  # LOSS状态
            plt.axvline(x=t[j], color='black', linestyle=':', alpha=0.5)
            plt.axvspan(t[j], t[j+1] if j+1 < len(t) else t[-1], 
                       color='gray', alpha=0.2)
    
    # 为每个状态绘制一段折线
    last_idx = 0
    last_state = st[0]
    for j in range(1, len(st)):
        if st[j] != last_state:
            label = f'cwnd ({state_names[int(last_state)]})' if last_state not in used_states else None
            if last_state == 3:  # LOSS状态使用更粗的线
                linewidth = 3
            else:
                linewidth = 2
            plt.plot(t[last_idx:j+1], c[last_idx:j+1], 
                    color=state_colors[last_state],
                    linewidth=linewidth,
                    label=label)
            used_states.add(last_state)
            last_idx = j
            last_state = st[j]
    
    # 绘制最后一段
    label = f'cwnd ({state_names[int(last_state)]})' if last_state not in used_states else None
    linewidth = 3 if last_state == 3 else 2
    plt.plot(t[last_idx:], c[last_idx:], 
            color=state_colors[last_state],
            linewidth=linewidth,
            label=label)
    
    start_time = i * interval
    end_time = (i + 1) * interval if (i + 1) * interval <= time[-1] else int(time[-1])
    plt.title(f'TCP Congestion Control ({start_time}-{end_time}ms)', fontsize=16)
    plt.xlabel('Time (ms)', fontsize=14)
    plt.ylabel('Window Size (bytes)', fontsize=14)
    
    plt.grid(True, alpha=0.3)
    plt.legend(fontsize=12)
    plt.tight_layout()
    
    plt.savefig(f'segment_{i+1}.png', dpi=300, bbox_inches='tight')
    plt.close()
```

它会对过长的流程分段截成多个图，1000个ACK一个图（我这里就是3800个ACK四个图）。这里给出前两个图做个分析。

<figure><img src="/files/HLYeEibVUYPxTBbttr02" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/81d1Q8Dro4mkknk1DULW" alt=""><figcaption></figcaption></figure>

这里有几个问题：

1.慢启动阶段是直线，但是拥塞控制反而是曲线（越来越慢）。

这是因为我们是按照ACK进行捕捉的，所谓的指数和线性是对时间来说的，对每个ACK来说慢启动都是加1MSS是线性的。

拥塞控制同理，CWND在增加，那1MSS/CWND就在减少，因此对于ACK来说会增的越来越慢。

2.即使有快速重传也可能会超时重传。

因此很可能会有多余的多传的包的ACK，需要被排除，这就是ack\_valid的作用，判断ACK是否合法，不合法就不用调整拥塞窗口。（乱序队列实现正确的话应该可以处理多的ACK，因此理论上不用管）。

如果你的实现正确，应该能像这个图看见recovery转换ssthresh和CWND减半，重传的峰（因为dupACK很多），偶尔的超时（你的超时应该是由retrans\_timer控制的，如果每次重传，就应该把状态改成LOSS状态,不要忘了去tcp\_timer.c里改）

3.注意reno和new reno的区别，详细看new reno基本知识。

## 关于代码

1.初始化

我这里就不在tcp\_update\_window里初始化了，直接把CWND的设置去掉，然后在client进入established的时候初始化。

```c
tsk->cwnd=(float)TCP_MSS;
                    tsk->ssthresh=(u32)64 *1024;
                    tsk->dup_ack_count=0;
                    tsk->c_state=TCP_CONG_OPEN;
```

这里要注意的首先是强制类型转换，还有在tcp\_sock里加上的int dup\_ack\_count。

然后注意ssthresh的大小是64kb(看实验文档的new reno基础知识)

{% hint style="warning" %}
注意cwnd的单位是多少TCP\_MSS！一个包的大小是TCP\_MSS！不是1！如果你设置为1，那么增加会特别慢，你的传输要到猴年马月。
{% endhint %}

2.拥塞控制函数的设置

这里不提供函数细节，按照new reno设置即可，但是要注意这几个：

```c
 tsk->cwnd = (float)(tsk->ssthresh + 3*TCP_MSS);
```

这是快速恢复，不要＋3，而是加3MSS。

```c
tsk->cwnd+=(float)(1*TCP_MSS);
```

这是慢启动，要加MSS而不是1。

```c
 tsk->cwnd += (float)((float)(TCP_MSS*TCP_MSS)/(float)tsk->cwnd);
```

这是最重要的，CWND的单位是MSS，一个MSS就是一个包，就是一个ACK包，所以拥塞控制的线性增加这么写（想想为什么？）

关于ack\_valid,你可以用它来控制，也可以遇到重复的超时ACK和快速重传ACK就不管，不更改拥塞窗口也可以，看你逻辑。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blueroaring-njus-organization.gitbook.io/njucs_25spring_network-exp_additional/shi-yan-si.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
