前言
- merge lab4的初始代码,出现如下变化

进入build文件夹
运行make -j4
然后依次检查lab0~lab3的结果,由于加入了新的样例,所以前面的实验可能由没通过的新的样例
DEBUG LAB0~LAB3
- LAB1样例没过,DEBUG过程见LAB1实现的第五阶段
- LAB3样例没过,DEBUG过程见LAB3实现的第三阶段
第一阶段
分析
- 新增成员
- bool _rst,如果收到RST报文段或者发送RST报文段,则_rst为true
疑问
- 终止连接需要做什么事情?
TCPConnection和TCPSender一样有一个_segments_out,我要做的事情是不是:从TCPSender中的_segments_out取出TCPSegment,添加ackno和window size字段,然后放到TCPConnection的_segments_out中,这个操作应该放在哪里?write方法吗?
- write方法
- segment_received方法,确保返回一个带着ack的段
- end_input_stream方法
ACK和window size字段是配套的吗?如果没有ack,还需要设置window size吗?
如何利用之前的接口发送一个空的RST报文段?
在哪里判断连续重传次数是否达到上限?tick()
什么时候连接会正常关闭?连接正常关闭需要做什么事情?如何进入lingering状态?
这样对que操作,不会修改_sender.segments_out的内容
TCPConnection::end_input_stream,也需要fill_window,以发送FIN
断开连接时,本地先发送FIN,对面的FIN和ACK可能会在一个报文段中传回来
什么时候会关闭入流?什么时候会关闭出流?什么时候将_linger_after_streams_finish置为false
- 关闭出流:TCPConnection::end_input_stream()
- 关闭入流:_receiver._reassembler::push_substring()
- 这个方法运行到,如果所有的字节已经组装完成(unassembled_bytes()==0&&_next_index==_eof_index)
- 那么就调用_output.end_input()
TCPConnection关闭之后,还能从里面读出数据吗?我觉得是可以的,只要数据已经被组装完成,什么时候取走都没关系
假如说先收到对方发送的FIN,但是前面还有一些数据报文段没到(乱序或丢失),然后在对面发过来的数据字节流完全组装完成之前,我先发送了FIN,那么这种情况到底算谁主动发起关闭连接?
换一种说法,我先发送FIN,还没收到这个FIN的ACK之前,对方也发了FIN过来,那么我是否需要lingering?如下图
在TCPConnection::end_input_stream()中查看_receiver.stream_out().input_ended(),如果是true,说明已经收到对面FIN,且已经被组装
如果_sender数据已经全部发完,但是还可能重传,这个时候出流算不算结束?
我们在学习四次挥手的时候,那个图是画的4个报文段,这种情况是一种比较理想的情况;实际上现实中的TCP连接在断开的时候,并不一定是这么理想的,双方可能在几乎相同的时间给对方发送FIN报文段,这种情况双方最后都会进入lingering状态。
如下图是理想状况
如下图是B将ACK和FIN合并
如下图是两者几乎同时发送FIN报文段,在这种情况下,我认为两者都会进入lingering状态
_linger_after_streams_finish**初始化为true。如果入站流在TCPConnection的出站流读到EOF之前结束(意思就是对方先发送FIN),则此变量需要设置为false
TCPConnection不一定是主动发起连接的一方,如果先收到对方的SYN报文段,我也要回复SYN+ACK报文段,所以在TCPConnection::segment_received中也需要调用_sender.fill_window()
代码
tcp_connection.hh
#ifndef SPONGE_LIBSPONGE_TCP_FACTORED_HH#define SPONGE_LIBSPONGE_TCP_FACTORED_HH#include "tcp_config.hh"#include "tcp_receiver.hh"#include "tcp_sender.hh"#include "tcp_state.hh"//! \brief A complete endpoint of a TCP connectionclass TCPConnection {private:TCPConfig _cfg;TCPReceiver _receiver{_cfg.recv_capacity};TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};//! outbound queue of segments that the TCPConnection wants sentstd::queue<TCPSegment> _segments_out{};//! Should the TCPConnection stay active (and keep ACKing)//! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,//! in case the remote TCPConnection doesn't know we've received its whole stream?bool _linger_after_streams_finish{true};bool _rst{false};size_t do_send(bool limit=false, size_t send_num=0);bool _lingering{false};size_t _linger_timer_ms{0};bool _closed{false};size_t _time_since_last_segment_received{0};public://! \name "Input" interface for the writer//!@{// 下面的4个接口是应用程序在使用这个TCPConnection,用来发送数据的接口//! \brief Initiate a connection by sending a SYN segmentvoid connect();//! \brief Write data to the outbound byte stream, and send it over TCP if possible//! \returns the number of bytes from `data` that were actually written.size_t write(const std::string &data);//! \returns the number of `bytes` that can be written right now.size_t remaining_outbound_capacity() const;//! \brief Shut down the outbound byte stream (still allows reading incoming data)void end_input_stream();//!@}//! \name "Output" interface for the reader//!@{// 这个就是TCPReceiver组装后的字节流//! \brief The inbound byte stream received from the peerByteStream &inbound_stream() { return _receiver.stream_out(); }//!@}//! \name Accessors used for testing// 下面的4个接口是用于校验测试结果的//!@{//! \brief number of bytes sent and not yet acknowledged, counting SYN/FIN each as one bytesize_t bytes_in_flight() const;//! \brief number of bytes not yet reassembledsize_t unassembled_bytes() const;//! \brief Number of milliseconds since the last segment was receivedsize_t time_since_last_segment_received() const;//!< \brief summarize the state of the sender, receiver, and the connectionTCPState state() const { return {_sender, _receiver, active(), _linger_after_streams_finish}; };//!@}//! \name Methods for the owner or operating system to call//!@{// 下面的5个接口是操作系统调用的// 网络层收到packet,转化成TCPSegment,调用这个函数提交给传输层//! Called when a new segment has been received from the networkvoid segment_received(const TCPSegment &seg);//! Called periodically when time elapses// 时间的流逝void tick(const size_t ms_since_last_tick);//! \brief TCPSegments that the TCPConnection has enqueued for transmission.//! \note The owner or operating system will dequeue these and//! put each one into the payload of a lower-layer datagram (usually Internet datagrams (IP),//! but could also be user datagrams (UDP) or any other kind).// 操作系统会从这个队列中拿出数据,放到网络层并发送到网络上std::queue<TCPSegment> &segments_out() { return _segments_out; }//! \brief Is the connection still alive in any way?//! \returns `true` if either stream is still running or if the TCPConnection is lingering//! after both streams have finished (e.g. to ACK retransmissions from the peer)// 连接是否activebool active() const;//!@}//! Construct a new connection from a configurationexplicit TCPConnection(const TCPConfig &cfg) : _cfg{cfg} {}//! \name construction and destruction//! moving is allowed; copying is disallowed; default construction not possible// 下面是构造函数和析构函数//!@{~TCPConnection(); //!< destructor sends a RST if the connection is still openTCPConnection() = delete;TCPConnection(TCPConnection &&other) = default;TCPConnection &operator=(TCPConnection &&other) = default;TCPConnection(const TCPConnection &other) = delete;TCPConnection &operator=(const TCPConnection &other) = delete;//!@}};#endif // SPONGE_LIBSPONGE_TCP_FACTORED_HH
tcp_connection.cc
#include "tcp_connection.hh"#include <iostream>// Dummy implementation of a TCP connection// For Lab 4, please replace with a real implementation that passes the// automated checks run by `make check`.template <typename... Targs>void DUMMY_CODE(Targs &&... /* unused */) {}using namespace std;//! \returns the number of `bytes` that can be written right now.// 返回outgoing bytestream的剩余容量// _sender.stream_in()size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }//! \brief number of bytes sent and not yet acknowledged, counting SYN/FIN each as one byte// 返回已经发送但未确认的字节数// _sender.bytes_in_flight()size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }//! \brief number of bytes not yet reassembled// 返回已经接收但未组装的字节数// _receiver.unassembled_bytes()size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }//! \brief Number of milliseconds since the last segment was received// 返回自接收到上个报文段经过的毫秒数size_t TCPConnection::time_since_last_segment_received() const { return _time_since_last_segment_received; }//! Called when a new segment has been received from the network// 当网络层收到packet后,会转化为TCPSegment,调用这个函数/*如果设置了RST(重置)标志,将入站和出站的流都设置为错误状态并永久终止连接。否则……将报文段提供给TCPReceiver,以便它可以检查其关注的字段:seqno、SYN、payload和FIN如果ACK标志已设置,告诉TCPSender它关心的字段:ackno和接收窗口大小。如果传入的段占用任何序列号,则TCPConnection确保至少发送一个段作为答复,以反映ackno和窗口大小的更新。*/void TCPConnection::segment_received(const TCPSegment &seg) {_time_since_last_segment_received=0;if (_closed||_rst) {return;}if (seg.header().rst) { // 如果设置了RST标志_sender.stream_in().set_error();_receiver.stream_out().set_error();_rst=true;} else {// 交给receiver处理_receiver.segment_received(seg);if (seg.header().ack) { // 如果设置了ACK标志// 告诉TCPSender它关心的字段:ackno和接收窗口大小_sender.ack_received(seg.header().ackno, seg.header().win);}// 如果收到了SYN报文,那么这里的调用可以回复SYN报文if (_receiver.ackno().has_value()&&!_receiver.stream_out().input_ended()) {_sender.fill_window();}if (seg.length_in_sequence_space()) { // 如果传入的段占用任何序列号// std::cout<<"****** TCPConnection::segment_received with seqno space occupied>0, at least one ack should be send"<<std::endl;// 则TCPConnection确保至少发送一个段作为答复,以反映ackno和窗口大小的更新if (!do_send()) { // 如果没有发送任何段,则发送一个空的ACK报文段// std::cout<<"****** TCPConnection::segment_received generate a empty ACK segment"<<std::endl;_sender.send_empty_segment();do_send();}}/* 如果当前收到的报文段带FIN标志,那么_receiver.segment_received(seg)处理完后,_receiver.stream_out.input_ended()可能已经是true,表示入流已经结束,上面的代码确保发送一个ACK,这个ACK有可能就是对这个FIN的ACK,此时需要检查出流是否也已经结束*//* 如果当前收到的报文段带ack,那么ACK有可能是对我之前发送的FIN的ACK,这里也需要检查出流和入流是否已经全部结束*/// std::cout<<"****** instream ended: "<<_receiver.stream_out().input_ended()// <<", outstream ended:"<<(_sender.stream_in().eof() && _sender.next_seqno_absolute()==_sender.stream_in().bytes_written()+2 && _sender.bytes_in_flight()>0)<<std::endl;// _stream.eof()&&_next_seqno==_stream.bytes_written()+2&&bytes_in_flight()==0if (_receiver.stream_out().input_ended()&&!_sender.stream_in().eof()) {_linger_after_streams_finish=false;}if (_receiver.stream_out().input_ended()&&(_sender.stream_in().eof() && _sender.next_seqno_absolute()==_sender.stream_in().bytes_written()+2 &&_sender.bytes_in_flight()==0)) {if (_linger_after_streams_finish) {if (!_lingering) {// std::cout<<"****** TCPConnection into lingering"<<std::endl;_lingering=true;}} else {// std::cout<<"****** TCPConnection closed"<<std::endl;_closed=true;}}}}//! \brief Is the connection still alive in any way?//! \returns `true` if either stream is still running or if the TCPConnection is lingering//! after both streams have finished (e.g. to ACK retransmissions from the peer)bool TCPConnection::active() const {return !_rst&&!_closed;// if (_rst||_closed) {// return false;// }// // _sender.stream_in(), _receiver.stream_out()// // 如何判断正在lingering?// // if (_sender.stream_in().eof()&&_receiver.stream_out().eof()&&!_linger_after_streams_finish) {// if (_sender.stream_in().eof()&&_receiver.stream_out().input_ended()&&!_linger_after_streams_finish) {// return false;// }// return true;}//! \brief Write data to the outbound byte stream, and send it over TCP if possible//! \returns the number of bytes from `data` that were actually written.size_t TCPConnection::write(const string &data) {size_t sz = _sender.stream_in().write(data);_sender.fill_window();do_send();return sz;}// 从TCPSender的_segments_out中取出TCPSegment,添加ack和window size字段,放入TCPConnection的_segments_out中size_t TCPConnection::do_send(bool limit, size_t send_num) {size_t cnt=0;// auto que = _sender.segments_out(); // 这么写是复制了一个队列出来,不会更改原队列while (_sender.segments_out().size()&&!(limit&&cnt<send_num)) {cnt++;TCPSegment seg = _sender.segments_out().front();_sender.segments_out().pop();std::optional<WrappingInt32> ackno=_receiver.ackno();if (ackno) {seg.header().ack=true;seg.header().ackno=ackno.value();size_t window_size=_receiver.window_size();// 需要转化为16位uint16_t uint16_window_size=0xFFFF&window_size;seg.header().win=uint16_window_size;}if (_rst) {seg.header().rst=true;}// (A=1,R=0,S=0,F=0,ackno=1,payload_size=0,)// std::cout<<"****** TCPConnection do send a segment(A="<<seg.header().ack// <<",R="<<seg.header().rst<<",S="<<seg.header().syn// <<",F="<<seg.header().fin<<",payload_size="<<seg.payload().size()<<")"<<std::endl;_segments_out.push(seg);}// std::cout<<"****** TCPConnection do send "<<cnt<<" segments and "<<_sender.segments_out().size()<<" left in _send._segments_out"<<std::endl;return cnt;}//! \param[in] ms_since_last_tick number of milliseconds since the last call to this methodvoid TCPConnection::tick(const size_t ms_since_last_tick) {_time_since_last_segment_received+=ms_since_last_tick;if (_rst||_closed) {return;}if (_lingering) {_linger_timer_ms+=ms_since_last_tick;// std::cout<<"****** Lingering, _linger_timer_ms: "<<_linger_timer_ms<<", up to: "<<10*_cfg.rt_timeout;// std::cout<<", _closed: "<<_closed<<", linger_after_streams_finish: "<<_linger_after_streams_finish;// std::cout<<", active():"<<active()<<std::endl;if (_linger_timer_ms>=10*_cfg.rt_timeout) {_lingering=false;_closed=true;// std::cout<<"****** TCPConnection lingering ended, now fully closed"<<std::endl;}return; // 我认为如果在lingering,说明_sender已经没有东西要发送了,也没有需要重传的东西}// 如果在lingering,这里后面的代码还需要执行吗?_sender.tick(ms_since_last_tick);if (_sender.consecutive_retransmissions()>TCPConfig::MAX_RETX_ATTEMPTS) { // 这段代码应该在do_send之前_sender.stream_in().set_error();_receiver.stream_out().set_error();_rst=true;}do_send(); // 需要发送重传的报文段}//! \brief Shut down the outbound byte stream (still allows reading incoming data)void TCPConnection::end_input_stream() {// std::cout<<"****** Call TCPConnection::end_intput_stream()"<<std::endl;// if (_receiver.stream_out().input_ended()) { // 这个判断条件对应了_recevier的FIN_RECEIVED状态// std::cout<<"****** in stream has ened, _linger_after_streams_finish set false"<<std::endl;// _linger_after_streams_finish=false;// }_sender.stream_in().end_input();// 如果对方的接收窗口大小不够,这里调用fill_window还不一定能发送FIN_sender.fill_window(); // fixeddo_send();}//! \brief Initiate a connection by sending a SYN segment// 初始化连接,发送一个SYN报文段// 需要保证这个函数只能被成功调用一次吧?void TCPConnection::connect() {// std::cout<<"****** TCPConnection::connect called";// 主动发起连接,发送SYN报文_sender.fill_window();// std::cout<<", _sender.segments_out().size()"<<_sender.segments_out().size();// if (do_send()==1) {// std::cout<<"****** send syn when connect"<<std::endl;// }do_send();// std::cout<<", TCPConnection._segments_out.size()"<<_segments_out.size()<<std::endl;}//!< destructor sends a RST if the connection is still openTCPConnection::~TCPConnection() {try {if (active()) {cerr << "Warning: Unclean shutdown of TCPConnection\n";_rst=true;_sender.stream_in().set_error();_receiver.stream_out().set_error();if (!do_send(true, 1)) {_sender.send_empty_segment();do_send(true, 1);}// Your code here: need to send a RST segment to the peer}} catch (const exception &e) {std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;}}
测试结果
每次测试都会有若干个测试样例没过,而且最骚的是每次错的样例编号还不一样


DEBUG
- tcp_receiver和tcp_sender的文档都说了,在这两个实验中,不需要set_error
- 调试半天不出不知道问题在哪里,躺在床上心如死灰,半睡半醒中忽然想起文档中的这句话,然后起来把tcp_receiver中的这行代码注释掉,就100%样例都过了

最终代码
说明
- 和第一阶段相比,只是去掉了tcp_receiver.cc中的
stream_out().set_error()这行代码,所以tcp_connection.cc和tcp_connecion.hh的逻辑都没有任何变化 - 另外把调试输出都去掉了
测试结果
运行了很多次,偶尔会出现1个样例的timeout,这个先不管了,就当过了
LAB4剩余部分
命令行测试
流程见LAB4翻译的测试一节,测试通过,指定-w 1也没有问题
性能测试
改写webget

总结
- 虽然TCP连接会有各种状态的演化,但是在代码中,并没有显式定义这些状态
- 例如,如果照本宣科的按照4次挥手的状态变化来写代码,很可能无法涵盖一些特殊情况,如下图,双方都会进入lingering状态

- 全程可以说没有参考别人的代码,自己在一周的时间内通过了LAB0~LAB4的所有样例
- 通过这些测试样例并不是终点,后面复习TCP知识点的时候还可以结合这里的代码来强化记忆
- 后面的实验有时间再做
