lab_2_3_144
lab2主要是32位相对序列号WrappingInt32和64位绝对序列号的转换
q:序列号的作用:
a:序列号的主要目的是确保数据的正确传输和顺序,以及在接收方进行数据的重传请求时能够准确地指出需要重传的数据部分
1、唯一性:在一个TCP连接中,每个字节都会被赋予一个唯一的序列号。序列号通常是从一个随机值开始,这个随机值称为初始序列号(ISN),用于防止网络攻击
2、数据段的标识:每个TCP数据段都会携带序列号,它指明了该数据段中的**第一个字节**在整个数据流中的位置
3、接收方在收到数据后会发送确认应答(ACK),其中包含期望收到的下一个数据段的序列号。这被称为累积确认,意味着这个序列号之前的所有数据都已经正确接收
4、重传机制:如果发送方没有在预期时间内收到确认应答,它会认为数据段可能已经丢失或出错,并会根据序列号重传丢失的数据段
5、顺序控制:序列号保证了数据的顺序。如果接收方收到的数据段不是按照顺序到达的,它会根据序列号重新排序,确保应用层接收到有序的数据流
6、流量控制:序列号也用于流量控制。TCP使用窗口大小来控制发送方的发送速率,而窗口大小的调整依赖于序列号
随着通信的进行,序列号会逐渐增加,直到达到最大值,然后循环回到0。由于序列号的循环性质,现代操作系统会使用足够大的序列号空间来减少序列号重复的可能性
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
return WrappingInt32{isn + static_cast<uint32_t>(n)};
}
/*
n 被转换为 uint32_t,相当于对 n 取模 2^32,保留 32 位低位部分
isn + static_cast<uint32_t>(n) 的结果是一个 WrappingInt32 类型的相对序列号
*/
isn就是初始序列号,n是64位绝对序列号
发送过程:
1.内部64位序列号:TCP协议的实现内部使用64位绝对序列号来跟踪数据流。这个64位序列号能够表示非常大的数值,从而减少了在高速网络环境中序列号回绕的可能性
2.转换为32位序列号:当TCP准备发送一个数据段时,它会将内部的64位绝对序列号转换为32位相对序列号
3.发送数据包:转换后的32位序列号被放置在TCP数据包的序列号字段中,然后数据包被发送到网络
接收过程:
1.接收32位序列号:接收方的TCP协议栈从接收到的数据包中提取32位序列号
2.恢复为64位序列号:接收方使用这个32位序列号,结合连接的初始序列号(ISN)和其他状态信息,将其恢复为64位绝对序列号。这样,接收方就能够准确地知道这个数据段在整体数据流中的位置
3.处理数据:使用恢复的64位序列号,接收方的TCP协议栈可以正确地处理数据,例如确认接收到的数据、请求重传丢失的数据段或者将数据按顺序传递给上层应用
处理32位:
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
uint32_t interval = n - wrap(checkpoint, isn);
uint64_t result = checkpoint + interval;
// 如果 interval 超过 2^31,则减去 2^32 以选择最接近 checkpoint 的绝对序列号
if (interval > (1ul << 31)) {
result -= (1ul << 32);
}
return result;
}
eg:
n = WrappingInt32(100):相对序列号是 100
isn = WrappingInt32(50):初始序列号是 50
checkpoint = 4294967400:最近的 64 位绝对序列号,接近 2^32 边界,即 4294967296
先将checkpoint通过wrap转换为32位;为104
将n - 104 = -4; uint32位中-4为2^32 - 4
1.-4 在二进制中的表示。在32位系统中,-4 的二进制表示是使用补码形式。4 的二进制是 0000 0000 0000 0000 0000 0000 0000 0100,取反得到 1111 1111 1111 1111 1111 1111 1111 1011,然后加1得到 -4 的补码表示:1111 1111 1111 1111 1111 1111 1111 1100
2.当这个补码表示的值被转换为一个 uint32_t 类型时,它的二进制位保持不变
3.-4 转换为 uint32_t 后的二进制表示是 1111 1111 1111 1111 1111 1111 1111 1100,这个值等于 4294967292(即 2^32 - 4)
result = checkpoint + interval = 4294967400 + 4294967292 = 8589934692
result -= (1ul << 32)
TCPReceiver::segment_received
、TCPReceiver::ackno()
和 TCPReceiver::window_size()
函数的完整实现
void TCPReceiver::segment_received(const TCPSegment &seg) {
TCPHeader head = seg.header();
if(!_is_syn && !head.syn) return ;
if(head.syn){
_is_syn = true;
_isn = head.seqno;
}
string payload = seg.payload().copy();
WrappingInt32 seqno = head.syn ? head.seqno + 1 : head.seqno; //第一个有效字符的序列号
uint64_t checkpoint = stream_out().bytes_written();
//uwarp出来是 absolute seqno 需要 -1 才能转化成 stream index
uint64_t index = unwrap(seqno,_isn,checkpoint) - 1;
_reassembler.push_substring(payload, index, _is_syn && head.fin);
}
TCPHeader结构体:
struct TCPHeader {
static constexpr size_t LENGTH = 20; //!< [TCP](\ref rfc::rfc793) header length, not including options
//! \struct TCPHeader
//! ~~~{.txt}
//! 0 1 2 3
//! 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Source Port | Destination Port |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Sequence Number |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Acknowledgment Number |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Data | |U|A|P|R|S|F| |
//! | Offset| Reserved |R|C|S|S|Y|I| Window |
//! | | |G|K|H|T|N|N| |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Checksum | Urgent Pointer |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | Options | Padding |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! | data |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! ~~~
//! \name TCP Header fields
//!@{
uint16_t sport = 0; //!< source port
uint16_t dport = 0; //!< destination port
WrappingInt32 seqno{
0}; //!< sequence number
WrappingInt32 ackno{
0}; //!< ack number
uint8_t doff = LENGTH / 4; //!< data offset
bool urg = false; //!< urgent flagc
bool ack = false; //!< ack flag
bool psh = false; //!< push flag
bool rst = false; //!< rst flag
bool syn = false; //!< syn flag
bool fin = false; //!< fin flag
uint16_t win = 0; //!< window size
uint16_t cksum = 0; //!< checksum
uint16_t uptr = 0; //!< urgent pointer
acko返回第一个未确认接收的字节序列号
optional<WrappingInt32> TCPReceiver::ackno() const {
if(!_is_syn) return {};
else{
size_t ack = stream_out().bytes_written() + 1;
if(stream_out().input_ended()) return wrap(ack + 1,_isn);
else return wrap(ack,_isn);
}
}
size_t TCPReceiver::window_size() const {
return stream_out().remaining_capacity();
}
TCP发送端
1、跟踪接收端的窗口大小,并根据接收端的窗口尺寸调整发送速率(流量控制)
2、在必要的时候填充窗口,通过从ByteStream读取并创建新的TCP段(包括SYN和FIN标志),最后发送它们。发送方应该一直发送段,直到窗口已满或字节流为空
3、跟踪已发送但尚未被接收的报文段,并在合适的时机重传(超时重传)
1、fill_window()
将输入的 ByteStream 尽可能填充到发送窗口中;接收端会给Sender一个windowsize(采用滑动窗口进行流量控制),根据该windowsize判断如果窗口没填满并且发送端有数据需要发送,就可以使用fill_window()发送segment
void TCPSender::fill_window() {
size_t remaining_winsize = (_window_size != 0 ? _window_size : 1);
size_t out_size = bytes_in_flight();
if (remaining_winsize <= out_size) // 若无窗口空间则直接返回
return;
remaining_winsize -= out_size;
while (true) {
size_t seg_size = remaining_winsize;
if (seg_size == 0)
break;
TCPSegment seg;
TCPHeader &header = seg.header();
// 设置 SYN 标志
if (!_syn_sent) {
if (seg_size < 1) // 确保窗口有空间容纳 SYN
break;
seg_size -= 1;
header.syn = true;
_syn_sent = true;
}
// 设置段的序列号
header.seqno = wrap(_next_seqno, _isn);
// 从流中读取数据,避免超过段负载上限
string seg_data = _stream.read(min(seg_size, TCPConfig::MAX_PAYLOAD_SIZE));
seg_size -= seg_data.size();
seg.payload() = Buffer(move(seg_data));
// 设置 FIN 标志
if (!_fin_sent && _stream.eof() && seg_size > 0) {
seg_size -= 1;
header.fin = true;
_fin_sent = true;
}
// 检查段是否为空
seg_size = seg.length_in_sequence_space();
if (seg_size == 0)
break;
// 推入待发送队列和重传缓冲区
_segments_out.emplace(seg);
_retrans_buf.emplace_back(seg);
_next_seqno += seg_size;
remaining_winsize -= seg_size;
// 若定时器未激活,则启动重传定时器
if (!_timer.active())
_timer.start(_retrans_timeout);
}
}
ack_received从接收者发送来的确认报文中,获取确认序列号和接收窗口大小便于 Sender 删除驻留在队列中已经发送但尚未被确认的报文,并调整发送窗口的大小
void TCPSender::ack_received(const WrappingInt32 ackno, const uint16_t window_size) {
// 更新接收方提供的窗口大小
_window_size = window_size;
// 将接收到的相对序列号(32位)转换为绝对序列号(64位)
uint64_t ack_seqno = unwrap(ackno, _isn, _next_seqno);
// 如果接收到的 ACK 序列号大于当前的下一待发送序列号,表示 ACK 无效,直接返回
if (ack_seqno > _next_seqno) {
return;
}
// 遍历重传缓冲区的段,清除被确认的段
for (auto it = _retrans_buf.begin(); it != _retrans_buf.end(); ) {
// 计算缓冲区中段的绝对序列号,加上段的长度,检查是否完全被确认
if (unwrap((*it).header().seqno, _isn, _next_seqno) + (*it).length_in_sequence_space() <= ack_seqno) {
// 如果被完全确认,则从缓冲区中移除该段
it = _retrans_buf.erase(it);
// 重置重传超时时间为初始超时时间
_retrans_timeout = _initial_retransmission_timeout;
// 重新启动定时器以检测新的未确认段
_timer.start(_retrans_timeout);
// 重置连续重传计数
_consec_retrans_count = 0;
} else {
// 一旦遇到一个未被完全确认的段,跳出循环
break;
}
}
// 如果所有待确认的段都已被确认,停止计时器
if (_retrans_buf.empty()) {
_timer.reset();
}
// 尝试填充发送窗口,发送更多的数据段
fill_window();
}
tick() 接口来检查已发送但尚未被接收的报文是否发生超时,若超时将执行重传
void TCPSender::tick(const size_t ms_since_last_tick) {
// 检查计时器是否处于激活状态,只有计时器激活时才进行时间更新
if (_timer.active()) {
// 更新计时器,将时间推进 `ms_since_last_tick` 毫秒
_timer.update(ms_since_last_tick);
}
// 检查计时器是否已超时
if (_timer.expired()) {
// 如果超时,重传 `_retrans_buf` 中的第一个未确认的段
_segments_out.emplace(_retrans_buf.front());
// 如果窗口大小大于 0,表示不是处于初始连接 SYN 未确认状态,可以进行指数回退
if (_window_size > 0) {
// 增加连续重传计数 `_consec_retrans_count`
_consec_retrans_count++;
// 进行超时时间的指数回退,将重传超时时间 `_retrans_timeout` 翻倍
_retrans_timeout *= 2;
}
// 重新启动计时器,使用更新后的重传超时时间
_timer.start(_retrans_timeout);
}
}