前言
本科期間修讀了《計(jì)算機(jī)網(wǎng)絡(luò)》課程,但是課上布置的作業(yè)比較簡(jiǎn)單,只是分析了一下 Wireshark 抓包的結(jié)構(gòu),沒有動(dòng)手實(shí)現(xiàn)過(guò)協(xié)議。所以最近在嗶哩大學(xué)在線學(xué)習(xí)了斯坦福大學(xué)的 CS144 計(jì)算機(jī)網(wǎng)課程,這門課搭配了幾個(gè) Lab,要求動(dòng)手實(shí)現(xiàn)一個(gè) TCP 協(xié)議,而不是簡(jiǎn)單地調(diào)用系統(tǒng)為我們提供好的 Socket。
實(shí)驗(yàn)準(zhǔn)備
CS144 Fall2019 的課件和實(shí)驗(yàn)指導(dǎo)書可以下載自 CS144 鏡像網(wǎng)站,代碼可以從我的 Github 倉(cāng)庫(kù)獲取。
本篇博客將會(huì)介紹 Lab0 的實(shí)驗(yàn)過(guò)程,實(shí)驗(yàn)環(huán)境為 Ubuntu20.04 虛擬機(jī),使用 VSCode 完成代碼的編寫。
實(shí)驗(yàn)過(guò)程
Lab0 有兩個(gè)任務(wù),第一個(gè)任務(wù)是實(shí)現(xiàn)能發(fā)送 Get 請(qǐng)求到任意網(wǎng)址的 webget 程序,第二個(gè)任務(wù)是實(shí)現(xiàn)內(nèi)存內(nèi)的可靠字節(jié)流。
webget
實(shí)驗(yàn)指導(dǎo)書中讓我們先用 Telnet 程序連接到斯坦福大學(xué)的 Web 服務(wù)器上,在命令行中輸入 telnet cs144.keithw.org http
并回車,不出意外的話會(huì)提示已成功連接上服務(wù)器。之后手動(dòng)構(gòu)造請(qǐng)求報(bào)文,包括請(qǐng)求行和請(qǐng)求頭,輸入兩次回車就能得到響應(yīng),響應(yīng)體內(nèi)容為 Hello, CS144
。
應(yīng)用層的 Http 協(xié)議使用 TCP 傳輸層協(xié)議進(jìn)行數(shù)據(jù)的可靠性傳輸,由于我們目前還沒有實(shí)現(xiàn) TCP 協(xié)議,只能先借用一下操作系統(tǒng)寫好的的 socket 來(lái)發(fā)送 http 請(qǐng)求。CS144 的老師們十分貼心地對(duì) socket 庫(kù)進(jìn)行了二次封裝,類圖如下所示:
FileDescriptor
的部分代碼如下,可以看到內(nèi)部類 FDWrapper
持有文件描述符,會(huì)在析構(gòu)的時(shí)候調(diào)用 close()
函數(shù)釋放對(duì)文件描述符的引用 。FileDescriptor
還提供了 read()
和 write()
函數(shù)進(jìn)行文件讀寫操作:
class FileDescriptor {
//! \brief A handle on a kernel file descriptor.
//! \details FileDescriptor objects contain a std::shared_ptr to a FDWrapper.
class FDWrapper {
public:
int _fd; //!< The file descriptor number returned by the kernel
bool _eof = false; //!< Flag indicating whether FDWrapper::_fd is at EOF
bool _closed = false; //!< Flag indicating whether FDWrapper::_fd has been closed
//! Construct from a file descriptor number returned by the kernel
explicit FDWrapper(const int fd);
//! Closes the file descriptor upon destruction
~FDWrapper();
//! Calls [close(2)](\ref man2::close) on FDWrapper::_fd
void close();
};
//! A reference-counted handle to a shared FDWrapper
std::shared_ptr<FDWrapper> _internal_fd;
public:
//! Construct from a file descriptor number returned by the kernel
explicit FileDescriptor(const int fd);
//! Free the std::shared_ptr; the FDWrapper destructor calls close() when the refcount goes to zero.
~FileDescriptor() = default;
//! Read up to `limit` bytes
std::string read(const size_t limit = std::numeric_limits<size_t>::max());
//! Read up to `limit` bytes into `str` (caller can allocate storage)
void read(std::string &str, const size_t limit = std::numeric_limits<size_t>::max());
//! Write a string, possibly blocking until all is written
size_t write(const char *str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
//! Write a string, possibly blocking until all is written
size_t write(const std::string &str, const bool write_all = true) { return write(BufferViewList(str), write_all); }
//! Close the underlying file descriptor
void close() { _internal_fd->close(); }
int fd_num() const { return _internal_fd->_fd; } //!< \brief underlying descriptor number
bool eof() const { return _internal_fd->_eof; } //!< \brief EOF flag state
bool closed() const { return _internal_fd->_closed; } //!< \brief closed flag state
};
// 析構(gòu)的時(shí)候自動(dòng)釋放文件描述符
FileDescriptor::FDWrapper::~FDWrapper() {
try {
if (_closed) {
return;
}
close();
} catch (const exception &e) {
// don't throw an exception from the destructor
std::cerr << "Exception destructing FDWrapper: " << e.what() << std::endl;
}
}
我們知道,在 Linux 系統(tǒng)中 “萬(wàn)物皆文件”,socket 也被認(rèn)為是一種文件,socket 被表示成文件描述符,調(diào)用 socket()
函數(shù)返回就是一個(gè)文件描述符,對(duì) socket 的讀寫就和文件的讀寫一樣。所以 Socket
類繼承自 FileDescriptor
類,同時(shí)擁有三個(gè)子類 TCPSocket
、UDPSocket
和 LocalStreamSocket
,我們將使用 TCPSocket
完成第一個(gè)任務(wù)。
第一個(gè)任務(wù)需要補(bǔ)全 apps/webget.cc
的 get_URL()
函數(shù),這個(gè)函數(shù)接受兩個(gè)參數(shù):主機(jī)名 host
和請(qǐng)求路徑 path
:
void get_URL(const string &host, const string &path) {
TCPSocket socket;
// 連接到 Web 服務(wù)器
socket.connect(Address(host, "http"));
// 創(chuàng)建請(qǐng)求報(bào)文
socket.write("GET " + path + " HTTP/1.1\r\n");
socket.write("Host: " + host + "\r\n\r\n");
// 結(jié)束寫操作
socket.shutdown(SHUT_WR);
// 讀取響應(yīng)報(bào)文
while (!socket.eof()) {
cout << socket.read();
}
// 關(guān)閉 socket
socket.close();
}
首先調(diào)用 connect()
函數(shù)完成 TCP 的三次握手,建立與主機(jī)的連接,接著使用 write()
函數(shù)手動(dòng)構(gòu)造請(qǐng)求報(bào)文。請(qǐng)求報(bào)文的格式如下圖所示,其中請(qǐng)求行的方法是 GET,URI 為請(qǐng)求路徑 path
,Http 協(xié)議版本為 HTTP/1.1
,而首部行必須含有一個(gè) Host
鍵值對(duì)指明將要連接的主機(jī):
發(fā)送完請(qǐng)求報(bào)文后就可以結(jié)束寫操作,并不停調(diào)用 TCPSocket.read()
函數(shù)讀取響應(yīng)報(bào)文的內(nèi)容直至結(jié)束,最后關(guān)閉套接字釋放資源。其實(shí)這里也可以不手動(dòng)關(guān)閉,因?yàn)?socket
對(duì)象被析構(gòu)的時(shí)候會(huì)自動(dòng)調(diào)用 FDWrapper.close()
釋放文件描述符。
在命令行中輸入下述命令完成編譯:
mkdir build
cd build
cmake ..
make -j8
之后運(yùn)行 ./apps/webget cs144.keithw.org /hello
就能看到響應(yīng)報(bào)文了:
接著運(yùn)行測(cè)試程序,也順利通過(guò)了:
in-memory reliable byte stream
任務(wù)二要求我們實(shí)現(xiàn)一個(gè)內(nèi)存內(nèi)的有序可靠字節(jié)流:
- 字節(jié)流可以從寫入端寫入,并以相同的順序,從讀取端讀取
- 字節(jié)流是有限的,寫者可以終止寫入。而讀者可以在讀取到字節(jié)流末尾時(shí),不再讀取。
- 字節(jié)流支持流量控制,以控制內(nèi)存的使用。當(dāng)所使用的緩沖區(qū)爆滿時(shí),將禁止寫入操作。
- 寫入的字節(jié)流可能會(huì)很長(zhǎng),必須考慮到字節(jié)流大于緩沖區(qū)大小的情況。即便緩沖區(qū)只有1字節(jié)大小,所實(shí)現(xiàn)的程序也必須支持正常的寫入讀取操作。
- 在單線程環(huán)境下執(zhí)行,無(wú)需考慮多線程生產(chǎn)者-消費(fèi)者模型下各類條件競(jìng)爭(zhēng)問題。
由于寫入順序和讀出順序相同,這種先入先出的 IO 特性可以使用隊(duì)列來(lái)實(shí)現(xiàn)。C++ 標(biāo)準(zhǔn)庫(kù)提供了 std::queue
模板類,但是 std::queue
不支持迭代器,這會(huì)對(duì)后續(xù)編碼造成一點(diǎn)麻煩,所以這里換成雙端隊(duì)列 std::deque
。
類聲明如下所示,使用 deque<char>
存儲(chǔ)數(shù)據(jù),_capacity
控制隊(duì)列長(zhǎng)度,_is_input_end
代表寫入是否結(jié)束:
class ByteStream {
private:
size_t _capacity;
std::deque<char> _buffer{};
size_t _bytes_written{0};
size_t _bytes_read{0};
bool _is_input_end{false};
bool _error{}; //!< Flag indicating that the stream suffered an error.
public:
//! Construct a stream with room for `capacity` bytes.
ByteStream(const size_t capacity);
//! Write a string of bytes into the stream. Write as many
//! as will fit, and return how many were written.
//! \returns the number of bytes accepted into the stream
size_t write(const std::string &data);
//! \returns the number of additional bytes that the stream has space for
size_t remaining_capacity() const;
//! Signal that the byte stream has reached its ending
void end_input();
//! Indicate that the stream suffered an error.
void set_error() { _error = true; }
//! Peek at next "len" bytes of the stream
//! \returns a string
std::string peek_output(const size_t len) const;
//! Remove bytes from the buffer
void pop_output(const size_t len);
//! Read (i.e., copy and then pop) the next "len" bytes of the stream
//! \returns a vector of bytes read
std::string read(const size_t len) {
const auto ret = peek_output(len);
pop_output(len);
return ret;
}
//! \returns `true` if the stream input has ended
bool input_ended() const;
//! \returns `true` if the stream has suffered an error
bool error() const { return _error; }
//! \returns the maximum amount that can currently be read from the stream
size_t buffer_size() const;
//! \returns `true` if the buffer is empty
bool buffer_empty() const;
//! \returns `true` if the output has reached the ending
bool eof() const;
//! Total number of bytes written
size_t bytes_written() const;
//! Total number of bytes popped
size_t bytes_read() const;
};
類實(shí)現(xiàn):
ByteStream::ByteStream(const size_t capacity) : _capacity(capacity) {}
size_t ByteStream::write(const string &data) {
size_t ws = min(data.size(), remaining_capacity());
for (size_t i = 0; i < ws; ++i)
_buffer.push_back(data[i]);
_bytes_written += ws;
return ws;
}
//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
auto rs = min(buffer_size(), len);
return {_buffer.begin(), _buffer.begin() + rs};
}
//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
auto rs = min(len, buffer_size());
_bytes_read += rs;
for (size_t i = 0; i < rs; ++i)
_buffer.pop_front();
}
void ByteStream::end_input() { _is_input_end = true; }
bool ByteStream::input_ended() const { return _is_input_end; }
size_t ByteStream::buffer_size() const { return _buffer.size(); }
bool ByteStream::buffer_empty() const { return _buffer.empty(); }
bool ByteStream::eof() const { return buffer_empty() && input_ended(); }
size_t ByteStream::bytes_written() const { return _bytes_written; }
size_t ByteStream::bytes_read() const { return _bytes_read; }
size_t ByteStream::remaining_capacity() const { return _capacity - buffer_size(); }
之后重新 make -j8
編譯,make check_lab0
的測(cè)試結(jié)果如下,也是成功通過(guò)了全部的測(cè)試用例:
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-417813.html
后記
由于 Lab0 只是個(gè)熱身實(shí)驗(yàn),所以整體而言還是比較簡(jiǎn)單的,通過(guò)這個(gè)實(shí)驗(yàn),可以加深對(duì) Http 請(qǐng)求報(bào)文結(jié)構(gòu)的理解,同時(shí)對(duì) C++ 的 RAII 機(jī)制也會(huì)有更直觀的認(rèn)識(shí),以上~~文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-417813.html
到了這里,關(guān)于CS144 計(jì)算機(jī)網(wǎng)絡(luò) Lab0:Networking Warmup的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!