lab 地址 :lab2-doc
代碼實現:lab2-code
完整目錄:
0. ByteStream
1. StreamReassembler
2. TCPReceiver
3. TCPSender
4. TCPConnection
5. ARP
6. IP-Router
1. 目標
lab0 實現了一個讀寫字節流 ByteStream
,lab1 實現了可靠有序不重複的字節流組裝器 StreamReassembler
,接下來要實現更上層的封裝,TCPSender
和 TCPReceiver
,分別負責 TCPConnection
的收發包功能。
首先要實現的是 TCPReceiver
。
除了寫入傳入流之外,TCPReceiver
負責告訴 TCPSender
兩件事:
1.“第一個未組裝”字節的索引,稱爲“確認號”(ackno),這是接收方從發送方需要的第一個字節。
2.“第一個未組裝”索引和“第一個不可接受”索引之間的距離。這稱爲“窗口大小”。
ackno 和窗口大小一起描述了接收方的窗口:允許 TCPSender
發送的一系列索引。使用該窗口,接收方可以控制傳入數據的流量,使發送方限制其發送量,直到接收方準備好接收更多數據。我們有時將 ackno
稱爲窗口的“左邊緣”(TCPReceiver
感興趣的最小索引),將 ackno
+ 窗口大小稱爲“右邊緣”(剛好超出 TCPReceiver
所感興趣的最大索引)。
2. 實現
正常情況下,TCPReceiver 會收到 3 種報文:
- SYN 報文,帶着初始 ISN ,用來標記字節流的起始位置,通常 ISN 是隨機值,防止被攻擊
- FIN 報文,表明通信結束
- 普通的數據報文,只需要寫入 payload 到 ByteStream 即可。
TCP 報文頭部的seqno
標識了 payload 字節流在完整字節流中的起始位置,然而這個字段只有 32 位,也就是說最多隻能支持 4gb 的字節流,這顯然是遠遠不夠的,爲了解決上面的問題,引入了absolute sequence number
(簡稱 abs_seqno)的概念,abs_seqno
定義爲uin64_t
類型,這樣可以支持最高2^64 - 1
長度的字節流。兩者的區別如下:
stream index
其實際上就是 ByteStream
的字節流索引,只是少了 FIN 和 SYN 各自在字節流中的 1 個字節佔用,它同樣也是 uint64_t
類型。
abs_seqno
的起始位置永遠是 0,這意味着它對於 seqno
會有 isn 長度的偏移,每次寫入時都不斷對其遞增,由於其長度更長,即便 seqno
溢出了,abs_seqno
也能正常記錄正確的長度。
2.1 seqno 和 abs_seqno 轉換
由於 seqno 不是真正的 字節流 起始位置,因此接受報文時,需要對其轉換成 abs_seqno
,纔可以方便 TCPReceiver
中計算窗口大小。
轉換的接口如下:
//! Transform a 64-bit absolute sequence number (zero-indexed) into a 32-bit relative sequence number
//! \param n the absolute sequence number
//! \param isn the initial sequence number
//! \returns the relative sequence number
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn);
//! Transform a 32-bit relative sequence number into a 64-bit absolute sequence number (zero-indexed)
//! \param n The relative sequence number
//! \param isn The initial sequence number
//! \param checkpoint A recent absolute sequence number
//! \returns the absolute sequence number that wraps to `n` and is closest to `checkpoint`
//! \note Each of the two streams of the TCP connection has its own ISN. One stream
//! runs from the local TCPSender to the remote TCPReceiver and has one ISN,
//! and the other stream runs from the remote TCPSender to the local TCPReceiver and
//! has a different ISN.
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint);
abs_seqno
轉換成 seqno
比較簡單,只需要把 abs_seqno
加上 isn
初始偏移量,然後取 abs_seqno
的低 32 位值即可。
seqno
轉換成 abs_seqno 則稍微麻煩些,因爲 seqno=17 可以表示多種 abs_seqno,如 2^32 + 17, or 2^33 + 17, 2^34 + 17 等等,這裏引入一個 checkpoint 的概念,在 TCPReceiver 中 checkpoint 是當前寫入的總字節數。
假定要將 n 從 seqno
轉換成 abs_seqno
,這裏先將 chekpoint
從 abs_seqno
轉換成 seqno
,然後計算 n(seqno
版本) 和 checkpoint
(seqno
版本) 的偏移量,最後加到 checkpoint
(abs_seqno
版本)上面即可得出 n(abs_seqno
版本),參考下圖:
實現如下:
WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) {
uint64_t mask = (1ul << 32) - 1;
return WrappingInt32{static_cast<uint32_t>((n + isn.raw_value()) & mask)};
}
uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) {
int32_t offset = n - wrap(checkpoint, isn);
int64_t result = checkpoint + offset;
result = result < 0 ? result + (1ul << 32) : result;
return result;
}
2.2 接收報文
處理報文比較簡單,主要關注前面說的 SYN 和 FIN 報文即可,及時更新最新的 abs_seqno
。
void TCPReceiver::segment_received(const TCPSegment &seg) {
// 1. process syn flag
if (seg.header().syn) {
if (!_received_syn) {
_received_syn = true;
_isn = seg.header().seqno;
}
}
// 2. process fin flag
bool eof = false;
if (seg.header().fin) {
eof = true;
}
// 3. push payload
if (ackno().has_value() && !_reassembler.stream_out().input_ended()) {
// relative seqno to stream index
uint64_t stream_index = unwrap(seg.header().seqno, _isn, _checkpoint);
// stream index should in the window
uint64_t abs_ackno = unwrap(ackno().value(), _isn, _checkpoint);
if (stream_index + seg.length_in_sequence_space() <= abs_ackno || stream_index >= abs_ackno + window_size()) {
return;
}
if (!seg.header().syn) {
stream_index -= 1; // ignore syn flag;
}
_reassembler.push_substring(seg.payload().copy(), stream_index, eof);
_checkpoint = _reassembler.stream_out().bytes_written();
}
}
2.3 窗口大小和 ackno
窗口大小用於通知對端當前可以接收的字節流大小,ackno 用於通知對端當前接收的字節流進度。兩者實現都比較簡單,如下:
optional<WrappingInt32> TCPReceiver::ackno() const {
// next_write + 1 ,because syn flag will not push in stream
size_t next_write = _reassembler.stream_out().bytes_written() + 1;
next_write = _reassembler.stream_out().input_ended() ? next_write + 1 : next_write;
return !_received_syn ? optional<WrappingInt32>() : wrap(next_write, _isn);
}
size_t TCPReceiver::window_size() const {
return _reassembler.stream_out().remaining_capacity();
}