前言
經(jīng)過(guò)前面幾個(gè)實(shí)驗(yàn)的鋪墊,終于到了將他們組合起來(lái)的時(shí)候了。Lab4 將實(shí)現(xiàn) TCP Connection 功能,內(nèi)部含有 TCPReceiver
和 TCPSender
,可以與 TCP 連接的另一個(gè)端點(diǎn)進(jìn)行數(shù)據(jù)交換。
實(shí)驗(yàn)要求
簡(jiǎn)單來(lái)說(shuō),這次實(shí)驗(yàn)就是要在 TCPConnection
類(lèi)中實(shí)現(xiàn)下圖所示的有限狀態(tài)機(jī):
這些狀態(tài)對(duì)應(yīng) TCPState
的內(nèi)部枚舉類(lèi) State
:
//! \brief Official state names from the [TCP](\ref rfc::rfc793) specification
enum class State {
LISTEN = 0, //!< Listening for a peer to connect
SYN_RCVD, //!< Got the peer's SYN
SYN_SENT, //!< Sent a SYN to initiate a connection
ESTABLISHED, //!< Three-way handshake complete
CLOSE_WAIT, //!< Remote side has sent a FIN, connection is half-open
LAST_ACK, //!< Local side sent a FIN from CLOSE_WAIT, waiting for ACK
FIN_WAIT_1, //!< Sent a FIN to the remote side, not yet ACK'd
FIN_WAIT_2, //!< Received an ACK for previously-sent FIN
CLOSING, //!< Received a FIN just after we sent one
TIME_WAIT, //!< Both sides have sent FIN and ACK'd, waiting for 2 MSL
CLOSED, //!< A connection that has terminated normally
RESET, //!< A connection that terminated abnormally
};
除了三次握手和四次揮手外,我們還得處理報(bào)文段首部 RST
標(biāo)志被置位的情況,這時(shí)候應(yīng)該將斷開(kāi)連接,并將內(nèi)部的輸入流和輸入流標(biāo)記為 error
,此時(shí)的 TCPState
應(yīng)該是 RESET
。
代碼實(shí)現(xiàn)
先在類(lèi)聲明里面加上一些成員:
class 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 sent
std::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 _is_active{true};
size_t _last_segment_time{0};
/**
* @brief 發(fā)送報(bào)文段
* @param fill_window 是否填滿(mǎn)發(fā)送窗口
*/
void send_segments(bool fill_window = false);
// 發(fā)送 RST 報(bào)文段
void send_rst_segment();
// 中止連接
void abort();
public:
// 省略其余成員
}
接著實(shí)現(xiàn)幾個(gè)最簡(jiǎn)單的成員函數(shù):
size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }
size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }
size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }
size_t TCPConnection::time_since_last_segment_received() const { return _last_segment_time; }
bool TCPConnection::active() const { return _is_active; }
主動(dòng)連接
客戶(hù)端可以調(diào)用 TCPConnection::connect
函數(shù)發(fā)送 SYN
報(bào)文段請(qǐng)求與服務(wù)端建立連接,由于 Lab3 中實(shí)現(xiàn)的 TCPSender::fill_window()
函數(shù)會(huì)根據(jù)發(fā)送方的狀態(tài)選擇要發(fā)送的報(bào)文段類(lèi)型,在還沒(méi)建立連接的情況下,這里直接調(diào)用 fill_window()
就會(huì)將一個(gè) SYN
報(bào)文段放在隊(duì)列中,我們只需將其取出放到 TCPConnection
的 _segments_out
隊(duì)列中即可:
void TCPConnection::connect() {
// 發(fā)送 SYN
send_segments(true);
}
void TCPConnection::send_segments(bool fill_window) {
if (fill_window)
_sender.fill_window();
auto &segments = _sender.segments_out();
while (!segments.empty()) {
auto seg = segments.front();
// 設(shè)置 ACK、確認(rèn)應(yīng)答號(hào)和接收窗口大小
if (_receiver.ackno()) {
seg.header().ackno = _receiver.ackno().value();
seg.header().win = _receiver.window_size();
seg.header().ack = true;
}
_segments_out.push(seg);
segments.pop();
}
}
主動(dòng)關(guān)閉
當(dāng)上層程序沒(méi)有更多數(shù)據(jù)需要發(fā)送時(shí),將會(huì)調(diào)用 TCPConnection::end_input_stream()
結(jié)束輸入,這時(shí)候需要發(fā)送 FIN
報(bào)文段給服務(wù)端,告訴他自己沒(méi)有更多數(shù)據(jù)要發(fā)送了,但是可以繼續(xù)接收服務(wù)端發(fā)來(lái)的數(shù)據(jù)??蛻?hù)端的狀態(tài)由 ESTABLISHED
轉(zhuǎn)移到 FIN_WAIT_1
,服務(wù)端收到 FIN
之后變成 CLOSE_WAIT
狀態(tài),并回復(fù) ACK
給客戶(hù)端,客戶(hù)端收到之后接著轉(zhuǎn)移到 FIN_WAIT_2
狀態(tài)。
如果服務(wù)端數(shù)據(jù)傳輸完成了,會(huì)發(fā)送 FIN
報(bào)文段給客戶(hù)端,轉(zhuǎn)移到 LAST_ACK
狀態(tài),此時(shí)客戶(hù)端會(huì)回復(fù)最后一個(gè) ACK
給服務(wù)端并進(jìn)入 TIME_WAIT
超時(shí)等待狀態(tài),如果這個(gè)等待時(shí)間內(nèi)沒(méi)有收到服務(wù)端重傳的 FIN
,就說(shuō)明 ACK
順利到達(dá)了服務(wù)端且服務(wù)端已經(jīng)變成 CLOSED
狀態(tài)了,此時(shí)客戶(hù)端也能斷開(kāi)連接變成 CLOSED
了。
void TCPConnection::end_input_stream() {
// 發(fā)送 FIN
_sender.stream_in().end_input();
send_segments(true);
}
在上述情景中,客戶(hù)端是主動(dòng)關(guān)閉(Active Close)的一方,服務(wù)端是被動(dòng)關(guān)閉(Passive Close)的一方。
主動(dòng)重置連接
有兩種情況會(huì)導(dǎo)致發(fā)送 RST
報(bào)文段來(lái)主動(dòng)重置連接:
- 當(dāng)
TCPSender
超時(shí)重傳的次數(shù)過(guò)多時(shí),表明通信鏈路存在故障; -
TCPConnect
對(duì)象被釋放但是 TCP 仍然處于連接狀態(tài)的時(shí)候;
和 Lab3 中類(lèi)似,TCPConnection
通過(guò)外部定期調(diào)用 tick()
函數(shù)來(lái)得知過(guò)了多長(zhǎng)時(shí)間,在 tick()
函數(shù)里還得處理超時(shí)等待的情況:
//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {
_sender.tick(ms_since_last_tick);
// 重傳次數(shù)太多時(shí)需要斷開(kāi)連接
if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
return send_rst_segment();
}
// 重傳數(shù)據(jù)包
send_segments();
_last_segment_time += ms_since_last_tick;
// TIME_WAIT 超時(shí)等待狀態(tài)轉(zhuǎn)移到 CLOSED 狀態(tài)
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED &&
_last_segment_time >= 10 * _cfg.rt_timeout) {
_linger_after_streams_finish = false;
_is_active = false;
}
}
TCPConnection::~TCPConnection() {
try {
if (active()) {
cerr << "Warning: Unclean shutdown of TCPConnection\n";
// Your code here: need to send a RST segment to the peer
send_rst_segment();
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}
void TCPConnection::send_rst_segment() {
abort();
TCPSegment seg;
seg.header().rst = true;
_segments_out.push(seg);
}
void TCPConnection::abort() {
_is_active = false;
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
}
接收?qǐng)?bào)文段
外部通過(guò) TCPConnection::segment_received()
將接收到的報(bào)文段傳給它,在這個(gè)函數(shù)內(nèi)部,需要將確認(rèn)應(yīng)答號(hào)和接收窗口大小告訴 TCPSender
,好讓他接著填滿(mǎn)發(fā)送窗口。接著還需要把報(bào)文段傳給 TCPReceiver
來(lái)重組數(shù)據(jù),并更新確認(rèn)應(yīng)答號(hào)和自己的接收窗口大小。然后 TCPSender
需要根據(jù)收到的包類(lèi)型進(jìn)行狀態(tài)轉(zhuǎn)移,并決定發(fā)送含有有效數(shù)據(jù)的報(bào)文段還是空 ACK
給對(duì)方。
為什么即使沒(méi)有新的數(shù)據(jù)要發(fā)送也要回復(fù)一個(gè)空 ACK
呢?因?yàn)槿绻贿@么做,對(duì)方會(huì)以為剛剛發(fā)的包丟掉了而一直重傳。
void TCPConnection::segment_received(const TCPSegment &seg) {
if (!active())
return;
_last_segment_time = 0;
// 是否需要發(fā)送空包回復(fù) ACK,比如沒(méi)有數(shù)據(jù)的時(shí)候收到 SYN/ACK 也要回一個(gè) ACK
bool need_empty_ack = seg.length_in_sequence_space();
auto &header = seg.header();
// 處理 RST 標(biāo)志位
if (header.rst)
return abort();
// 將包交給發(fā)送者
if (header.ack) {
need_empty_ack |= !_sender.ack_received(header.ackno, header.win);
// 隊(duì)列中已經(jīng)有數(shù)據(jù)報(bào)文段了就不需要專(zhuān)門(mén)的空包回復(fù) ACK
if (!_sender.segments_out().empty())
need_empty_ack = false;
}
// 將包交給接受者
need_empty_ack |= !_receiver.segment_received(seg);
// 被動(dòng)連接
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::SYN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::CLOSED)
return connect();
// 被動(dòng)關(guān)閉
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::SYN_ACKED)
_linger_after_streams_finish = false;
// LAST_ACK 狀態(tài)轉(zhuǎn)移到 CLOSED
if (TCPState::state_summary(_receiver) == TCPReceiverStateSummary::FIN_RECV &&
TCPState::state_summary(_sender) == TCPSenderStateSummary::FIN_ACKED && !_linger_after_streams_finish) {
_is_active = false;
return;
}
if (need_empty_ack && TCPState::state_summary(_receiver) != TCPReceiverStateSummary::LISTEN)
_sender.send_empty_segment();
// 發(fā)送其余報(bào)文段
send_segments();
}
測(cè)試
在終端中輸入 make check_lab4
就能運(yùn)行所有測(cè)試用例,測(cè)試結(jié)果如下:
發(fā)現(xiàn)有幾個(gè) txrx.sh
的測(cè)試用例失敗了,但是單獨(dú)運(yùn)行這些測(cè)試用例卻又可以通過(guò),就很奇怪:
接著測(cè)試一下吞吐量(請(qǐng)確保構(gòu)建類(lèi)型是 Release 而不是 Debug),感覺(jué)還行, 0.71Gbit/s,超過(guò)了實(shí)驗(yàn)指導(dǎo)書(shū)要求的 0.1Gbit/s。但是實(shí)際上還可以?xún)?yōu)化一下 ByteStream
類(lèi),將內(nèi)部數(shù)據(jù)類(lèi)型換成 BufferList
,這樣在寫(xiě)入數(shù)據(jù)的時(shí)候就不用一個(gè)字符一個(gè)字符插入隊(duì)列了,可以大大提高效率。
最后將 Lab0 中 webget
使用的 TCPSocket
換成 CS144TCPSocket
,重新編譯并運(yùn)行 webegt
,發(fā)現(xiàn)能夠正確得到響應(yīng)結(jié)果,說(shuō)明我們實(shí)現(xiàn)的這個(gè) CS144TCPSocket
已經(jīng)能和別的操作系統(tǒng)實(shí)現(xiàn)的 Socket
進(jìn)行交流了:
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-435157.html
后記
至此,CS144 的 TCP 實(shí)驗(yàn)部分已全部完成,可以說(shuō)是比較有挑戰(zhàn)性的一次實(shí)驗(yàn)了,尤其是 Lab4 部分,各種奇奇怪怪的 bug,編碼一晚上,調(diào)試時(shí)長(zhǎng)兩天半(約等于一坤天),調(diào)試的時(shí)候斷點(diǎn)還總是失效,最后發(fā)現(xiàn)是優(yōu)化搞的鬼,需要將 etc/cflags.cmake
第 18 行改為 set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3 -O0")
才行。以上~~文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-435157.html
到了這里,關(guān)于CS144 計(jì)算機(jī)網(wǎng)絡(luò) Lab4:TCP Connection的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!