# 实验三

{% hint style="info" %}
本实验要实现：不丢包和丢包情况下的文件传输（最简化版）
{% endhint %}

{% hint style="danger" %}
请确保你真正理解实验二和实验二（没测到）的rcv\_nxt等tcp\_sock中的参数，以及其他结构体（如ringbuffer,锁）的用法和操作，否则你会白忙活很久。
{% endhint %}

### 记得修改：挥手

如果你的挥手还是client端只发一个FIN,请改成发FIN|ACK，为什么实验文档里有说。

### 让我们从抓包开始：Wireshark

实验文档一开始就手把手教你如何使用Wireshark,请完全按照它的方式自己先抓出一个完全正确的.pcap并留下来，它将成为你的对照组来分析你的传输错在哪里。

抓包建议改名字，要不一直被覆盖不方便检查某些随机错误。（没错，本次实验很多时候都是神秘的随机事件导致错误，~~本质上是这个实现的小收发玩意太过于简陋以至于不能处理除了丢包以外的一切问题，并且丢包处理也是最慢的一种）~~

还是一样，bulk.py是完全正确的收发逻辑，你抓包两边都完全对的收发逻辑，你得到的抓包就是成功的模板。

记得及时pkill掉pcap,防止抓的包混一起不好辨别。

除了实验手册里提到的TCP Window Full,重复ACK(TCP Dup ACK x #xx),丢包（TCP Previous segment not captured）（核心）以外，你还很容易遇到如下的错误（黑色的）报文：

TCP Retransmission/TCP fast Retransmission：你的某个端在重传某个报文。（fast Retransmission是拥塞控制阶段，python端可能会做）

TCP Port numbers reused：你重复用了一个port,原先的port被占用还没释放。（很好笑，这其实不是你的问题，是它tcp\_topo.py没写很细的bug）

TCP Out-of-Order:发送的报文乱序，这个是很tricky的问题，后面单独讲。

总之，有黑报文很正常（毕竟你测试的就是丢包），你需要的是看出来哪些是你想要发生的，哪些是你没有预料到要发生的。

{% hint style="success" %}
Wireshark将是你有力的武器，请务必学会使用它！
{% endhint %}

### 几个抓包的例子

1.TCP ACKed unseen segment

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

这个问题你一开始很可能以为是你握手写的有问题，然后调半天发现无果……

实际上这个问题只会出现在你如果每次测试不重新python3 tcp\_topo\_loss.py生成h1和h2,多次快速重复测试的时候（尤其当你不耐烦经常前面ctrl+c没传完包就强制终止之后）。

这个问题就是因为你之前测试发的包还没通过网络层传过去，然后你就终止了开下一个测试，然后之前测试的包这时候到了你的包传输逻辑就炸了。

传统的TCP协议栈按照教材说是会每次设置一个新的特别大的isn来避免这个情况的，但是这个框架太蠢了没有这个机制（当然，你想写也可以加），所以不是你的问题。

2.到底丢了哪个包

<figure><img src="/files/9p0ZYFF58wLkdYl5bB5S" alt=""><figcaption></figcaption></figure>

是56941这个序号的包丢了吗？不是！是它的前一个包丢了！

3.奇怪的关闭（3.1最容易出现）

<figure><img src="/files/3SuAr362teaULFhBzsdo" alt=""><figcaption></figcaption></figure>

好奇怪，为什么我发了FIN，回复我的ACK序号不对劲？

因为你还没传完，没收到所有希望收到的ACK就直接把传输close了，然后挥手逻辑就被这些晚到的ACK打乱了。

有几个可行方法来解决：

A.去做3.2，把乱序队列控制好，加上这段在你的tcp\_apps.c的client端发完包后。

```
// 关闭文件和连接
    fclose(fp);
    pthread_mutex_lock(&tsk->send_buf_lock);
    while (!list_empty(&tsk->send_buf)) {
        pthread_mutex_unlock(&tsk->send_buf_lock);
        sleep_on(tsk->wait_send);
        pthread_mutex_lock(&tsk->send_buf_lock);
    }
    pthread_mutex_unlock(&tsk->send_buf_lock);
   // sleep(1);  // 等待数据发送完成
   //log(DEBUG,"close");
    tcp_sock_close(tsk);
```

这样就是send\_buffer中的包没全收到对应的ACK就不会close。

B.不写上面这个，直接写个sleep(),但是不好，一个是你不知道你要sleep多久，还有一个是不工程。

C.3.1你可以在tcp\_sock\_write里临时实现一个停等协议（每发一个包后等到它的ACK收到了才发下一个包），等3.2实现完后再删了，这样也行（当然丢包就做不了了）

```
// 等待之前发送的包被确认
       /* while (less_than_32b(tsk->snd_una, tsk->snd_nxt)) {
            pthread_mutex_unlock(&tsk->sk_lock);
            sleep_on(tsk->wait_send);
            if (tsk->state != TCP_ESTABLISHED) {
                return -1;
            }
            pthread_mutex_lock(&tsk->sk_lock);
        }*/
```

除了这些之外，你还可能会通过抓包发现例如没有成功重传丢的包（显示一堆dup ACK，但是没有对应的seq的包被重传），或者你还可以通过seq等信息发现你的rcv\_nxt等设置的有错误（请理解这些参数和抓到包的包里的某些参数的关系），或者你还可以去寻找从哪个包开始出现了问题（很多时候问题不出在黑色报文，而是之前的报文错了的连锁反应），因此你需要经验和学习。

### 为什么说TCP收发控制是GBN和SR的结合

GBN（Go-Back-N）协议在数据传输中，如果某个报文段没有被正确接收，那么从这个报文段到后面的所有报文段都需要重新发送。GBN采用累计应答的方式，如果接收端返回ACK=3，则证明报文段3以及之前的所有报文段都被正确接收。GBN不设置缓冲区，接收端不对失序到达的报文段进行缓存，保证了报文的按序交付‌。\
SR（Selective Repeat）协议在接收方设置缓冲区，用于接收失序到达的报文段。如果某个报文段没有被正确接收，但后面的报文段被正确接收了，那么只需要重发这个未确认的报文段。接收端返回ACK是当前接收成功的报文段序号，不采用累计应答的方式。SR为每个报文段设置单独的计时器，单个分组计时器超时只重发这一个报文段‌。\
TCP协议与GBN和SR有所不同，它采用累计应答的方式，接收端返回ACK是期待接收的下一个报文段的序号。TCP在接收端设置缓存，用于缓存正确接收但失序的报文段。TCP还具有快速重传机制，如果在一定时间内收到多个冗余的ACK，就会提前重传丢失的报文段，大大提高了效率。TCP的ACK表示接收端希望从发送端收到的下一字节的序号‌。\
本次实验的收发也是遵循TCP协议，在其中，GBN体现在累计应答，SR体现在乱序队列和重传计时器。

### 万事开头难：加锁

当你看到实验文档的一堆关于锁的描述时，你肯定处于懵逼状态--我要怎么写？

别急，我们来整理一下：

* timer\_list\_lock:你在实验二就应该实现了，很简单，记得初始化就能用了，用来锁住所有你的计时器（在本实验里，如果你实现persist timer,这个锁应该会保护三个timer:timewait\_timer,persist timer,重传计时器），只要你在某处动了计时器的参数，就要先加锁，然后解锁。
* sk\_lock:后面的锁也是一样，自己加一个（让AI给你写结构体，它除了有时候会忘记初始化没啥问题）。这个锁是用来保护我们的snd\_una,snd\_nxt,rcv\_nxt等参数的修改不被并发毁掉的。（没错，这些参数就是struct tcp\_sock里定义的那些，这就是为什么让你回去理解这些参数是什么意思，做什么的。）只要你这些参数被改了，在改之前你就要加锁。
* rcv\_buf\_lock:只要你的server端维护的receive\_buffer(后面会实现)被修改（进或出），那么就得加这个锁。
* send\_buf\_lock:只要你的client端维护的send\_buffer(后面会实现)被修改，那么就得加这个锁。

说了这么多，可能你还是云里雾里的，我这里直接贴几个截图来解释。

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

比如这里我tcp\_scan\_timer\_list函数就先加锁，然后在函数结尾解锁。（我的timer\_list的初始化是一个static 的tricky,和并发执行顺序有关）

{% hint style="info" %}
（个人认为）应该加timer\_list\_lock锁的函数：tcp\_scan\_timer\_list,tcp\_set\_persist\_timer,tcp\_unset\_persist\_timer,tcp\_set\_retrans\_timer,tcp\_unset\_retrans\_timer,tcp\_update\_retrans\_timer(它们没有线性执行关系)
{% endhint %}

<figure><img src="/files/4dh9zoT3HfkEV5yiIiDv" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
应该加rcv\_buf\_lock的函数：tcp\_recv\_ofo\_buffer\_add\_packet(tcp\_move\_recv\_ofo\_buffer不能加，因为有内外执行关系)，tcp\_sock\_read(没错，你需要改一下这个读取函数（AI能帮你很多）)
{% endhint %}

<figure><img src="/files/62GBjlXab7XUl2DOBDHE" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
应该加send\_buf\_lock的函数：tcp\_update\_retrans\_timer(因为这个函数可能会重发一个新的独立报文)，tcp\_sock\_write(当然，你也可以加在tcp\_client里，这样write里就不用加了)，tcp\_send\_buffer\_add\_packet,tcp\_update\_send\_buffer,tcp\_retrans\_send\_buffer
{% endhint %}

```c
void tcp_process(struct tcp_sock *tsk, struct tcp_cb *cb, char *packet)
{
    //log(DEBUG,"process_lockstart");
    pthread_mutex_lock(&tsk->sk_lock);
    //log(DEBUG,"process_locksuccess");
    //省略中间处理代码
   
    //log(DEBUG,"process unlock start");
    pthread_mutex_unlock(&tsk->sk_lock);
    //log(DEBUG,"process unlock success");
	//fprintf(stdout, "TODO: implement %s please.\n", __FUNCTION__);
}
```

一个好的tcp\_process应该是这样的加锁解锁方式，如果你的所有对incoming packet的处理都在tcp\_process里做（指调用函数在tcp\_process里），那么这个大锁就能保你平安。

除此之外，tcp\_sock\_read应该也加上这个锁。（虽然不加理论上不会在OJ上出问题）

接下来我们来说几个注意事项。

1.关于死锁

锁的名称是它的唯一标识，同一时间只能一个线程持有锁，如果你不解锁，那么其他代码永远不会得到它，这就死循环了。

所以不要在外面加锁了，里面调用的函数又申请了同样名称的一把锁！

如果你很不幸死锁了，打前后log看哪个锁没拿到。（就像我tcp\_process注释做的那样）

2.~~关于锁，如果你操作系统上的不是jyy的，建议去jyy.wiki.cn听听正在讲的并发(私货)~~

#### 一个关于sleep\_on的问题

```c
int tcp_sock_read(struct tcp_sock *tsk, char *buf, int len) {
	pthread_mutex_lock(&tsk->rcv_buf_lock);
    //log(DEBUG,"get_read_lock_success");
    while (ring_buffer_empty(tsk->rcv_buf)) {
        pthread_mutex_unlock(&tsk->rcv_buf_lock); // 在sleep前释放锁
        sleep_on(tsk->wait_recv);
        pthread_mutex_lock(&tsk->rcv_buf_lock); // sleep返回后重新获取锁
    }
    
    // 读取数据
    int read_len = read_ring_buffer(tsk->rcv_buf, buf, len);
    pthread_mutex_unlock(&tsk->rcv_buf_lock);
   // log(DEBUG,"read_len=%d",read_len);
    return read_len;
}
```

例如我的tcp\_sock\_read,记得在sleep\_on前先解锁（具体原因请自行探索）。

### 第二步：写3.1部分的代码

关于滑动窗口的部分看完后，就能先把tcp\_tx\_window\_test和tcp\_update\_window实现了。（注意死锁，int溢出），代码注释给的很明白，也没啥可变通的地方，我直接贴上。

```c
static inline void tcp_update_window(struct tcp_sock *tsk, struct tcp_cb *cb)
{
	/*u16 old_snd_wnd = tsk->snd_wnd;
	tsk->snd_wnd = cb->rwnd;
	if (old_snd_wnd == 0)
		wake_up(tsk->wait_send);*///原来的
        
        //0.在.h define TCP_MSS
        // 1. 记录更新前的窗口状态
        int had_window = tcp_tx_window_test(tsk);
        
        // 2. 更新相关参数
        tsk->snd_una = cb->ack;        // 更新已确认序号
        tsk->adv_wnd = cb->rwnd;       // 更新对端通告窗口
        tsk->cwnd = 0x7f7f7f7f;        // 设置较大的拥塞窗口
        tsk->snd_wnd = min(tsk->adv_wnd, tsk->cwnd); // 发送窗口取两者最小值
        
        // 3. 检查更新后的窗口状态
        int has_window = tcp_tx_window_test(tsk);
        if (tsk->snd_wnd < TCP_MSS) {
            log(DEBUG,"snd_wnd < TCP_MSS");
            tcp_set_persist_timer(tsk);
        } else {
            tcp_unset_persist_timer(tsk);
        }
        // 4. 如果原本没有足够窗口,现在有了,则唤醒发送
        if (!had_window && has_window){
           // log(DEBUG,"has_window ");
            wake_up(tsk->wait_send);
        }
       
}
int tcp_tx_window_test(struct tcp_sock *tsk)
{
   // log(DEBUG,"snd_nxt=%u snd_una=%u snd_wnd=%u",tsk->snd_nxt,tsk->snd_una,tsk->snd_wnd);
    //1. 计算已发送但未确认的数据量
    //2.用less than 32b等函数比较！要不可能出现int溢出
    // 3.计算剩余可用窗口大小
}
```

### 关于Persist timer

建议还是实现了，也不难，注释很明白，照着写就行（写代码作为理解锁的一种方式）。我这里实现了，不过根据不同同学的反馈，使用probe报文的可能性与你的机子的并发执行顺序和你的其他具体实现有关，有的很频繁触发，有的n次才触发一次……。

记得如果实现persist\_timer要改tcp\_timer.c，并且probe报文和正常协议栈的不一样（注意发回的PROBE报文特殊处理，且报文的checksum等要设置好）就行，反正你后面还要实现一个类似的锁，现在先写一个又怎么样呢？

对probe报文在tcp\_process函数里的处理：

```
if (cb->pl_len > 0) {
        if(cb->pl_len==1){
        log(DEBUG,"pl_len=%d",cb->pl_len);
        pthread_mutex_lock(&tsk->rcv_buf_lock);
        tsk->rcv_wnd = ring_buffer_free(tsk->rcv_buf); 
        tcp_send_control_packet(tsk, TCP_ACK);
        pthread_mutex_unlock(&tsk->rcv_buf_lock);
        }
```

probe报文的设置和发送:

```
void tcp_send_probe_packet(struct tcp_sock *tsk)
{
log(DEBUG,"send probe");
int pkt_size = ETHER_HDR_SIZE + IP_BASE_HDR_SIZE + TCP_BASE_HDR_SIZE + 1;
char *packet = malloc(pkt_size);
if (!packet) {
log(ERROR, "Failed to allocate probe packet");
return;
}
struct iphdr *ip = packet_to_ip_hdr(packet);
ip_init_hdr(ip, tsk->sk_sip, tsk->sk_dip, 
            IP_BASE_HDR_SIZE + TCP_BASE_HDR_SIZE + 1, IPPROTO_TCP);

struct tcphdr *tcp = (struct tcphdr *)((char *)ip + IP_BASE_HDR_SIZE);
tcp_init_hdr(tcp, tsk->sk_sport, tsk->sk_dport, 
             tsk->snd_una - 1,  // 探测包序列号 = snd_una - 1（避免影响正常数据）
             tsk->rcv_nxt,       // ACK 号不变
             TCP_ACK,            // 仅设置 ACK 标志
             tsk->rcv_wnd);      // 当前接收窗口

char probe_byte = 0;
memcpy((char *)tcp + TCP_BASE_HDR_SIZE, &probe_byte, 1);
tcp->checksum = tcp_checksum(ip, tcp);
ip_send_packet(packet, pkt_size);

//log(DEBUG, "Sent window probe: seq=%u, ack=%u", 
    //tsk->snd_una - 1, tsk->rcv_nxt);
    }
```

{% hint style="danger" %}

## 实现完3.1不要先进行测试

{% endhint %}

关于神秘“未查出根因”的乱序到达，是怎么一回事呢？

首先我们来说对于你的结果的影响。

目前，你还没有实现乱序到达的处理机制，所以一旦有数据乱序到达，你收到的文件一定和client端传的不一样，少东西了（这不是丢包，只是因为顺序问题）

这个问题会同时影响三个测试，所以你可能会有时候能传对有时候又不行很红温，所以先别管！

这个问题你wireshark抓包也看不出来，包看起来都是顺序发送的，但是你如果printf到达报文的seq\_end和ack再重定向到文件就会发现问题。

个人认为应该是多核的参数问题导致报文乱序到达，总之3.1就别管了，并发bugs不是你我能处理的。

{% hint style="warning" %}
但是还是有警示作用的：server端每次收到包的rcv\_nxt可以直接变成cb->seq\_end吗？并不是！
{% endhint %}

因此，我们先给出警告（后面还会提到）：

{% hint style="danger" %}
谨慎对待你的rcv\_nxt，snd\_una等参数：想明白它们是做什么的，会对报文的seq,ack值造成什么样的影响！还有，要在哪个函数修改它们！如果你实验二对待它们很随便，那你就得修改你的握手和挥手了。
{% endhint %}

## 3.2:丢包重传和乱序处理（核心）

虽然实验文档只说是丢包重传，但是还有一个核心是乱序处理。

乱序处理这个框架采取的是最原始的维护乱序队列（server端recv\_ofo\_buf,client端send\_buf）

关于它们的定义和初始化，如果你没信心，让AI帮你写好，它们对于这种dirty work很擅长（但是工程逻辑能力不行）

### 乱序处理：干了什么？

先拿简单的接收队列来说。接收队列就是一个插入搜索的乱序队列，严格根据收到的包的seq\_end和目前的rcv\_nxt进行比较，看是否等于来决定是否给ring\_buffer写入。

{% hint style="warning" %}
请看实验文档3.1.2的机制来理解可能收到的四种报文，以及3.3.4要求实现的函数的注释来深入理解！
{% endhint %}

一个很tricky的问题就是为什么会收到重复之前已经ACK的包。如果你抓包你会发现，收发端的报文回复很不及时，所以有时候dupack会被python端快速重传。

但是这个可能会对你的代码逻辑造成毁灭性打击。如果client多发了一个重复的数据到了，然后这个重复的数据又已经离开了你的乱序队列里，那么你可能会误判把它又放进去了一次乱序队列里，这就完蛋了，对你的逻辑造成了毁灭性打击。正确的做法是丢弃它们！

如下是一个代码提示，核心代码请自行实现。

```c
int tcp_recv_ofo_buffer_add_packet(struct tcp_sock *tsk, struct tcp_cb *cb)
{
   
    pthread_mutex_lock(&tsk->rcv_buf_lock);
    if (less_or_equal_32b(cb->seq_end, tsk->rcv_nxt)) {
        // 1.这是一个重复的包，直接丢弃
        pthread_mutex_unlock(&tsk->rcv_buf_lock);
       // printf("duplicate packet: seq_end=%u, rcv_nxt=%u\n", cb->seq_end, tsk->rcv_nxt);
        return -1;
    }
    // 2.创建新的队列项
   
    // 3.遍历乱序队列找到合适的位置插入
    
        // 3.1检查是否有重复数据
      
        
        // 3.2找到合适的插入位置
       
    
    // 4.如果遍历完还没插入，则添加到末尾
   

    // 5.尝试上送数据
   // log(DEBUG,"try_to_move");
    tcp_move_recv_ofo_buffer(tsk);
    
    pthread_mutex_unlock(&tsk->rcv_buf_lock);
    return 0;
}

int tcp_move_recv_ofo_buffer(struct tcp_sock *tsk)
{
    struct recv_ofo_buf_entry *entry, *q;
    int moved = 0;
    
    list_for_each_entry_safe(entry, q, &tsk->rcv_ofo_buf, list) {
        // 1.检查是否有序
       
            // 1.1检查接收缓冲区是否有空间
            
            
            // 1.2写入接收缓冲区
           // log(DEBUG,"write_ring_buffer");
           
            
            // 2.更新接收窗口
           
            
            // 3.从乱序队列中移除
           
        // 4.遇到乱序报文就退出
        
    

    if (moved) {
        wake_up(tsk->wait_recv);
    }
   // log(DEBUG,"moved=%d",moved);
    return moved;
}
```

我们先不要管client端的send\_buffer和重传要做什么，先看server端是否正确。

### 测试你的server端

首先，你需要写对你的函数，我们来看server端需要的函数。（recv\_buffer上文已经阐述，不再赘述）

一个重点是tcp\_process里establish阶段收到数据包如何处理？实验文档里说了要看pl\_len。（正常来说，处理报文应该是单独的处理，不应该和你握手或挥手的处理冲突，也就是说不要一不小心tcp\_process调用了错误的case,因为数据报文本身也带ACK，如果你的逻辑不对可能会执行你不想执行的代码）

给一个我个人的逻辑代码：

```
    // 如果是数据包,写入接收缓冲区
if (cb->pl_len > 0) { 
    tcp_recv_ofo_buffer_add_packet(tsk, cb);
   // tsk->rcv_nxt = cb->seq_end;（不要加上这个！）
    // 发送累积ACK
    tcp_send_control_packet(tsk, TCP_ACK);
    wake_up(tsk->wait_recv);
}
else{
switch (tsk->state) {
//...
```

就像我说的一样，不要在这里写tsk->rcv\_nxt=cb->seq\_end,如果你还不知道为什么，说明你没懂滑动窗口和函数做了什么。所以说这些参数要谨慎修改，最好只在被封装的函数里修改。

在client端没有写对前，你可以通过server端跑tcp\_stack,client端跑python来测试（注意这时候要用tcp\_topo\_loss.py了）。

如果你这几个部分的函数都是对的，那么无论是tcp\_topo.py还是tcp\_topo\_loss.py的server用tcp\_stack,client用python最后收到的文件都应该是正确的。

如果你的发送出现问题，可以先抓包看看问题出在哪里。

1.如果你是无法建立连接/挥手炸了，那么你需要重新调整你tcp\_process里握手和挥手的tcp\_sock的参数的设置，抓包或打log看你的哪个参数有问题。

2.如果你是发现rcv\_nxt被莫名奇妙修改了，首先检查你是不是在封装函数之外的哪里偷偷改了你没发现，还有一种可能就是你没处理好我前面说的“重复数据包”问题，你可以通过Printf某些参数进行重定向来看看是否符合你的期待。

比如我打的log抓包（部分）：

```
rcv_nxt=3947955320, seq=3947955320, seq_end=3947955856
rcv_nxt=3947955856, seq=3947955856, seq_end=3947956392
rcv_nxt=3947956392, seq=3947956392, seq_end=3947956928
rcv_nxt=3947956928, seq=3947956928, seq_end=3947957464
rcv_nxt=3947957464, seq=3947957464, seq_end=3947958000
rcv_nxt=3947958000, seq=3947958000, seq_end=3947958536
rcv_nxt=3947958536, seq=3947958536, seq_end=3947959072
rcv_nxt=3947959072, seq=3947959072, seq_end=3947959393
rcv_nxt=3947959393, seq=3947894001, seq_end=3947894537
invalid
rcv_nxt=3947959393, seq=3947894537, seq_end=3947895073
invalid
rcv_nxt=3947959393, seq=3947959393, seq_end=3947959929
rcv_nxt=3947959929, seq=3947959929, seq_end=3947960072
rcv_nxt=3947960072, seq=3947895073, seq_end=3947895359
invalid
rcv_nxt=3947960072, seq=3947960072, seq_end=3947960608
rcv_nxt=3947960608, seq=3947895359, seq_end=3947895895
invalid
rcv_nxt=3947960608, seq=3947895895, seq_end=3947896038
invalid
rcv_nxt=3947960608, seq=3947960608, seq_end=3947961144
rcv_nxt=3947961144, seq=3947961144, seq_end=3947961430
```

你可以看到我发现了重复的报文（seq\_end\<rcv\_nxt），并丢弃了它们，没有出错，当然你也可以输出更多

信息来辅助你。（其实重复报文是人为丢包导致的，一定会出现）

至于乱序报文（seq\_end>rcv\_nxt），由于时不时丢包，一定会乱序到达，这时候你就得维护乱序队列，当然，如果你实现正确，这不应该是问题,重点就是你要明白我们的简陋的实验遵循GBN原则（没错，SR只体现在重传计时器和乱序处理上,这是TCP两者结合的典型例子），因此你的rcv\_nxt永远是希望ACK的下一个报文，这个值是最小的没收到的报文（如果你还不明白，回去学教材）。

### 开始写你的client端

对于client端，需要实现send\_buffer这个乱序队列和重传机制。

实验的实现很简陋，重传由于还没有实现，目前你的C代码的唯一重传手段就是你的重传计时器到点了重传，没有对dupACK的处理！（dupACK直接扔了）

因此你的send\_buffer只是遵循TCP重发的原则，没有拥塞控制的三个阶段（在实验四），因此你的C收发逻辑和抓包和python端不一样，python端有快速重传什么的奇怪机制，你都没有实现（所以，你也不应该管）

那我们开始写代码。

### 重传定时器

加一个struct(让AI帮你写)

加三个函数（注释很清楚）

改一下scan函数（如果你已经实现了persist timer,你应该知道怎么写了）

这部分代码请自行实现，记得保护好锁，如果不会写，问AI。（~~我怕查重这里就不放代码了~~）

但是有个tricky的并发问题。

```c
void *tcp_timer_thread(void *arg)
{
	init_list_head(&timer_list);
	while (1) {
		usleep(TCP_TIMER_SCAN_INTERVAL);
		tcp_scan_timer_list();
	}

	return NULL;
}
```

如果你像我这样在这里init\_list\_head并且用的是static 变量,有可能出现并发问题，即先调用了tcp\_set\_retrans\_timer时还没有调用tcp\_timer\_thread导致seg fault。

我的解决方法是在static声明时就初始化（当然，这不是一个很好的工程代码）

```c
static struct list_head timer_list = {&timer_list, &timer_list};
static pthread_mutex_t timer_list_lock= PTHREAD_MUTEX_INITIALIZER;
```

### 发送队列维护

还是一样，让AI给你写send\_buffer\_entry这个struct可能比你写得准确。（误）

记得在alloc\_tcp\_sock补上你增加的一系列新struct（timer,buffer等）的初始化代码。（free\_tcp\_sock同步修改）

然后实现三个函数，按照注释即可。（附部分代码）

```c
void tcp_send_buffer_add_packet(struct tcp_sock *tsk, char *packet, int len)
{
    pthread_mutex_lock(&tsk->send_buf_lock);

    // 1.创建新的发送缓冲区条目
   
   
    // 2.获取报文序号
    struct tcphdr *tcp = packet_to_tcp_hdr(packet);
    entry->seq = ntohl(tcp->seq);
  // log(DEBUG,"2");
    // 3.添加到发送队列尾部
  //  log(DEBUG,"3");
    // 4.设置重传定时器，可以写在这里也可以不写外面调用
    //tcp_set_retrans_timer(tsk);

    pthread_mutex_unlock(&tsk->send_buf_lock);
}

int tcp_update_send_buffer(struct tcp_sock *tsk, u32 ack)
{
    pthread_mutex_lock(&tsk->send_buf_lock);
    
    struct send_buffer_entry *entry, *q;
    int updated = 0;
    
    list_for_each_entry_safe(entry, q, &tsk->send_buf, list) {
        
            // 1.数据已被确认，移除该条目
            
        }
    
    
    // 2.如果队列为空，取消重传定时器
    if (list_empty(&tsk->send_buf))
        tcp_unset_retrans_timer(tsk);
    
    pthread_mutex_unlock(&tsk->send_buf_lock);
    return updated;
}

int tcp_retrans_send_buffer(struct tcp_sock *tsk)
{
    pthread_mutex_lock(&tsk->send_buf_lock);
    
    if (list_empty(&tsk->send_buf)) {
        pthread_mutex_unlock(&tsk->send_buf_lock);
        return -1;
    }
    
    // 1.获取队列头部的数据包
  
    
    // 2.创建新的数据包用于重传
    
    //3. 更新TCP头部
  
    
    // 4.发送重传包
    ip_send_packet(new_packet, entry->len);
    
    pthread_mutex_unlock(&tsk->send_buf_lock);
    return 0;
}
```

### 怎么调用这堆函数

1.tcp\_set\_retrans\_timer和tcp\_send\_buffer\_add\_packet（注意死锁）

这两个是连体的，参考实验文档，只应该出现在tcp\_send\_packet和tcp\_send\_control\_packet的函数里。

```c
// 只为数据包添加重传
    if (tcp_data_len > 0) {
        tcp_send_buffer_add_packet(tsk, packet, len);
        tcp_set_retrans_timer(tsk);
    }
```

```c
if ((flags & TCP_SYN)||(flags &TCP_FIN)) {
		   
			tcp_send_buffer_add_packet(tsk, packet, pkt_size);
			//log(DEBUG,"start retran");
			tcp_set_retrans_timer(tsk);
           // log(DEBUG,"set succeed");
		}
```

在两个函数调用ip\_send\_packet前分别加这些代码即可，其他地方不应该出现这两个函数的身影。

2.tcp\_unset\_retrans\_timer:

在tcp\_update\_send\_buffer里可能使用。

```c
 // 如果队列为空，取消重传定时器
    if (list_empty(&tsk->send_buf))
        tcp_unset_retrans_timer(tsk);
    
```

在tcp\_update\_retrans\_timer删除定时器会用到。

```c
if (list_empty(&tsk->send_buf)) {
        // 发送队列为空,删除定时器
        tcp_unset_retrans_timer(tsk);
        wake_up(tsk->wait_send);
    } 
```

在tcp\_process处理更新数据时会用到，基本上有tcp\_update\_window的地方就有它。

对我的实现逻辑出现在这些地方：

<pre class="language-c"><code class="lang-c"><strong>1.case TCP_SYN_RECV:
</strong>          // log(DEBUG,"SYN_RECV");
            if (cb->flags &#x26; TCP_ACK) {
2.case TCP_SYN_SENT:
           // log(DEBUG,"SYN_SENT");
            if (cb->flags &#x26; (TCP_SYN|TCP_ACK)) {
 3.case TCP_TIME_WAIT:
            if (cb->flags &#x26; TCP_FIN&#x26;&#x26;!(cb->flags&#x26;TCP_ACK))
</code></pre>

每个人的处理逻辑不同不能照搬，请自行思考什么时候要重设重传定时器。

这里除了死锁还要注意你的free\_tcp\_sock的被调用，如果你的这个函数调用错了，容易free掉你的socket导致seg fault(所以你还要检查你的free\_tcp\_sock是否正确，重点在于ref\_count计数是否正确)。

3.tcp\_update\_retrans\_timer:只应该出现在数据传输阶段client端收到ACK时

```c
case TCP_ESTABLISHED:
else if ((cb->flags & TCP_ACK)&&cb->pl_len==0) {
```

4.tcp\_update\_send\_buffer:收到了ACK时，包括了SYN和FIN的ACK，所以你的握手和挥手要改。

我的逻辑出现在这些地方：

```c
1.case TCP_SYN_SENT:
           // log(DEBUG,"SYN_SENT");
            if (cb->flags & (TCP_SYN|TCP_ACK)) {
2.case TCP_ESTABLISHED:
else if ((cb->flags & TCP_ACK)&&cb->pl_len==0) {
3. case TCP_LAST_ACK:
            if (cb->flags & TCP_ACK) {
4.case TCP_FIN_WAIT_1:
             if(cb->flags &(TCP_ACK | TCP_FIN)){

```

有这个函数紧跟着的就得调用tcp\_update\_window\_safe更新窗口，所以你也知道什么时候update\_window了。

5.tcp\_retrans\_send\_buffer:只在tcp\_scan\_timer\_list检测到需要重传时被调用，其他地方不应该出现。

接收队列函数前面已讲，不再赘述。

### 修改你的tcp\_apps.c

你需要修改一下收发逻辑保证你的文件收发正常，因为多了很多队列和buffer。

如果你用了tcp\_sock\_write和tcp\_sock\_read封装，那你就要修改这两个封装的函数。

比如我的tcp\_sock\_write加了这段：

```c

    int total_sent = 0;
    int remaining = len;

    while (remaining > 0) {
        pthread_mutex_lock(&tsk->sk_lock);
        
        // 检查发送窗口
        while (!tcp_tx_window_test(tsk)) {
            pthread_mutex_unlock(&tsk->sk_lock);
            sleep_on(tsk->wait_send);
            if (tsk->state != TCP_ESTABLISHED) {
                return -1;
            }
            pthread_mutex_lock(&tsk->sk_lock);
        }
```

具体逻辑每个人实现的不一样，请根据自己的逻辑合理调整。

### 测试所有的样例

如果你以上的所有东西都实现正确了，那么你应该所有的测试都不会出现问题啦！

否则，你就要抓包或者打log来调试。

不过有一个小问题，来自于server端用python,client端用tcp\_stack,如果你一直用一个port(每次只Makeclean 再Make，但是mnexec不重置反复测试，第二次这个测试就会fail)。

如果你抓包看，就会看到python端握手发的syn|ack包显示port reused,这是它的问题，重复用同一个端口发，所以不要管它，会引发一系列Bug。

<figure><img src="/files/3P1kNpEpM45eYsQfy2Ec" alt=""><figcaption><p>port numbers reused</p></figcaption></figure>

## 最后：一些小细节

1.你之前的握手和挥手很可能不是完全正确符合TCP协议栈的。

举个例子，

```c
case TCP_SYN_SENT:
           // log(DEBUG,"SYN_SENT");
            if (cb->flags & (TCP_SYN|TCP_ACK)) {
                    // 更新接收序号：对方的序号+1(SYN占一个序号)
                   tsk->rcv_nxt = cb->seq+1;   
                    // 更新发送序号
                    tcp_update_send_buffer(tsk, cb->ack);
                    // 更新窗口等参数
                    tcp_unset_retrans_timer(tsk);
                    tcp_update_window_safe(tsk, cb);
                    tsk->snd_nxt = tsk->snd_una;  // 下一个发送序号
                    // 切换状态并发送ACK
                    tcp_set_state(tsk, TCP_ESTABLISHED);
                    tcp_send_control_packet(tsk, TCP_ACK);
                    
                    wake_up(tsk->wait_connect);
                
            }
            break;
```

如果不更新rcv\_nxt和snd\_nxt可能会被python端回复错报文，想想为什么？

并且为什么rcv\_nxt是cb->seq+1而不是cb->seq\_end?想想为什么！

2.你的free正确吗？

现在有多个计时器都在运行，你的unset是否正确调用？free了几次？请谨慎对待内存泄露问题。

3.你的封装严谨吗？

是否有封装函数外的代码做了其他的事情影响了你的逻辑？（比如snd\_una被update\_window以外的函数修改了）

4.关于实验文档里的3.4一些特殊情况

我自己是没遇到1和2两点，如果你遇到了自己修改一下即可。

第三点其实实验二就有，用pl\_len判断。

5.我有时能传对有时传不对，为什么？

如果你在做3.1，那可能是你的报文有时候乱序到达，不用管继续做。

如果你都写完了，可能是锁没设好偶然出现并发Bugs或者偶然丢包没事或者你的连接握手不对，运气好连上了，这些都能打Log和抓包解决。

牢记：问题出在你身上，不在python端。

6.我代码跑的太慢了

把你的log和printf都注释掉，然后如果还不行把sk\_lock这个大锁删了赌运气。


---

# 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-san.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.
