CS144--Lab4笔记

CS144——Lab4笔记

Getting started

首先是版本控制操作,从lab3分支创建用于lab4开发的分支,我是在Clion的Git图形操作界面设定的。然后按照文档中的的介绍,从远程仓库拉取lab4的实验内容

//在新分支dev-lab4下
git fetch
git merge origin/lab4-startercode
make -j4

会弹出一个询问界面,让你提交此次合并到你本地仓库分支的说明commit,可以不管,直接ctrl+X键。
如果你的receiver和sender设计地足够鲁棒性,将有助于此Lab实现
前面任务完成的越多,现在需要做的越少。文档建议实现整个Lab4的代码函数大概在100~150行。本次实验就是将receiver和sender组合实现TCPConnection,其是一个全双工的协议,意思是作为端点的双方都是发送方和接收方。下图一目了然:

在这里插入图片描述

Lab4 : The TCP connection

TCPConnection 需要遵循如下基本规则:
接收数据段TCPConnection 将调用 segment_received() 函数从互联网上接收 TCPSegment ,且将查看该segment和报文段标志:

  • 如果设置了RST 标志位,则立即将 inboundoutbound 流置于 error state,且关闭连接。否则······
  • 将segment交给TCPReceiver,以便于检查其关心的数据域:seqno、SYN、payload、FIN
  • 如果设置了SYN标志位,则TCPSender只关心segment的数据域是:ackno、window_size
  • 如果接收到的TCPsegment包含一个有效的seqno,则TCPConnection必须至少返回一个segment作为告知远程端点此时自己的ackno和window size
  • 如果远端发送一个包含无效seqno的segment,则必须回复一个包含无效seqno的包,来确认双方是否有效。同时可以查看彼此的ackno和window size,这称为“keep-alive”机制。实现代码如下:
if(_receiver.ackno().has_value() and (seg.length_in_sequence_sapce()==0)and seg.header().seqno==_receiver.ackno().value()-1){
_sender.send_empty_segment();
}

发送数据段TCPConnection 将会通过互联网发送 TCPSegment

  • TCPSender 将一个 segment 添加到发送队列中时,TCPConnection 需要设置标志位:seqno、SYN、payload、FIN
  • 在发送segment前,TCPConnection 将会向自己 TCPReceiver 取得 acknowind_size ,并设置 ACK 标志

操作系统需要调用一个 tick() 函数来检测时间,当 TCPConnection 的tick()函数被调用后,它需要:

  • 告知 TCPSender 时间流逝,以便于重发没有被确认的包
  • 在条件满足情况下关闭TCP连接(处于 TIME_WAIT 状态时)
  • 当连续重传次数超过 TCPConfig::MAX_RETX_ATTEMPTS 时,发送 RST包

TCP连接的关闭稍微麻烦一些,需要满足以下几种情况:

  • 对于 unclean shutdown :既 TCPConnection 收到或者发出带有 RST 标志的segment,发生此情况,对于 inboundoutbound 的ByteStreams都会置于 ** error state**,可能会导致还在传输中的正常包被丢弃,无法接收到/发送完整数据
  • 对于 clean shutdown :既双方都确认发送完数据后退出,则比较麻烦,这就是著名的 Two Generals Problem , 证明了很难实现通过不可靠信道交换消息并达成共识。但TCP的设计近乎完美的实现了,就是需要满足 四个前提条件 :
  1. 输入流(inbound stream)报文必须已经结束且完成组装
  2. 输出流(outbound stream)报文必须已经结束且全部发送给对方(这里的全部包括 FIN 标志)
  3. 接收到远程端对自己输出流报文的 结束确认ACK
  4. 自己的 结束确认ACK 被对方收到

这里的 结束确认ACK 均指对双方 FIN 的ACK

对于前面三个条件的检测是比较容易的,但是对于第四个条件是困难的(TCP协议不会对ack包进行ack),所以有两种情况:

  • 主动关闭 : 在输入、输出流都结束后等待一段时间后结束连接。首先要满足前面3个前提条件,再继续等待通常是2倍MSL(Maximum Segment Lifetime),本实验中为10倍 initial retransmission timeout
  • 被动关闭 :对方先结束流,则已经满足前提条件1、2、3,即可无需等待结束连接。(自己可以确认对方满足前提1~3,则自己满足条件4,所以可以直接结束,有点绕·········)

个人理解:理论上无法完美满足前提条件4,所以先发送完报文的一方(本地端)发送了FIN,肯定能收到对方(远程端)确认的ACK,远程端已经收到本地端的FIN,且发送了过了ACK,则只需要再发送自己FIN给本地端就可以直接结束了(本地端比你先结束,你最后发FIN表明自己也结束了,发完就可以结束连接了),本地端再接收到远端这个FIN后会回复一个确认ACK过去(但远端已经结束或者因为本身不会确认这种ack包,收不到对应的ack),然后等待2MSL后结束

结合TCP连接的经典图来理解吧:
在这里插入图片描述

这就是TCP连接中的三次握手和四次挥手。还有对应此过程的FSM图:
在这里插入图片描述

图上箭头连线上的 FIN/ACK,左边只事件,右边指相应动作。注意区分实线箭头和虚线箭头。

Implementation

对于TCPConnection类需要额外添加一些数据成员和函数:

    bool connection_active = true;
    //! count how much milliseconds since the last TCPSegment was received
    size_t _time_since_last_segment_received = 0;
    //! RST状态标志,特殊处理,如果此标志为true,则发送RST包
    void _set_rst_state(const bool send_rst);
    //! 由于全双工,对于发送出的包都需要设置ackno和window size
    void _add_ackno_and_window_to_send();

然后就是在对应源文件中实现:

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

void TCPConnection::segment_received(const TCPSegment &seg) {
    //根据头文件中声明,其接收来自网络的segment
    //那么就首先要重新统计一个 _time_since_last_segment_received
    //即重新置零
    _time_since_last_segment_received = 0;
    //定义一个TCPHeader变量来序列化传入的TCPSegment参数
    const TCPHeader &header = seg.header();
    //对于是否是RST包
    //对于RST包,需要传入false参数
    if (header.rst) {
        _set_rst_state(false);
        return;
    }
    //处理其他状态的包,直接交给TCPReceiver,(鲁棒性很强)
    _receiver.segment_received(seg);
    //对于特殊ACK包,即不占序列号空间的
    //任何序列号有效的包都要ACK,keep-alive机制也需要空ACK包
    //设置一个bool标志,省的多次调用 length_in_sequence_space()函数
    bool _empty_ack_seg = seg.length_in_sequence_space() > 0;
    //对于收到的包需要查看是否设置了ACK标志
    //如果为真,则需要TCPSender处理
    if (header.ack) {
        //调用sender专门处理ack的函数
        _sender.ack_received(header.ackno, header.win);
        //由于该函数会自己fill_window(),所以不需要再发送空ack包
        //_empty_ack_seg变量需要置为false
        if (_empty_ack_seg && !_segments_out.empty())
            _empty_ack_seg = false;
    }
    //由closed到Listen状态,对于收到SYN包后处于FSM中SYN RECEIVED状态处理
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED) {
        //则建立连接
        connect();
        return;
    }
    //当接收到先FIN包,则要判断是否为被动关闭,并进入FSM的CLOSE WAIT状态
    if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED) {
        //将流结束后等待标志置为false
        _linger_after_streams_finish = false;
    }
    //当被动关闭中完成上面的动作,继续判断是否进入FSM的CLOSE状态
    if (!_linger_after_streams_finish && TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED) {
        connection_active = false;
        return;
    }
    // keep-alive机制判断
    if (_receiver.ackno().has_value() && (seg.length_in_sequence_space() == 0) &&
        seg.header().seqno == (_receiver.ackno().value() - 1)) {
        _empty_ack_seg = true;
    }
    //对于空ACK包调用send_empty_segment()
    if (_empty_ack_seg) {
        _sender.send_empty_segment();
    }
    //对于每一个待发送的包都需要添加上 ackno 和 wind_size 再发送
    _add_ackno_and_window_to_send();
}

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

size_t TCPConnection::write(const string &data) {
    size_t ret = _sender.stream_in().write(data);
    _sender.fill_window();
    _add_ackno_and_window_to_send();
    return ret;
}

//! \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) {
    _time_since_last_segment_received += ms_since_last_tick;
    //调用sender的tick()函数
    _sender.tick(ms_since_last_tick);
    //处理超过连续重传次数阈值的情况,发送RST包
    if (_sender.consecutive_retransmissions() > TCPConfig::MAX_RETX_ATTEMPTS) {
        //本地可能还有包在发送队列中,直接清除
        while (!_sender.segments_out().empty()) {
            _sender.segments_out().pop();
        }
        _set_rst_state(true);
        return;
    }
    //主动关闭检查,判断是否进入CLOSE状态
    if (_linger_after_streams_finish && TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
        TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED &&
        _time_since_last_segment_received >= (10 * _cfg.rt_timeout)) {
        connection_active = false;
        _linger_after_streams_finish = false;
    }
    //可能会有新的包发送
    _add_ackno_and_window_to_send();
}

void TCPConnection::end_input_stream() {
    _sender.stream_in().end_input();
    //结束了,可能还需要发送FIN包
    _sender.fill_window();
    _add_ackno_and_window_to_send();
}

void TCPConnection::connect() {
    //建立连接首先要发送SYN包
    _sender.fill_window();
    _add_ackno_and_window_to_send();
}

TCPConnection::~TCPConnection() {
    try {
        if (active()) {
            cerr << "Warning: Unclean shutdown of TCPConnection\n";

            // Your code here: need to send a RST segment to the peer
            _set_rst_state(true);
        }
    } catch (const exception &e) {
        std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
    }
}
void TCPConnection::_set_rst_state(const bool send_rst) {
    if (send_rst) {
        TCPSegment seg;
        seg.header().seqno = _sender.next_seqno();
        seg.header().rst = true;
        //由于是RST包,不必要添加ackno和wind_size
        _segments_out.emplace(std::move(seg));
    }
    //将输入/输出流置于error state
    _sender.stream_in().set_error();
    _receiver.stream_out().set_error();
    //将等待和连接状态全部置为false
    _linger_after_streams_finish = false;
    connection_active = false;
}
void TCPConnection::_add_ackno_and_window_to_send() {
    while (!_sender.segments_out().empty()) {
        TCPSegment seg = std::move(_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 = min(static_cast<size_t>(numeric_limits<uint16_t>::max()), _receiver.window_size());
        _segments_out.emplace(std::move(seg));
    }

然后接着在build目录下打开terminal:

make -j4
make check_lab4

这个会检测所提供的162个测试案例。结果如下:
在这里插入图片描述

然后来测试按照文档要求使用wireshark抓包检测。
注意这个测试,你需要先执行make_lab4检测开启虚拟网卡(虚拟部分代码也很有趣),然后才能在wireshark中看到虚拟网卡
首先在一个ternimal界面中的build目录下:

sudo ./apps/tcp_ipv4 -l 169.254.144.9 9090

此作为server端,然后如果成功,就能看到下图:
在这里插入图片描述

接着可以打开wireshark在图形控制界面操作,也可以直接按照文档操作。

//如果没有安装tshark,则需要先安装,可能有点慢
sudo apt install tshark

安装完成后在另外一个terminal的build目录下执行:

sudo tshark -Pw /tmp/debug.raw -i tun144

这个命令会抓取tun144虚拟网卡的数据包记录到debug.raw文件中,你可以用wireshark打开此文件后分析详细内容。
接着你在第三个ternimal中的build目录下继续执行:

sudo ./apps/tcp_ipv4 -d tun145 -a 169.254.145.9 169.254.144.9 9090

此作为client端,成功后你将看到:
在这里插入图片描述

然后你在server/client端的ternimal窗口可以互相发送字符消息,结束流操作为 ctrl + D,现在服务器端结束流,然后再到客户端结束流。就能看到如下结果:
在这里插入图片描述

最后是性能测试:
在这里插入图片描述

实验要求是都超过0.10 Gbit/s,我这算是压线吧。好垃圾的实现呀,有点丢脸,所以去找了优秀的实现方案,等做完整个实验再来优化。

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值