关于linux:CS144-Lab-Assignments-手写TCP-LAB4

32次阅读

共计 6920 个字符,预计需要花费 18 分钟才能阅读完成。

CS 144: Introduction to Computer Networking, Fall 2020
https://cs144.github.io/

My Repo
https://github.com/wine99/cs1…

工作

本节实现 TCPConnection 类,实例化这个类将作为一个残缺的 TCP 连贯中的一个 peer(能够充当任意一方,Server 或 Client)。后面两个试验别离实现的 TCPSender 和 TCPReceiver 并不能作为一个独立的 Server 或 Client,这两个类的实例是用作 TCPConnection 实例的外部成员,即下图。

Sender 和 Receiver 的作用

  • 收到报文段时

    • 告诉 _receiver:依据报文段的 seqno、SYN、FIN 和 payload,以及以后状态,更新 ackno;收集数据
    • 告诉 \_sender:依据报文段的 ackno 以及以后状态,更新 next_seqno;更新 window_size
  • 发送报文段时

    • \_sender 负责填充 payload、seqno、SYN、FIN,留神有可能既没有 payload 也没有 S、F 标识(empty segment),这和 Lab3 实现的 \_sender 的 ack_received() 逻辑不同
    • \_receiver 负责填充 ackno、window size

FSM

联合 Lab2、Lab3 讲义中的 TCPSender 和 TCPReceiver 的状态转换图,tcp_state.cc 中 TCPConnection 的各状态与 sender、receiver 状态的对应关系,以及上面的 TCPConnection 的状态转换图,了解整个 TCP 连贯。

Edge case

在实现过程中,须要额定关注收到报文段时 TCPSender 和 TCPConnection 的逻辑的不同之处。这些细节来源于

  1. Lab2 中的 receiver 只关怀收到数据和数据无关的标识;Lab3 中 sender 只关怀收到的 ackno 和 win,不解决也不晓得收到的数据和其余信息,在 \_stream_in() 没有数据时可能不会做任何动作(我的 Lab3 实现是这样的),而在 Lab4 中可能还须要发一个空的 ACK 报文段
  2. 连贯建设和开释过程中的各种非凡状况

    1. 发完 SYN 后马上收到 RST
    2. 发完 SYN 后马上收到 FIN
    3. Simultaneous open
    4. Simultaneous shutdown

试验给出的测试套十分齐备,笼罩了各种非凡状况,Simultaneous open 和 Simultaneous shutdown 的状况见下图。依照讲义所说,如果你的 Lab2 和 Lab3 实现十分 robust,Lab4 的大部分工作是 wire up 后面两个类的接口,但也有可能你须要批改前两个试验的实现。

下图出处:TCP State Transitions

实现

我的试验四的函数框架参考了 这篇博客,但实现不同。我在网上浏览过的几个实现,均改变了 Lab2、Lab3 的函数签名,让 Lab2、Lab3 的实现变得不太洁净。我的最终实现没有入侵 Lab3 和 Lab2 的代码,细节逻辑全副在 TCPConnection 类中实现。

留神如果 tests 文件夹中的测试全副通过然而 txrx.sh 中的测试不通过,并且不通过的起因是后果的哈希值不同,去掉所有的本人增加的打印语句,再进行测试。

试验四刚开始时一度想要放弃,但最终破费的工夫竟然比试验三要少(试验三零零碎碎花了六天左右,试验四大略破费了集中的两天半工夫)。通过全副测试的时候,还感觉有点懵逼,怎么就通过了,我真的把细节都解决完了?第一次意识到,简单的我的项目中,齐备的测试比“充斥自信”的实现代码牢靠多了,也不得不感叹课程品质之高以及讲师和助教付出的心血。

代码

增加的成员变量

class TCPConnection {
  private:
    size_t _time_since_last_segment_received{0};
    bool _active{true};

    void send_sender_segments();
    void clean_shutdown();
    void unclean_shutdown();

实现代码

#include "tcp_connection.hh"

#include <iostream>

using namespace std;

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;}

bool TCPConnection::active() const { return _active;}

void TCPConnection::segment_received(const TCPSegment &seg) {if (!_active)
        return;
    _time_since_last_segment_received = 0;
    // State: closed
    if (!_receiver.ackno().has_value() && _sender.next_seqno_absolute() == 0) {if (!seg.header().syn)
            return;
        _receiver.segment_received(seg);
        connect();
        return;
    }
    // State: syn sent
    if (_sender.next_seqno_absolute() > 0 && _sender.bytes_in_flight() == _sender.next_seqno_absolute() &&
        !_receiver.ackno().has_value()) {if (seg.payload().size())
            return;
        if (!seg.header().ack) {if (seg.header().syn) {
                // simultaneous open
                _receiver.segment_received(seg);
                _sender.send_empty_segment();}
            return;
        }
        if (seg.header().rst) {_receiver.stream_out().set_error();
            _sender.stream_in().set_error();
            _active = false;
            return;
        }
    }
    _receiver.segment_received(seg);
    _sender.ack_received(seg.header().ackno, seg.header().win);
    // Lab3 behavior: fill_window() will directly return without sending any segment.
    // See tcp_sender.cc line 42
    if (_sender.stream_in().buffer_empty() && seg.length_in_sequence_space())
        _sender.send_empty_segment();
    if (seg.header().rst) {_sender.send_empty_segment();
        unclean_shutdown();
        return;
    }
    send_sender_segments();}

size_t TCPConnection::write(const string &data) {if (!data.size())
        return 0;
    size_t write_size = _sender.stream_in().write(data);
    _sender.fill_window();
    send_sender_segments();
    return write_size;
}

//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {if (!_active)
        return;
    _time_since_last_segment_received += ms_since_last_tick;
    _sender.tick(ms_since_last_tick);
    if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS)
        unclean_shutdown();
    send_sender_segments();}

void TCPConnection::end_input_stream() {_sender.stream_in().end_input();
    _sender.fill_window();
    send_sender_segments();}

void TCPConnection::connect() {_sender.fill_window();
    send_sender_segments();}

TCPConnection::~TCPConnection() {
    try {if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";
            _sender.send_empty_segment();
            unclean_shutdown();}
    } catch (const exception &e) {std::cerr << "Exception destructing TCP FSM:" << e.what() << std::endl;
    }
}

void TCPConnection::send_sender_segments() {
    TCPSegment seg;
    while (!_sender.segments_out().empty()) {seg = _sender.segments_out().front();
        _sender.segments_out().pop();
        if (_receiver.ackno().has_value()) {seg.header().ack = true;
            seg.header().ackno = _receiver.ackno().value();
            seg.header().win = _receiver.window_size();
        }
        _segments_out.push(seg);
    }
    clean_shutdown();}

void TCPConnection::unclean_shutdown() {// When this being called, _sender.stream_out() should not be empty.
    _receiver.stream_out().set_error();
    _sender.stream_in().set_error();
    _active = false;
    TCPSegment seg = _sender.segments_out().front();
    _sender.segments_out().pop();
    seg.header().ack = true;
    if (_receiver.ackno().has_value())
        seg.header().ackno = _receiver.ackno().value();
    seg.header().win = _receiver.window_size();
    seg.header().rst = true;
    _segments_out.push(seg);
}

void TCPConnection::clean_shutdown() {if (_receiver.stream_out().input_ended()) {if (!_sender.stream_in().eof())
            _linger_after_streams_finish = false;
        else if (_sender.bytes_in_flight() == 0) {if (!_linger_after_streams_finish || time_since_last_segment_received() >= 10 * _cfg.rt_timeout) {_active = false;}
        }
    }
}

性能优化

剖析

因为没有做过 profiling,性能剖析的工作抄了下面提到的博客的作业。

批改 sponge/etc/cflags.cmake 中的编译参数,将 -g 改为-Og -pg,使生成的程序具备分析程序可用的链接信息。

make -j8
./apps/tcp_benchmark
gprof ./apps/tcp_benchmark > prof.txt

如讲义中所说,很可能须要改变 ByteStream 或 StreamReassembler。调优办法是利用 buffer.h 中提供的 BufferList。实际上测试代码中就有用到 BufferList,简而言之它是一个 deque\<Buffer\>,而 Buffer 则在整个实现与测试代码中被大量应用,例如 payload() 就是一个 Buffer 实例。

改变

把 ByteStream 类中字节流的容器由 Lab0 最后的 std::list<char> _stream{}; 改为 BufferList _stream{};

byte_stream.cc 改变的函数:

size_t ByteStream::write(const string &data) {size_t write_count = data.size();
    if (write_count > _capacity - _buffer_size)
        write_count = _capacity - _buffer_size;
    _stream.append(BufferList(move(string().assign(data.begin(), data.begin() + write_count)))); 
    _buffer_size += write_count;
    _bytes_written += write_count;
    return write_count;
}

//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
    const size_t peek_length = len > _buffer_size ? _buffer_size : len;
    string str = _stream.concatenate();
    return string().assign(str.begin(), str.begin() + peek_length);
}

//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
    size_t pop_length = len > _buffer_size ? _buffer_size : len;
    _stream.remove_prefix(pop_length);
    _bytes_read += pop_length;
    _buffer_size -= pop_length;
}

改变后的 benchmark

webget revisited

间接依照讲义中的步骤,把 Linux 自带的 TCPSocket,换成咱们本人的实现。

void get_URL(const string &host, const string &path) {CS144TCPSocket sock1{};
    sock1.connect(Address(host, "http"));
    sock1.write("GET" + path + "HTTP/1.1\r\n" + "Host:" + host + "\r\n" + "Connection: close\r\n\r\n");
    while (!sock1.eof()) {cout << sock1.read();
    }
    sock1.shutdown(SHUT_WR);
    sock1.wait_until_closed();}

替换后 webget 仍然 work(不晓得为什么 WSL 替换后连贯建设不起来,但在云主机上测试后没有问题),至此,手写 TCP 正式实现。

正文完
 0