CS144-Lab2-TCPReceiver

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 ,接下來要實現更上層的封裝,TCPSenderTCPReceiver,分別負責 TCPConnection 的收發包功能。
首先要實現的是 TCPReceiver

image

除了寫入傳入流之外,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 長度的字節流。兩者的區別如下:

image

stream index 其實際上就是 ByteStream 的字節流索引,只是少了 FIN 和 SYN 各自在字節流中的 1 個字節佔用,它同樣也是 uint64_t 類型。
abs_seqno 的起始位置永遠是 0,這意味着它對於 seqno 會有 isn 長度的偏移,每次寫入時都不斷對其遞增,由於其長度更長,即便 seqno 溢出了,abs_seqno 也能正常記錄正確的長度。

image

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,這裏先將 chekpointabs_seqno 轉換成 seqno,然後計算 n(seqno 版本) 和 checkpoint(seqno 版本) 的偏移量,最後加到 checkpointabs_seqno 版本)上面即可得出 n(abs_seqno 版本),參考下圖:

image

實現如下:

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

3. 測試

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章