Yra's blog Yra's blog
Homepage
  • LeetCode周赛
  • Acwing周赛
  • 刷题整理
  • 题解
  • CPP
  • Golang
  • System

    • MIT6.S081 Labs
  • Computer Networking

    • CS144: Computer Networking
  • DataBase

    • MySQL
    • Redis
  • Others

    • ......
  • Paper Reading

    • Petri Net
  • Static Analysis

    • NJU Course Notes
  • Deep Learning

    • Dive into Deep Learning
Casual Records
Archives

Yra

Only a vegetable dog.
Homepage
  • LeetCode周赛
  • Acwing周赛
  • 刷题整理
  • 题解
  • CPP
  • Golang
  • System

    • MIT6.S081 Labs
  • Computer Networking

    • CS144: Computer Networking
  • DataBase

    • MySQL
    • Redis
  • Others

    • ......
  • Paper Reading

    • Petri Net
  • Static Analysis

    • NJU Course Notes
  • Deep Learning

    • Dive into Deep Learning
Casual Records
Archives
  • System

  • Computer Networking

    • CS144:Computer Networking

      • Lab0:Networking warmup
      • Lab1:stitching substrings into a byte stream
      • Lab2:The TCP Receiver
      • Lab3:The TCP Sender
      • Lab4:The TCP Connection
    • DataBase

    • Software Engineering

    • Others

    • Learning Notes
    • Computer Networking
    • CS144:Computer Networking
    Yra
    2023-03-18
    目录

    Lab4:The TCP Connection

    # Lab4:The TCP Connection

    lab4.pdf (cs144.github.io) (opens new window)

    在这个 lab 中,我们要站在全局的视角上,利用之前实现的 Sender 和 Receiver 来实现 TCP 的连接。

    过程中参考了一部分 Stanford-CS144-Sponge 笔记 - Lab 4: The TCP Connection | 吃着土豆坐地铁的博客 (opens new window)

    首先在做这个 lab 之前,我们必须对 Sender、Receiver、Connection 的状态机十分熟悉,可以说这个 lab 就是对着他们的 FSM 图来进行实现的。

    做之前可以看一下下面几篇文章:

    • TCP状态机 - 简书 (jianshu.com) (opens new window)

    • 终于搞懂了 TCP 的 11 种状态 ,太不容易了… - 腾讯云开发者社区-腾讯云 (tencent.com) (opens new window)

    • TCP State Transitions (opens new window)

    下面是我自己画的一张 TCP 连接的状态机,一共 11 个状态(字有点丑):


    7f65192b74f922330d05a741e5eb1f6e

    每一个状态都将由 Sender、Receiver 和 Connection 三者的状态唯一定义,我们将在 segment_received() 对其实现。

    下面先来实现一下其他的一些函数。

    先是写入数据,也就是我方的 Sender 需要发送的数据。

    size_t TCPConnection::write(const string &data) { // 返回写入长度
        if (data.empty()) {
            return 0;
        }
        size_t res = _sender.stream_in().write(data); // 往发送方的 bytestream 写入数据
        _sender.fill_window(); // 根据接收窗口发送数据,数据放进 _segments_out
        send_data(); // 发送数据到 receiver
        return res;
    }
    

    而其中的 send_data() 执行了发送的操作,同时还更新了一下我方 Receiver 的操作。具体来说,我们会将 Sender 发送队列中的数据移到 Connection 的队列中;另外,至于后面的发送传输就不是传输层所关心的事了。

    void TCPConnection::send_data() {
        // 发送 sender 的信息
        while (!_sender.segments_out().empty()) { // 将 sender 发送队列中的数据放入 connection 的发送队列中
            auto seg = _sender.segments_out().front();
            _sender.segments_out().pop();
            if (_receiver.ackno().has_value()) { // 将自己这里的 receiver 的数据写入首部
                seg.header().ack = true;
                seg.header().ackno = _receiver.ackno().value();
                seg.header().win = _receiver.window_size() > std::numeric_limits<uint16_t>::max() ? std::numeric_limits<uint16_t>::max() : _receiver.window_size(); // 限定最大值
                
            }
            segments_out().emplace(seg); // 放入 connection 的发送队列
        }
    
        // 如果对方的发送完毕了
        if (_receiver.stream_out().input_ended()) {
            if (_sender.stream_in().eof() == false) {// 我方的发送还未结束(被动连接)
                _linger_after_streams_finish = false; // 不用进入 TIME_WAIT
            } else if (_sender.stream_in().eof() == true && _sender.bytes_in_flight() == 0) { // 我方已经发送完了,且全送达了
                // 如果我方不用进入 TIME_WAIT 或者 我方 TIME_WAIT 结束
                if (_linger_after_streams_finish == false || _time_since_last_segment_received >= 10 * _cfg.rt_timeout)
                _active = false; // 我方关闭连接 cleanly
            }
        }
    }
    

    接下来我们要实现计时的函数,其维护了一个变量,来记录距离上一次传输的报文被接受的时间间隔

    void TCPConnection::tick(const size_t ms_since_last_tick) {
        if (_active == false) return;
    
        _time_since_last_segment_received += ms_since_last_tick;
        _sender.tick(ms_since_last_tick);
    
        if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) { // 重传次数超过上限
            reset_connection(); // 设置 RST
            return;
        }
        send_data();
    }
    

    还有设置 RST 位的函数,我们将在适当时候调用该函数,来设置并发送 RST 报文段和设置 error,以及关闭连接,例如重传次数超过上限、unclearly 地关闭连接

    void TCPConnection::reset_connection() {
        // 发送 RST 段
        TCPSegment seg;
        seg.header().rst = true;
        segments_out().emplace(seg);
    
        // 标记错误 和 设置 _active
        _sender.stream_in().set_error();
        _receiver.stream_out().set_error();
        _active = false; 
    }
    

    下面是其他的一些比较简单的函数,就不多做解释了

    bool TCPConnection::active() const {
        return _active; 
    }
    size_t TCPConnection::remaining_outbound_capacity() const {
        return _sender.stream_in().remaining_capacity(); 
    }
    
    size_t TCPConnection::bytes_in_flight() const {
        return _sender.bytes_in_flight();
    }
    
    size_t TCPConnection::unassembled_bytes() const {
        return _receiver.unassembled_bytes(); 
    }
    
    size_t TCPConnection::time_since_last_segment_received() const {
        return _time_since_last_segment_received; 
    }
    

    最后就是重头 segment_received() 函数。

    上文说过我们通过 sender、receiver、connection 状态来定义唯一的连接状态。

    其中 sender 和 receiver 的状态定义在之前的实验手册中已经有给出了,下面我们来简单谈一谈 connection 的状态以及八股重灾区 TCP 连接的 3 次握手和 4 次挥手。

    在端系统之间利用 TCP 进行通信之前,会先建立 TCP 的连接,其通过 3 次握手来实现,下面是一张来自自顶向下书上的图。

    image-20230318223519199

    客户主机先主动发起连接,他初始化了一个数据的起始序号 client_isn,并标记了 SYN,具体含义在之前的 lab 中已经有解释过了。服务器接收到这个建立请求的报文段后,如果可以建立,那么他就会为这段连接分配相应的资源,此时服务器是 Passive Open,而这第一个报文段的发送也就是第一次握手;

    之后服务器会对这个报文段进行确认,并发送对应的 ACK 确认报文段 client_isn + 1 。与此同时 TCP 是双向的连接,因此服务器也要向客户主机发起一个连接的请求,他会进行和客户主机第一次握手类似的行为,初始化了数据的起始序号 server_isn。然而,前面所说的这两件事可以合并在一起做,即服务器主机发起第二次握手,其用来确认客户主机的连接请求和发起由服务器向客户主机的连接,这个合并操作也被称为捎带;

    同理,客户主机也要对接受到的来自服务器的连接请求报文段发送 ACK 确认 server_isn + 1,同时还可以捎带客户要发送给服务器的数据,这被称为第三次握手。

    至此,C/S 双方便建立起了 TCP 连接,可以依此来传输数据了。

    而当数据传输结束,那么对应的也需要拆除连接,也就需要四次挥手了,下面也是一张来自自顶向下书上的图片:

    image-20230318225126031

    如图所示,客户主动先发送带有 FIN 的报文段来主动结束,此时服务器是 Passive Close,并回复对应的 ACK 报文段;而随后当服务器的数据页传输结束后,它也会向客户发送带有 FIN 的报文段,客户也会回复对应的 ACK 报文段,这一来一回一共四回,被称为四次挥手。

    上面的描述会让 TCP 关闭连接上看上去非常简单,但事实却不是如此,在客户等待由服务器发送回来的 ACK 时,还可能会接收到来此服务器的 FIN,抑或是同时接受到 ACK 和 FIN...而这些都会引起连接进入不同的状态。在这里我们先关注图中有一个特别的部分 “定时等待”,客户此时也就对应着状态机中的 TIME_WAIT 状态。

    为什么要设置定时等待呢?

    试想如果在接受到 FIN 报文段,并发送 ACK 报文段之后,客户直接关闭连接会有什么事情发生呢?很显然客户发送的 ACK 未必能保证到达目的服务器,那么服务器长时间未能接收到 ACK 的话,它就会不断的重复发送 FIN 报文段,当重复次数到达上限后就会设置 RST 以及 ERROR,并且 unclealy 地关闭连接。此时服务器的 FIN 其实已经被正确接受了,但最终却是以错误的方式关闭了连接,而这显然不是我们想要的结果。

    为了尽可能地避免这种现象,在主动关闭连接的一方会在此时进入定时等待状态,其就像个无情的回复机器,一旦接收到 FIN 就会回复对应的 ACK 确认报文段。

    需要注意的是,这种现象只能尽可能的避免,具体可以先了解一下两军问题 (opens new window),因为不能保证发出去的所有 ACK 都能被接受,只要连接关闭依赖于最后一次 ACK,那么就不能完美解决此问题。

    在上文中,端系统是否要进入定时等待,即 TIME_WAIT 状态,就用变量 _linger_after_streams_finish 来表示,其值初始默认为 true,而这也就是 connection 的状态部分。

    接下来我们对 TCP 连接过程中的 11 种状态以及出现错误的 1 种状态进行定义(可以直接参考 tcp_state.cc )

    TCP 连接状态 Receiver 状态 Sender 状态 Connection 状态
    LISTEN LISTEN CLOSED TRUE
    SYN_REVD SYN_REVD SYN_SENT TRUE
    SYN_SENT LISENT SYN_SENT TRUE
    ESTABLISHED SYN_REVD SYN_ACKED TRUE
    CLOSE_WAIT FIN_REVD SYN_ACKED FALSE
    LAST_ACK FIN_REVD FIN_SENT FALSE
    FIN_WAIT_1 SYN_REVD FIN_SENT TRUE
    FIN_WAIT_2 SYN_REVD FIN_ACKED TRUE
    CLOSING FIN_REVD FIN_SENT TRUE
    TIME_WAIT FIN_REVD FIN_ACKED TRUE
    CLOSED FIN_REVD FIN_ACKED FALSE
    RESET ERROR ERROR FALSE

    另外,在 CLOSED 和 RESET 状态中,连接的状态 active 为 false,其余状态都为 true。

    了解了上面的内容后就可以开始实现 segment_received() 啦,我们要关注在不同状态下的接收到报文段的各种行为。

    // sender、receiver、connection 确定唯一状态,对照 tcp_state.cc
    void TCPConnection::segment_received(const TCPSegment &seg) {
        if (_active == false) return;
    
        // 获取 sender 状态
        auto get_sender_state = [&](const TCPSender &sender) {
            if (sender.stream_in().error()) {
                return "ERROR";
            } else if (sender.next_seqno_absolute() == 0) {
                return "CLOSED";
            } else if (sender.next_seqno_absolute() == sender.bytes_in_flight()) {
                return "SYN_SENT";
            } else if (not sender.stream_in().eof()) {
                return "SYN_ACKED";
            } else if (sender.next_seqno_absolute() < sender.stream_in().bytes_written() + 2) {
                return "SYN_ACKED";
            } else if (sender.bytes_in_flight()) {
                return "FIN_SENT";
            } else {
                return "FIN_ACKED";
            }
        };
    
        // 获取 receiver 状态
        auto get_receiver_state = [&](const TCPReceiver &receiver) {
            if (receiver.stream_out().error()) {
                return "ERROR";
            } else if (not receiver.ackno().has_value()) {
                return "LISTEN";
            } else if (receiver.stream_out().input_ended()) {
                return "FIN_RECV";
            } else {
                return "SYN_RECV";
            }
        };
    
    
        std::string sen_st = get_sender_state(_sender);
        std::string rev_st = get_receiver_state(_receiver);
    
        _time_since_last_segment_received = 0; // 重置时间
    
        // 收到 RST 段
        if (seg.header().rst == true) { 
            _sender.stream_in().set_error();
            _receiver.stream_out().set_error();
            _active = false; 
        } 
        // LISTEN
        else if (rev_st == "LISTEN" && sen_st == "CLOSED" && _linger_after_streams_finish == true) {
            // 被动连接 Passive open(进入SYN_REVD)
            if (seg.header().syn) {
                _receiver.segment_received(seg);
                connect(); // 会发送 ACK 和 SYN
            }   
        }
        // SYN_REVD
        else if (rev_st == "SYN_RECV" && sen_st == "SYN_SENT" && _linger_after_streams_finish == true) 
        {
            // sender 接受 ACK(进入ESTABLISHED)
            _receiver.segment_received(seg);
            _sender.ack_received(seg.header().ackno, seg.header().win);
        }
        // SYN_SENT
        else if (rev_st == "LISTEN" && sen_st == "SYN_SENT" && _linger_after_streams_finish == true) 
        {
            // client 收到 ACK 和 SYN,发送 ACK 后进入 ESTABLISHED
            if (seg.header().ack == true && seg.header().syn == true) {
                _sender.ack_received(seg.header().ackno, seg.header().win);
                _receiver.segment_received(seg);
                _sender.send_empty_segment(); // 通过空包,来发送 ACK
                send_data();
            }       
            // client 也作为 server,收到了 SYN,发送 SYN 和 ACK 后进入 SYN_REVD
            else if (seg.header().syn == true && seg.header().ack == false) {
                _receiver.segment_received(seg);
                _sender.send_empty_segment();
                send_data();
            }
        }
        // ESTABLISHED
        else if (rev_st == "SYN_RECV" && sen_st == "SYN_ACKED" && _linger_after_streams_finish == true) {
            _sender.ack_received(seg.header().ackno, seg.header().win);
            _receiver.segment_received(seg);
            if (seg.length_in_sequence_space() > 0) {
                _sender.send_empty_segment(); // 发送 ACK
            }
            _sender.fill_window();
            send_data();
        }
        // FIN_WAIT_1(发送完毕,但接受未完毕)
        else if (rev_st == "SYN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == true) {
            // 收到 FIN,进入 CLOSING
            if (seg.header().fin == true && seg.header().ack == false) {
                _sender.ack_received(seg.header().ackno, seg.header().win);
                _receiver.segment_received(seg);
            } 
            // 收到 FIN、ACK,发送 ACK,进入 TIME_WAIT
            else if (seg.header().fin == true && seg.header().ack == true) {
                _sender.ack_received(seg.header().ackno, seg.header().win);
                _receiver.segment_received(seg);
                _sender.send_empty_segment();
                send_data();
            }
            // 收到 ACK,进入 FIN_WAIT_2
            else if (seg.header().fin == false && seg.header().ack == true) {
                _sender.ack_received(seg.header().ackno, seg.header().win);
                _receiver.segment_received(seg);
                send_data();
            }
        }
        // FIN_WAIT_2,收到 FIN,发送 ACK,进入 TIME_WAIT
        else if (rev_st == "SYN_RECV" && sen_st == "FIN_ACKED" && _linger_after_streams_finish == true) {
            _sender.ack_received(seg.header().ackno, seg.header().win);
            _receiver.segment_received(seg);
            _sender.send_empty_segment();
            send_data();
        }
        // TIME_WAIT
        else if (rev_st == "FIN_RECV" && sen_st == "FIN_ACKED" && _linger_after_streams_finish == true) {
            // 收到 FIN 的话就发送 ACK(不断开连接)
            if (seg.header().fin == true) {
                _sender.ack_received(seg.header().ackno, seg.header().win);
                _receiver.segment_received(seg);
                _sender.send_empty_segment();
                send_data();
            }
        }
        // CLOSE_WAIT
        else if (rev_st == "FIN_RECV" && sen_st == "SYN_ACKED" && _linger_after_streams_finish == false) {
            if (seg.header().fin) { // 此状态接收到 FIN 报文段说明需要重传
                _sender.send_empty_segment();
            }
            // 继续发送数据
            _sender.ack_received(seg.header().ackno, seg.header().win);
            _receiver.segment_received(seg);
            send_data();
        }
        // LAST_ACK
        else if (rev_st == "FIN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == false) {
            _sender.ack_received(seg.header().ackno, seg.header().win);
            _receiver.segment_received(seg);
            if (_sender.bytes_in_flight() == 0) {
                _active = false;
            }
        }
        // CLOSING
        else if (rev_st == "FIN_RECV" && sen_st == "FIN_SENT" && _linger_after_streams_finish == true) {
            _sender.ack_received(seg.header().ackno, seg.header().win);
            _receiver.segment_received(seg);
            send_data();
        }
    }
    

    由于测试样例很多,会有很多的 corner case......

    有一个点我调了很久,也就是在进入 CLOSE_WAIT 状态后,此时如果接受到的报文段带有 FIN,那么我们要重传 ACK 确认报文段,其说明我们先前传过的 ACK 没能正确到达对方。

    # 实验结果


    5998267a1de03775a465ea649a07c024

    (TEST 我是在 win 上用 WSL2 挂代理跑的,不然很有可能会 TIME_OUT)

    #Learning Notes#Computer Networking#CS144:Computer Networking
    Last Updated: 3/18/2023, 11:39:18 PM
    Lab3:The TCP Sender
    MySQL 问题整理

    ← Lab3:The TCP Sender MySQL 问题整理→

    最近更新
    01
    408 计组笔记
    07-19
    02
    Dive into Deep Learning
    01-27
    03
    25 考研随记
    11-27
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式