目录
该试验就是在之前实验的基础上实现TCPSender类。
实现原则
刚开始拿到这个类时,看着实验文档那么多的规定和限制,我也有点懵,但慢慢的还是理出来一些头绪。
- 该类负责TCP字节发送的大部分事务,包括超时重传,ack报文确认,SYN和FIN字段的设立,这些仅靠它自己能搞定的他都应该自己负责。
- 但该类并不负责TCP发送的全部事务。有些事务仅靠TCPSender解决不了,比如ack字段的设立,这需要TCPReceive的协调。这里可以发现有函数ack_received(),专门传递过来ackno和window_size,这肯定是从己方的TCPReceiver传递过来的,那么这个为什么要专门写个函数传递过来参数呢?我的思考是这部分确实应该是两者协调才能处理的,确实应该在TCPConnection中,但由于其处理逻辑比较复杂,而且需要TCPSender内部信息,不如直接弄成函数方便TCPConnection调用即可。再比如这里ack字段的设立为什么不需要弄成函数让TCPSender处理呢?我想是处理逻辑比较简单而且也不设计TCPSender内部信息。
- 怎样才算是真正的发送数据??文档里有答案,TCPSender类中有输出队列_segments_out,将要发送的TCPSegment放入队列即可。之后TCPConnection会从队列中取出并向真实网卡传递数据发送到真实网络中。
- 超时重传具体怎么实现??首先我们得知需要拿一个数据结构存储已经发送但尚未确认的TCPSegment,方便超时后我们重新传送。之后比较关键的一个点是定时器仅针对未确认队列的队头TCP段。这里没有复杂的选择重传,也不选择笨拙的回退N步,策略仅仅是未确认队列队头TCP段超时重传,根据接收方的ack号滑动发送窗口。随后搞清楚该试验具体告诉我们怎么实现超时重传,他给了一个tick函数,该函数代表的意思是TCPSender的所有者即TCPConnection会通过tick函数显性告知TCPSender自上次调用tick函数过去了多少时间,TCPSender自己获取时间信息的方式,只能通过TCPConnnection告知。那么我们需要一系列变量去实现这个计时器,首先需要_time_elapsed记录未确认队列队头已经经历多少时间,_rto记录目前的超时传输超时时间,_timer_running布尔变量记录计时器是否打开,以及TCPConnection需要的信息_retransmission_count记录连续重传次数。之后处理就比较简单了,累计计时器时间,超时时根据文档要求重新设置一些列变量状态,并且重新发送未确认队列队头TCP段即可。
- 这里采用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的核心函数,作用就是尽可能的填充接收方的窗口。
- 首先获取窗口大小,注意这里如果窗口大小为0,我们也要改为1,以便发送探测报文。(如果发送方听从接收方的安排将窗口大小改为0,则永远也无法发送数据,即使接收方窗口大小缓解了,发送方也不知道该消息。)
- 这里判断的条件是是未确认队列的字节数<窗口大小。未确认字节数就是我们已经发送的字节数,我们要做的就是未确认字节数等于窗口大小。
- 创建TCP段之后,构建内容,按照步骤来,先尝试写入SYN字段,后写入数据部分,最后写入FIN字段。这里面细节挺多的,注意SYN字段和FIN字段都占用一个字节且没必要在数据部分时就给FIN留位置,数据部分尽可能的塞,最后FIN能放就放不能放下个TCP段放。这里注意FIN字段可能被多次发送,当窗口大小很大时,该循环会多次写入FIN字段并发送空数据的TCP段,直至把窗口大小撑满,所以这里用_fin_sent标记是否发送过,只发送一次fin字节。syn字节没有这个问题,因为syn字节判断条件是_next_seqno == 0,这个值在第一次syn之后就会增加,所以syn只会发送一次。
- 之后查看构建的tcp段是否为空,为空说明数据部分没有可发送的数据流,直接退出循环。
- 否则,发送该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;
}
最后实验成功截图,完结撒花!!!

481

被折叠的 条评论
为什么被折叠?



