CS144 Lab:Lab3

目录

实现原则

头文件声明

tick函数实现

ack_received()函数实现

send_empty_segment()函数实现

fill_window()函数实现


该试验就是在之前实验的基础上实现TCPSender类。

实现原则

刚开始拿到这个类时,看着实验文档那么多的规定和限制,我也有点懵,但慢慢的还是理出来一些头绪。

  1. 该类负责TCP字节发送的大部分事务,包括超时重传,ack报文确认,SYN和FIN字段的设立,这些仅靠它自己能搞定的他都应该自己负责。
  2. 但该类并不负责TCP发送的全部事务。有些事务仅靠TCPSender解决不了,比如ack字段的设立,这需要TCPReceive的协调。这里可以发现有函数ack_received(),专门传递过来ackno和window_size,这肯定是从己方的TCPReceiver传递过来的,那么这个为什么要专门写个函数传递过来参数呢?我的思考是这部分确实应该是两者协调才能处理的,确实应该在TCPConnection中,但由于其处理逻辑比较复杂,而且需要TCPSender内部信息,不如直接弄成函数方便TCPConnection调用即可。再比如这里ack字段的设立为什么不需要弄成函数让TCPSender处理呢?我想是处理逻辑比较简单而且也不设计TCPSender内部信息。
  3. 怎样才算是真正的发送数据??文档里有答案,TCPSender类中有输出队列_segments_out,将要发送的TCPSegment放入队列即可。之后TCPConnection会从队列中取出并向真实网卡传递数据发送到真实网络中。
  4. 超时重传具体怎么实现??首先我们得知需要拿一个数据结构存储已经发送但尚未确认的TCPSegment,方便超时后我们重新传送。之后比较关键的一个点是定时器仅针对未确认队列的队头TCP段。这里没有复杂的选择重传,也不选择笨拙的回退N步,策略仅仅是未确认队列队头TCP段超时重传,根据接收方的ack号滑动发送窗口。随后搞清楚该试验具体告诉我们怎么实现超时重传,他给了一个tick函数,该函数代表的意思是TCPSender的所有者即TCPConnection会通过tick函数显性告知TCPSender自上次调用tick函数过去了多少时间,TCPSender自己获取时间信息的方式,只能通过TCPConnnection告知。那么我们需要一系列变量去实现这个计时器,首先需要_time_elapsed记录未确认队列队头已经经历多少时间,_rto记录目前的超时传输超时时间,_timer_running布尔变量记录计时器是否打开,以及TCPConnection需要的信息_retransmission_count记录连续重传次数。之后处理就比较简单了,累计计时器时间,超时时根据文档要求重新设置一些列变量状态,并且重新发送未确认队列队头TCP段即可。
  5. 这里采用deque结构存储未确认队列,原因是需要遍历队列去获取已经发送了多少字节,queue数据结构无法有效遍历。

头文件声明

具体头文件声明如下:

//! Accepts a ByteStream, divides it up into segments and sends the
//! segments, keeps track of which segments are still in-flight,
//! maintains the Retransmission Timer, and retransmits in-flight
//! segments if the retransmission timer expires.
class TCPSender {
  private:
    //发送方自己的isn
    //! our initial sequence number, the number for our SYN.
    WrappingInt32 _isn;

    //输出队列
    //! outbound queue of segments that the TCPSender wants sent
    std::queue<TCPSegment> _segments_out{};

    //未确认队列,这里用双向队列是为了可以便利,方便计算总字节数
    std::deque<TCPSegment> _outstanding_segments{};

    //初始RTO
    //! retransmission timer for the connection
    unsigned int _initial_retransmission_timeout;

    //还没有被发送的字节流
    //! outgoing stream of bytes that have not yet been sent
    ByteStream _stream;

    //标记是否已经发送过fin标记了,防止一直发送
    bool _fin_sent{false};

    //下一个要被发送的绝对序列号
    //! the (absolute) sequence number for the next byte to be sent
    uint64_t _next_seqno{0};

    uint16_t _window_size{1};

    //以下变量主要和计时器相关
    //目前rto的值
    size_t _rto;

    //目前计时器已经积累的时间
    size_t _time_elapsed{};

    //计时器是否打开
    bool _timer_running{};

    //连续重传次数
    size_t _retransmission_count{};

  public:
    //! Initialize a TCPSender
    TCPSender(const size_t capacity = TCPConfig::DEFAULT_CAPACITY,
              const uint16_t retx_timeout = TCPConfig::TIMEOUT_DFLT,
              const std::optional<WrappingInt32> fixed_isn = {});

    //! \name "Input" interface for the writer
    //!@{
    ByteStream &stream_in() { return _stream; }
    const ByteStream &stream_in() const { return _stream; }
    //!@}

    //! \name Methods that can cause the TCPSender to send a segment
    //!@{

    //! \brief A new acknowledgment was received
    void ack_received(const WrappingInt32 ackno, const uint16_t window_size);

    //! \brief Generate an empty-payload segment (useful for creating empty ACK segments)
    void send_empty_segment();

    //! \brief create and send segments to fill as much of the window as possible
    void fill_window();

    //! \brief Notifies the TCPSender of the passage of time
    void tick(const size_t ms_since_last_tick);
    //!@}

    //! \name Accessors
    //!@{

    //! \brief How many sequence numbers are occupied by segments sent but not yet acknowledged?
    //! \note count is in "sequence space," i.e. SYN and FIN each count for one byte
    //! (see TCPSegment::length_in_sequence_space())
    size_t bytes_in_flight() const;

    //! \brief Number of consecutive retransmissions that have occurred in a row
    unsigned int consecutive_retransmissions() const;

    //! \brief TCPSegments that the TCPSender has enqueued for transmission.
    //! \note These must be dequeued and sent by the TCPConnection,
    //! which will need to fill in the fields that are set by the TCPReceiver
    //! (ackno and window size) before sending.
    std::queue<TCPSegment> &segments_out() { return _segments_out; }
    //!@}

    //! \name What is the next sequence number? (used for testing)
    //!@{

    //! \brief absolute seqno for the next byte to be sent
    uint64_t next_seqno_absolute() const { return _next_seqno; }

    //! \brief relative seqno for the next byte to be sent
    WrappingInt32 next_seqno() const { return wrap(_next_seqno, _isn); }
    //!@}
};

tick函数实现

大部分实现激励在实现原则4中已经提到了,这里注意一个坑点,只有当接收方窗口大小不为0时才让RTO值翻倍和增加连续重传次数。当接收方口大小为0时,说明接收方缓存已满无法接受新的数据报,此时发送方只能每隔RTO的时间内发送“探测报文”,该报文通常带一个字节数据(强制接收方回复,不带任何数据的ACK报文即“纯确认报文”不强制接收方回复),尝试发送给接收方看接收方窗口是否已经恢复正常。

tick具体代码实现如下:

//计时功能,所有者告知TCP sender过去了多少时间,TCP sender应该自主负责所有超时事务
//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void TCPSender::tick(const size_t ms_since_last_tick) { 
    //如果计时器没打开,则直接返回
    if(!_timer_running) return;

    //累计计时器时间
    _time_elapsed += ms_since_last_tick;
    //超时,启动重传服务
    if(_time_elapsed >= _rto) {
        //先重设状态,之后拿出队头,重新传输
        _time_elapsed = 0;
        //这里只有窗口大小不为0时才可翻倍rto和增加连续重传次数。
        if(_window_size != 0) {
            _rto *= 2;
            _retransmission_count ++;
        }
        _segments_out.push(_outstanding_segments.front());
        return;
    }
}

ack_received()函数实现

接下来尝试实现ack函数,该函数接收接收方回应的ackno和窗口大小。

这个函数要完成两个任务,一个是要根据ackno清理未确认队列,第二个是更新窗口大小,之后尽可能填充窗口。

清理未确认队列,就是将所有被确认的tcp段pop出队列,这里注意只有当tcp段全部字节都被ack才被pop出未确认队列(文档里规定不分片,采取最简单的措施,部分字节被ack仍被认为整个未ack)。尽可能填充窗口就是更新窗口大小,随后调用fill_window函数。

//接收到的ackno 和 窗口大小
//! \param ackno The remote receiver's ackno (acknowledgment number)
//! \param window_size The remote receiver's advertised window size
void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) { 
    //首先检查ackno是否合规,
    if(next_seqno().raw_value() < ackno.raw_value()) return;

    //第一步,清理未确认队列中确认的tcp段
    while (!_outstanding_segments.empty()) {
        const TCPSegment tcp = _outstanding_segments.front();
        //这里用seqno+数据负载长度确认是否该tcp段全部数据都被ack了,注意syn,fin数据报比较特殊,要+1
        if(tcp.header().seqno.raw_value() + tcp.length_in_sequence_space() > ackno.raw_value()) 
            break;
        //将确认过的tcp段出队
        _outstanding_segments.pop_front();
        //更新时钟,累计传输次数和rto
        _time_elapsed = 0;
        _retransmission_count = 0;
        _rto = _initial_retransmission_timeout;
    }
    //如果未确认队列为空,则将计时器关闭。
    if(_outstanding_segments.empty()) _timer_running = false;

    //更新窗口信息并填充窗口,窗口大小至少为1
    _window_size = window_size;
    fill_window();
    
}

send_empty_segment()函数实现

先实现发送纯ACKTCP段的函数,熟悉一下如何创建seg并发送。

具体代码如下:

//发送一个空的tcp段,仅将seqno设置正确,不消耗seqno
void TCPSender::send_empty_segment() {
    //创建seg,且正确设置seqno
    TCPSegment seg;
    seg.header().seqno = next_seqno();
    //将其输出,且不需要跟踪。
    _segments_out.push(seg);
    return;
}

fill_window()函数实现

这个函数是TCPSender的核心函数,作用就是尽可能的填充接收方的窗口。

  1. 首先获取窗口大小,注意这里如果窗口大小为0,我们也要改为1,以便发送探测报文。(如果发送方听从接收方的安排将窗口大小改为0,则永远也无法发送数据,即使接收方窗口大小缓解了,发送方也不知道该消息。)
  2. 这里判断的条件是是未确认队列的字节数<窗口大小。未确认字节数就是我们已经发送的字节数,我们要做的就是未确认字节数等于窗口大小。
  3. 创建TCP段之后,构建内容,按照步骤来,先尝试写入SYN字段,后写入数据部分,最后写入FIN字段。这里面细节挺多的,注意SYN字段和FIN字段都占用一个字节且没必要在数据部分时就给FIN留位置,数据部分尽可能的塞,最后FIN能放就放不能放下个TCP段放。这里注意FIN字段可能被多次发送,当窗口大小很大时,该循环会多次写入FIN字段并发送空数据的TCP段,直至把窗口大小撑满,所以这里用_fin_sent标记是否发送过,只发送一次fin字节。syn字节没有这个问题,因为syn字节判断条件是_next_seqno == 0,这个值在第一次syn之后就会增加,所以syn只会发送一次。
  4. 之后查看构建的tcp段是否为空,为空说明数据部分没有可发送的数据流,直接退出循环。
  5. 否则,发送该tcp段,同时将它push进未确认队列,设定计时器并累加_next_seqno。

最终具体代码如下:

//填充窗口操作
void TCPSender::fill_window() {
    //这里确保window_size至少为1,可以发送syn探测报文
    uint16_t window_size = _window_size;
    if(!window_size) window_size = 1;

    //如果未确认队列总字节数 < 收到的窗口大小,且还有要发送的字节,就发送tcp段
    uint64_t tot_bytes;
    while((tot_bytes = bytes_in_flight()) < window_size) {
        //创建tcp段,并写入序列号
        TCPSegment seg;
        seg.header().seqno = next_seqno();

        //先写入syn字段
        if(_next_seqno == 0) {
            seg.header().syn = true;
            tot_bytes += 1;
        }        
        //接下来构造数据部分,这里bytes计算需要从stream中拿出多少,需要满足
        size_t bytes = min(window_size - tot_bytes, _stream.buffer_size());
        bytes = min(bytes, TCPConfig().MAX_PAYLOAD_SIZE);
        seg.payload() = Buffer(_stream.read(bytes));
        tot_bytes += bytes;

        //最后尝试写入fin字段?需要字节流结束并且当前字节还能容纳
        //坑点,这里需要注意之前没有发送过fin才行
        if(!_fin_sent && _stream.eof() && tot_bytes < window_size) {
            seg.header().fin = true;
            tot_bytes += bytes;
            _fin_sent = true;
        } 

        //如果是在没事干了,这就直接退出。
        if(seg.length_in_sequence_space() == 0) break;

        //放入输出队列和未确认队列中
        _segments_out.push(seg);
        _outstanding_segments.push_back(seg);
        //设定计时器
        if(!_timer_running) {
            _timer_running = true;
            _time_elapsed = 0;
        }
        _next_seqno += seg.length_in_sequence_space();
    }
}

其中用到的函数bytes_in_flight(),原理就是暴力遍历未确认队列所有tcp段,并累加其子节总数,具体代码如下:
 

//放回未确认队列中的总字节数
uint64_t TCPSender::bytes_in_flight() const { 
    //暴力便利队列中的每个字节段
    uint64_t tot_bytes = 0;
    for(const auto &seg : _outstanding_segments) 
        tot_bytes += seg.length_in_sequence_space();
    return tot_bytes;
}

最后实验成功截图,完结撒花!!!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值