Stanford CS 144, Lab 0: networking warmup
>>> lsb_release -a // 執行環境展示
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04 LTS
Release: 22.04
Codename: jammy
>>> g++ -v
gcc version 8.4.0 (Ubuntu 8.4.0-3ubuntu2)
2 Networking by hand
2.1 Fetch a Web page
visit http://cs144.keithw.org/hello and observe the result:
Now, we want to do same things by our hand.
-
telnet cs144.keithw.org http
This tells the telnet program to open a reliable byte stream between your computer and another computer (named \(\texttt{cs144.keithw.org}\)), and with a particular service running on that computer: the “http” service, for the Hyper-Text Transfer Protocol, used by the World Wide Web. Then, i saw the output in the terminal:>>> user$ telnet cs144.keithw.org http Trying 104.196.238.229... Connected to cs144.keithw.org. Escape character is '^]'. Connection closed by foreign host.
Telnet是一種網路協議,用於遠端登入到計算機或其他裝置上並在其上執行命令。透過Telnet,使用者可以透過網路連線到遠端主機並像本地主機一樣進行命令列操作。在Linux系統中,Telnet客戶端程式和伺服器程式可以使用telnet命令來啟動和連線。
Telnet協議使用客戶端/伺服器模型。Telnet客戶端向Telnet伺服器建立連線並提供憑據以進行身份驗證。一旦連線建立,Telnet客戶端可以像使用本地終端一樣在遠端系統上執行命令。
儘管Telnet協議在過去很受歡迎,但由於其不安全性,現在已被SSH協議取代。因為Telnet協議在傳輸資料時未加密,所以可能會洩露使用者的敏感資訊(例如使用者名稱和密碼)。SSH協議提供了加密和身份驗證功能,可以更安全地遠端連線到Linux系統。
-
Type
GET /hello HTTP/1.1 ⏎
-
Type
Host: cs144.keithw.org ⏎
-
Type
Connection: close ⏎
-
Hit the Enter key one more times:
⏎
This sends an empty line and tells the server that you are done with your HTTP request. -
If all went well, you will see the same response that your browser saw, preceded by HTTP headers that tell the browser how to interpret(explain) the response.
>>> user$ telnet cs144.keithw.org http
Trying 104.196.238.229...
Connected to cs144.keithw.org.
Escape character is '^]'.
GET /hello HTTP/1.1
Host: cs144.keithw.org
Connection: close
HTTP/1.1 200 OK
Date: Mon, 20 Mar 2023 11:23:14 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Connection: close
Content-Type: text/plain
Hello, CS144!
Connection closed by foreign host.
Then, we'll explain the meaning of each step.
GET /hello HTTP/1.1
This tells the server the path part of the URL(The starting with the third slash, like: http://cs144.keithw.org/hello
.)
這段命令是 HTTP 協議中客戶端向伺服器傳送 HTTP 請求的一部分,它由三部分組成:
- 請求方法(Request Method):在這裡是 GET。它指定了客戶端請求的動作型別,常見的方法有 GET、POST、PUT、DELETE 等。
- 請求 URI(Uniform Resource Identifier):在這裡是 /hello。它指定了客戶端要請求的資源的位置,URI 由路徑和查詢引數組成。例如,在這個例子中,URI 是 /hello,表示客戶端請求位於伺服器根目錄下的名為 hello 的資源。
- 協議版本(Protocol Version):在這裡是 HTTP/1.1。它指定了客戶端使用的 HTTP 協議版本。在 HTTP/1.1 中,客戶端和伺服器之間的通訊是持久連線的,這意味著客戶端可以在同一連線上傳送多個請求,並且伺服器可以在同一連線上返回多個響應。
因此,GET /hello HTTP/1.1 這個命令的意思是客戶端使用 HTTP/1.1 協議,向伺服器傳送一個 GET 請求,請求伺服器位於根目錄下的名為 hello 的資源。伺服器在收到請求後將返回相應的響應,包括狀態碼、頭部資訊和響應內容等。
Host: cs144.keithw.org
This tells the server the host part of the URL. (The part between http://
and the third slash.)
這段命令是HTTP請求中的一個頭部資訊(header),用於指定客戶端請求的目標伺服器。
在這個例子中,Host: cs144.keithw.org 指定了客戶端要請求的伺服器主機名為 cs144.keithw.org。HTTP/1.1 引入了“虛擬主機”(Virtual Host)的概念,使得多個域名可以共享同一個IP地址,並根據 Host 頭部資訊將請求路由到正確的伺服器。因此,Host 頭部資訊對於客戶端請求的處理非常重要。
除了 Host 頭部資訊,HTTP請求還可以包含許多其他頭部資訊,用於傳遞關於客戶端、請求內容、請求處理方式和請求接受格式等方面的資訊。這些頭部資訊通常使用“鍵值對”的形式表示,例如“Content-Type: application/json”表示請求中包含的資料型別為JSON格式。
Connection: close
This tells the server that you are finished making requests, and it should close the connection as soon as if finishes replying.
這段命令是HTTP請求中的一個頭部資訊(header),用於指定客戶端和伺服器之間的連線型別。
在這個例子中,Connection: close 指定了客戶端和伺服器之間的連線型別為“關閉連線”。這意味著,在客戶端傳送完請求並收到伺服器的響應後,連線將被立即關閉,而不是保持開啟狀態以等待其他請求。這種連線型別稱為“短連線”(short-lived connection)。
在HTTP/1.1中,預設情況下,客戶端和伺服器之間的連線型別為“持久連線”(persistent connection),也稱為“長連線”(long-lived connection)。這意味著客戶端可以在同一連線上傳送多個請求,並且伺服器可以在同一連線上返回多個響應。在這種情況下,Connection頭部資訊應設定為“Connection: keep-alive”。
在HTTP/2中,連線型別預設為“持久連線”,而不需要顯式指定 Connection頭部資訊。
因此,Connection頭部資訊用於指定客戶端和伺服器之間的連線型別,通常包括“關閉連線”和“保持連線”兩種型別。它對於HTTP請求和響應的處理和效能最佳化非常重要。
Assignment:
>>> user$ telnet cs144.keithw.org http
Trying 104.196.238.229...
Connected to cs144.keithw.org.
Escape character is '^]'.
GET /lab0/sunetid HTTP/1.1
Host: cs144.keithw.org
Connection: close
HTTP/1.1 200 OK
Date: Mon, 20 Mar 2023 11:59:46 GMT
Server: Apache
X-You-Said-Your-SunetID-Was: sunetid
X-Your-Code-Is: 746452
Content-length: 111
Vary: Accept-Encoding
Connection: close
Content-Type: text/plain
Hello! You told us that your SUNet ID was "sunetid". Please see the HTTP headers (above) for your secret code.
2.2 Send yourself an email
Now that you know how to fetch a Web page, it’s time to send an email message, again using a reliable byte stream to a service running on another computer. (Since we don't have a Stanford email, we have to use our own, such as QQ email.)
QQ 郵箱授權碼 tuplkrnwplxtbage
由於我們沒有 Stanford 的郵箱?,所以我們用 QQ 郵箱進行測試。首先確保:
為什麼需要 base64 編碼?
SMTP 協議使用 Base64 編碼格式是為了在傳輸郵件時保證郵件內容的完整性和可靠性。
SMTP 協議是一種文字協議,只能傳輸 ASCII 字元。如果郵件中包含了非 ASCII 字元,例如二進位制資料或其他字符集中的字元,那麼這些字元就需要進行編碼,才能在 SMTP 協議中進行傳輸。Base64 編碼是一種將任意二進位制資料轉換為 ASCII 字元的編碼方式,因此在 SMTP 協議中廣泛使用。
使用 Base64 編碼後,郵件中的非 ASCII 字元會被轉換成 ASCII 字元,從而可以在 SMTP 協議中進行傳輸。在 SMTP 協議中,郵件頭和郵件正文都需要使用 Base64 編碼進行編碼,以保證郵件內容的完整性和可靠性。同時,SMTP 協議中還規定了最大行長和最大行數等限制,以便於郵件傳輸過程中的處理和識別。
需要注意的是,使用 Base64 編碼會增加郵件的大小,從而增加傳輸的時間和頻寬消耗。因此,在實際應用中,需要根據郵件的具體情況和傳輸的環境選擇合適的編碼方式,以便實現高效的郵件傳輸。
具體實現:
- 每次選 3 個位元組共 24 位去進行編碼
- 將 24 位從高到低劃分成四個不同的 6 位進行輸出。
// 將 3 個位元組的二進位制資料編碼為 4 個 Base64 字元 void encode_triplet(const char *triplet, char *output) { // Base64 編碼使用的字符集 const char encoding_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; // 分別獲取 3 個位元組的 6 個位元的值 int byte1 = triplet[0] & 0xFF; int byte2 = triplet[1] & 0xFF; int byte3 = triplet[2] & 0xFF; // 將 3 個位元組的 24 個位元拆分為 4 個 6 位的數字 int sextet1 = byte1 >> 2; int sextet2 = ((byte1 & 0x3) << 4) | (byte2 >> 4); int sextet3 = ((byte2 & 0xF) << 2) | (byte3 >> 6); int sextet4 = byte3 & 0x3F; // 將 4 個 6 位的數字轉換為對應的 Base64 字元 output[0] = encoding_table[sextet1]; output[1] = encoding_table[sextet2]; output[2] = encoding_table[sextet3]; output[3] = encoding_table[sextet4]; }
Next is the process of sending e-mail by SMTP.
Step 1. telnet smtp.qq.com smtp(or 25)
The port number of SMTP is \(25\).
>>> user$ telnet smtp.qq.com smtp
Connected to smtp.qq.com.
Escape character is '^]'.
220 newxmesmtplogicsvrsza10-0.qq.com XMail Esmtp QQ Mail Server.
Step 2. helo qq.com
To identify your computer to the email server.
helo qq.com (our input)
250-newxmesmtplogicsvrsza10-0.qq.com-11.137.201.48-65426365
250-SIZE 73400320
250 OK
Extra Step. Authorization our qq-mail
auth login (out input)
334 VXNlcm5hbWU6 // base64 =(decode)> 334 Username:
MTM0MzAzMzA4MkBxcS5jb20= (out input, base64)
334 UGFzc3dvcmQ6 // base64 =(decode)> 334 Password:
dHVwbGtybndwXXXXYmFnZQ== (out input, base64)
235 Authentication successful
We just need to get ready for QQ Mail and the base64 code of the authorization code.
Step 3. Send your email
mail from: <1343033082@qq.com> (our input)
250 OK
rcpt to: <whuwkl@gmail.com> (our input)
250 OK
data (ourinput)
354 End data with <CR><LF>.<CR><LF>.
from: wkl <1343033082@qq.com> // Here are all our inputs.
to: whuwkl <whuwkl@gmail.com>
Cc: pkuwkl@gmail.com
Data: Tue, 15 Jan 2023
Subject: Test SMTP
Hello, my frineds. I'm testing SMTP protocal
Your friend, Keli Wen
.
250 OK: queued as.
mail from: <xx@xx.com>
denote who is sending?rcpt to: <xx@xx.com>
denote who is receiving?data
tell server you're ready to start. end with⏎ . ⏎
Result ✅
Listening and connection
netcat -v -l -p 9090
on server side.
>>> $ netcat -v -l -p 9090
Listening on [0.0.0.0] (family 0, port 9090)
talnet localhost 9090
on client side. will received Connected to localhost.
Then, you'll notice that anything you type in one window appear in the other, and vice versa.
netcat
是一種網路工具,可以在不同的網路裝置之間建立 TCP 或 UDP 連線,用於傳輸資料。在命令列中輸入netcat
命令可以啟動netcat
程式,並使用不同的引數配置其行為。在給定的命令
netcat -v -l -p 9090
中,使用了以下三個引數:
-v
:表示啟用詳細模式(verbose mode),在傳輸資料時顯示更多的資訊。例如,可以顯示連線建立的細節、傳輸的資料大小、傳輸速度等。-l
:表示將netcat
程式設定為監聽模式(listen mode),等待其他裝置建立連線並傳輸資料。在監聽模式下,netcat
會一直等待連線,直到接收到連線請求為止。-p 9090
:表示指定監聽的埠號為 9090。在 TCP/IP 網路中,埠號用於標識不同的網路應用程式,例如 Web 伺服器、SMTP 伺服器等。netcat
使用指定的埠號進行監聽,等待其他裝置連線並傳輸資料。因此,執行
netcat -v -l -p 9090
命令後,netcat
會啟動並進入監聽模式,等待其他裝置連線到本機的 9090 埠並傳輸資料。在傳輸資料時,netcat
會顯示詳細的傳輸資訊,例如連線建立的細節、傳輸的資料大小、傳輸速度等。如果帶了引數
-u
則是 UDP,反之則是預設的 TCP。
3 Writing a network program using an OS stream socket
在這個熱身實驗的下一部分中,您將編寫一個簡短的程式,透過網際網路獲取Web頁面。您將利用Linux核心以及大多數其他作業系統提供的功能:在您的計算機上執行一個程式,另一個程式在網際網路上的另一臺計算機上執行,它們之間可以建立可靠的雙向位元組流(例如,像Apache或nginx這樣的Web伺服器或netcat程式)。
這個功能被稱為流式套接字(stream socket)。對於您的程式和Web伺服器,套接字看起來像一個普通的檔案描述符(類似於磁碟上的檔案或標準輸入輸出流)。當兩個流式套接字連線在一起時,寫入一個套接字的任何位元組最終都會以相同的順序從另一個計算機的另一個套接字中出現。
但實際上,網際網路並沒有提供可靠的位元組流服務。相反,網際網路實際上只是盡力將稱為網際網路資料包的短資料塊傳遞到其目標。每個資料包包含一些後設資料(標頭),用於指定諸如源地址和目標地址之類的內容 - 它來自哪臺計算機以及它將前往哪臺計算機,以及要傳遞到目標計算機的一些負載資料(最多約1500個位元組)。
雖然網路嘗試傳遞每個資料包,但在實踐中,資料包可能會(1)丟失,(2)無序傳遞,(3)傳遞的內容發生改變,甚至(4)重複並傳遞多次。通常,連線兩端的作業系統的任務是將“盡力而為的資料包”(網際網路提供的抽象)轉換為“可靠的位元組流”(應用程式通常想要的抽象)。
兩臺計算機必須合作,以確保流中的每個位元組最終都按其在行中的正確位置傳遞到另一側的流式套接字。它們還必須告訴彼此它們準備從另一臺計算機接受多少資料,並確保不傳送超過另一側願意接受的資料量。所有這些都是使用1981年制定的約定方案完成的,稱為傳輸控制協議(TCP,Transmission Control Protocal)。
在本實驗中,您只需使用作業系統對傳輸控制協議的預先存在的支援。您將編寫一個名為“webget”的程式,建立一個TCP流式套接字,連線到Web伺服器並獲取頁面 - 就像您在本實驗的早期所做的那樣。在未來的實驗中,您將從不太可靠的資料包中,透過自己實現傳輸控制協議來建立可靠的位元組流,從而實現此抽象的另一側。
3.1 Let's get started
兩個要點:
git clone https://github.com/cs144/sponge
。make -j4
to use four processors.j
means jobs in parallel.
3.2 Morden C++: mostly safe but still fast and low-level
LAB將使用現代C ++風格完成,使用最近(2011年)的功能來儘可能安全地程式設計。這可能與您以前被要求編寫C ++的方式不同。有關此樣式的參考,請參閱C ++核心指南(http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)。
基本思想是確保每個物件的設計具有可能的最小公共介面,具有大量的內部安全檢查,並且很難被不正確使用,並且知道如何在自身清理之後進行清理。我們希望避免“配對”操作(例如,malloc / free或new / delete),其中第二個操作可能不會發生(例如,如果函式提前返回或丟擲異常)。相反,操作發生在物件的建構函式中,相反的操作發生在解構函式中。這種風格稱為“資源獲取即初始化”(RAII, resource acquisition is initialization)。
具體來說:
- 使用 cppreference 作為文件
- 不要使用
malloc()
,free()
,new
和delete
。(有點好奇兩者的區別) - 不要使用裸露的指標
*
,使用智慧指標如:unique_ptr
andshared_ptr
,當然在 CS144 用不太到。 - 避免使用 templates, threads, locks and virtual functions. will not need to use these in CS 144.
- 避免使用 C-style 的字串(\(\texttt{char *str}\))或字串函式(\(\texttt{strlen(), strcpy()}\))。它們很容易出錯。改用 \(\texttt{std::string}\)。
- 永遠不要使用 C-style 轉換(例如,\(\texttt{(FILE *) x}\))。如果必須使用,請使用 C++ \(\texttt{static\_cast}\)(在 CS144 中通常不需要)。
- 優先透過
const
引用傳遞函式引數(例如:\(\texttt{const Address&address}\))。 - 除非需要改變,否則將每個變數設定為
const
。 - 除非需要改變物件,否則將每個方法設定為
const
。 - 避免使用全域性變數,並使每個變數具有最小的作用域。
- 在提交作業之前,請執行
make format
以規範化編碼樣式。
3.3 Reading the Sponge documentation
在 CS144 計算機網路課程中,Sponge 是一個網路模擬器(Network Emulator)庫,用於模擬計算機網路中的資料包傳輸和路由過程。Sponge 可以在單個計算機上模擬多個網路節點,每個節點可以執行自己的應用程式,透過模擬器進行通訊。Sponge 還提供了豐富的網路除錯和分析工具,例如抓包工具、流量監控工具等,可以幫助學生理解和除錯網路協議。
在 CS144 課程中,學生可以使用 Sponge 模擬各種網路場景,例如頻寬受限、延遲抖動、擁塞控制等,從而深入理解計算機網路中的各種原理和技術。Sponge 還提供了豐富的網路協議庫,例如 TCP、UDP、IP 等,可以幫助學生實現自己的網路應用程式。
為了支援這種程式設計風格,Sponge的類將作業系統函式(可從C中呼叫)包裝在“現代”C ++中。
- 使用 Web 瀏覽器閱讀起始程式碼的文件,網址為 \(\texttt{https://cs144.github.io/doc/lab0}\)。
- 特別注意\(\texttt{FileDescriptor}\),\(\texttt{Socket}\),\(\texttt{TCPSocket}\)和\(\texttt{Address}\)類的文件。(注意,\(\texttt{Socket}\) 是 \(\texttt{FileDescriptor}\) 的一種型別,而 \(\texttt{TCPSocket}\) 是 \(\texttt{Socket}\) 的一種型別。)
- 現在,在\(\texttt{libsponge/util}\)目錄中找到並閱讀描述這些類介面的標頭檔案:\(\texttt{file\_descriptor.hh}\),\(\texttt{socket.hh}\) 和 \(\texttt{address.hh}\)。
3.4 Writing webget
It’s time to implement webget
, a program to fetch Web pages over the Internet using the operating system’s TCP support and stream-socket abstraction-just like you did by hand earlier in this lab.
後續的操作就是按照 lab 的指導進行的,我就不重複性描述了,記錄幾點我當時出現問題的地方:
- 首先,我是參考
TCPSocket
部分的示例文件進行第一步的。 - 注意,換行符應該是
\r\n
。而且,最後結束需要連續兩個\r\n
。 - 其他就是一些 API 的應用了。
程式碼如下:
void get_URL(const string &host, const string &path) {
// Your code here.
// You will need to connect to the "http" service on
// the computer whose name is in the "host" string,
// then request the URL path given in the "path" string.
// Then you'll need to print out everything the server sends back,
// (not just one call to read() -- everything) until you reach
// the "eof" (end of file).
Address HostAddress(host, "http");
TCPSocket Socket1;
// connect a socket to a specified peer address with connect(2)
Socket1.connect(HostAddress);
// create a HTTP Request. Remember you should add space.
std::string HTTPrequest = "GET " + path + " HTTP/1.1\r\n" + \
"Host: " + host + " \r\n" + \
"Connection: close\r\n" + \
"\r\n";
Socket1.write(HTTPrequest);
while (!Socket1.eof()){
auto recv = Socket1.read();
std::cout << recv;
}
Socket1.close();
}
需要注意的是:
在網路程式設計中,
connect(2)
是一個函式呼叫,用於將一個套接字(socket)連線到一個指定的網路地址(peer address),以建立一個網路連線。具體來說,
connect(2)
函式通常用於客戶端程式,用於連線到一個執行在遠端主機上的伺服器程式。在呼叫connect(2)
函式時,需要指定連線目標的 IP 地址和埠號,這個目標地址就是所謂的 "peer address"。例如,下面是一個簡單的客戶端程式,它使用
connect(2)
函式連線到遠端伺服器:
下面是程式碼日誌:
>>> $ make
[ 33%] Built target sponge
Consolidate compiler generated dependencies of target webget
[ 36%] Building CXX object apps/CMakeFiles/webget.dir/webget.cc.o
[ 40%] Linking CXX executable webget
[ 40%] Built target webget
[ 46%] Built target spongechecks
[ 53%] Built target byte_stream_construction
[ 60%] Built target byte_stream_one_write
[ 66%] Built target byte_stream_two_writes
[ 73%] Built target byte_stream_capacity
[ 80%] Built target byte_stream_many_writes
[ 86%] Built target address_dt
[ 93%] Built target parser_dt
[100%] Built target socket_dt
>>> $ ./apps/webget cs144.keithw.org /hello
HTTP/1.1 200 OK
Date: Tue, 28 Mar 2023 15:02:24 GMT
Server: Apache
Last-Modified: Thu, 13 Dec 2018 15:45:29 GMT
ETag: "e-57ce93446cb64"
Accept-Ranges: bytes
Content-Length: 14
Connection: close
Content-Type: text/plain
Hello, CS144!
>>> $ make check_webget
[100%] Testing webget...
Test project /home/xxx/CS144/lab0/sponge/build
Start 31: t_webget
1/1 Test #31: t_webget ......................... Passed 1.54 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 1.55 sec
[100%] Built target check_webget
4 An in-memory reliable byte stream
By now, you’ve seen how the abstraction of a reliable byte stream can be useful in communicating across the Internet, even though the Internet itself only provides the service of “best-effort” (unreliable) datagrams.
To finish off this week’s lab, you will implement, in memory on a single computer, an object that provides this abstraction. (You may have done something similar in CS 110.) Bytes are written on the “input” side and can be read, in the same sequence, from the “output” side. The byte stream is finite: the writer can end the input, and then no more bytes can be written. When the reader has read to the end of the stream, it will reach “EOF” (end of file) and no more bytes can be read.
Your byte stream will also be flow-controlled to limit its memory consumption at any given time. The object is initialized with a particular “capacity”: the maximum number of bytes it’s willing to store in its own memory at any given point. The byte stream will limit the writer in how much it can write at any given moment, to make sure that the stream doesn’t exceed its storage capacity. As the reader reads bytes and drains them from the stream, the writer is allowed to write more. Your byte stream is for use in a single thread—you don’t have to worry about concurrent writers/readers, locking, or race conditions.
To be clear: the byte stream is finite, but it can be almost arbitrarily long 4 before the writer ends the input and finishes the stream. Your implementation must be able to handle streams that are much longer than the capacity. The capacity limits the number of bytes that are held in memory (written but not yet read) at a given point, but does not limit the length of the stream. An object with a capacity of only one byte could still carry a stream that is terabytes and terabytes long, as long as the writer keeps writing one byte at a time and the reader reads each byte before the writer is allowed to write the next byte.
到現在為止,您已經瞭解到可靠位元組流的抽象可以在網際網路上進行通訊時非常有用,即使網際網路本身只提供“盡力而為”的(不可靠的)資料包服務。
為了完成本週的實驗,您將在單臺計算機的記憶體中實現一個提供此抽象的物件。 (您可能已經在CS 110中做過類似的事情。)位元組在“輸入”端被寫入,可以按相同順序從“輸出”端讀取。位元組流是有限的:作者可以結束輸入,然後不能再寫入更多位元組。當讀者讀取到流的末尾時,它將達到“EOF”(檔案結束)狀態,不能再讀取更多位元組。
您的位元組流還將進行流量控制,以限制其在任何給定時間的記憶體消耗。該物件使用特定的“容量”進行初始化:它願意在任何給定時刻儲存在自己的記憶體中的最大位元組數。位元組流將限制作者在任何給定時刻寫入的數量,以確保流不超過其儲存容量。隨著讀者讀取位元組並從流中排除它們,作者被允許寫更多內容。您的位元組流用於單個執行緒 - 您不必擔心併發作者/讀者、鎖定或競爭條件。(暫時不需要考慮執行緒安全)。
要明確:位元組流是有限的,但在作者結束輸入並完成流之前,它可以幾乎任意長。您的實現必須能夠處理比容量長得多的流。容量限制在給定點保留在記憶體中的位元組數(已寫入但尚未讀取),但不限制流的長度。容量僅為一個位元組的物件仍然可以攜帶數千億個位元組長的流,只要作者一次寫入一個位元組並且讀者在作者被允許寫入下一個位元組之前讀取每個位元組。
接下來是一些我們需要去實現的 interface
,具體的檔案可以開啟 \(\texttt{libsponge/byte\_stream.hh}\) 和 \(\texttt{libsponge/byte\_stream.cc}\)。具體每個成員函式需要實現什麼功能同樣在文件中可以查詢。
由於,我們需要同時入隊,出隊,並且還需要遍歷 buffer
中的元素,所以我們選擇使用 deque
來作為主要資料結構。下面是我們定義的類的私有變數,主要意思如變數名所示。
/* byte_stream.hh file */
class ByteStream {
private:
size_t _capacity;
size_t _buffer_read_count;
size_t _buffer_written_count;
std::deque<char> _buffer;
// Hint: This doesn't need to be a sophisticated data structure at
// all, but if any of your tests are taking longer than a second,
// that's a sign that you probably want to keep exploring
// different approaches.
bool _end_input{}; //?< Flag indicating that the stream has end the input.
bool _error{}; //!< Flag indicating that the stream suffered an error.
public:
/* omit */
};
4.x 實驗問題解析
隨後是原始碼,我遇到了其中如下幾個問題:
這個問題,是一個編譯器錯誤。指出類的建構函式中某個成員變數應該在成員初始化列表中進行初始化,而不應該在建構函式的函式體中進行初始化。
這兩個都很重要,這代表了我在類的建構函式方面知識的缺失。以下知識均來自 《C++ primer V5》知識的總結(P258)。首先,我們瞭解問題2,建構函式中,冒號:
和第一個左花括號{
之間的內容是建構函式初始值列表。理論上,這是在無論何種情況下更好的程式設計習慣,但有時候是必須使用建構函式初始值列表。我們將列舉幾個必須使用的情況。
首先,我們要分清楚兩種不同建構函式之間的區別,case1
是直接進行初始化,而 case2
相當於先初始化再進行賦值,如果沒有在建構函式初始值列表的話,就會直接呼叫預設的建構函式再進行賦值。稍微靈敏一些的話,不難發現此處應該存在好幾個問題:
- 先構造,再賦值,會存在一些特殊的情況是不能進行賦值的,並且有些成員變數並沒有預設建構函式/初始化方法。
- 先構造,再賦值,在效率方面也會存在很多問題。
class Test{
public:
Test(int ii): i(ii), ci(ii), ri(i) {} // case1
Test(int ii){ // case2
i = ii; // 正確
ci = ii; // 錯誤,不能給 const 賦值
ri = i; // 錯誤,ri 沒有被初始化
}
private:
int i;
const int ci;
int &ri;
}
所以,如果成員是 const
,引用或者屬於某種未提供預設建構函式的類型別,我們必須透過建構函式初始值列表為這些成員變數提供初值。
其次,初始化和賦值的區別事關底層效率問題,建議讀者養成使用建構函式初始值的習慣。
此時,,回到問題1,需要知道的是,在使用建構函式初始值時,初始化的順序,一定和類定義中出現的順序一致,和程式碼順序無關。一般來說,不存在問題,但是如果出現了一個成員是用另一個成員來初始化的話,這個初始化順序就很關鍵裡,如下面的例子。這也是為什麼 CS144 要求大家必須按照類中定義好的順序進行初始化。
class X{
int i;
int j;
public:
// undefine: i 在 j 之前進行初始化
X(int val): j(val), i(j) { }
}
/* byte_stream.cc file */
ByteStream::ByteStream(const size_t capacity):
_capacity(capacity),
_buffer_read_count(0),
_buffer_written_count(0),
_buffer(deque<char>()),
_end_input(false),
_error(false) { }
size_t ByteStream::write(const string &data) {
if (input_ended()) // input is ended
return 0;
size_t data_len = data.length();
size_t write_len = 0;
// while the _buffer not full
while (_buffer.size() < _capacity && write_len < data_len){
_buffer.push_back(data[write_len]);
++ write_len;
}
_buffer_written_count += write_len; // update
return write_len;
}
//! \param[in] len bytes will be copied from the output side of the buffer
string ByteStream::peek_output(const size_t len) const {
size_t _buffer_len = _buffer.size();
std::string output = "";
for (int i = 0, _lim = std::min(_buffer_len, len);
i < _lim; ++ i){
output.push_back(_buffer[i]);
}
return output;
}
//! \param[in] len bytes will be removed from the output side of the buffer
void ByteStream::pop_output(const size_t len) {
size_t _buffer_len = _buffer.size();
for (int i = 0, _lim = std::min(_buffer_len, len);
i < _lim; ++ i){
_buffer.pop_front(); // removed from the output side
}
_buffer_read_count += std::min(_buffer_len, len); //! pop count not read count
}
//! Read (i.e., copy and then pop) the next "len" bytes of the stream
//! \param[in] len bytes will be popped and returned
//! \returns a string
std::string ByteStream::read(const size_t len) {
auto output = peek_output(len);
pop_output(len);
return output;
}
void ByteStream::end_input() { _end_input = true; }
bool ByteStream::input_ended() const { return _end_input; }
size_t ByteStream::buffer_size() const { return _buffer.size(); }
bool ByteStream::buffer_empty() const { return _buffer.empty(); }
//TODO:看的網上的
bool ByteStream::eof() const { return input_ended() && buffer_empty(); }
size_t ByteStream::bytes_written() const { return _buffer_written_count; }
size_t ByteStream::bytes_read() const { return _buffer_read_count; }
size_t ByteStream::remaining_capacity() const { return _capacity - _buffer.size(); }
最開始,一直在這個 test
上報錯,後面參考了網上的一些設計才過關。
在 C++ 中,return {} 語句表示返回一個值初始化的物件,具體取決於函式的返回型別。
對於類型別,返回的是一個預設建構函式建立的物件;對於內建型別,返回的是預設值 0 或者 nullptr。
例如,如果函式返回型別為 int,則 return {} 表示返回整數值 0。如果函式返回型別為
std::vector<int>
,則 return {} 表示返回一個空的 int 型別的 vector 物件。這種方式的語法稱為 "值初始化",它可以確保返回的物件在被建立時被初始化為預設值,從而避免了可能存在的未初始化值的問題。使用這種方式還可以避免手動編寫預設建構函式或特殊處理預設情況的程式碼,從而簡化了程式碼實現的複雜度。
需要注意的是,return {} 語句僅適用於 C++11 及以上版本,之前的 C++ 版本可能需要使用其他語法來實現相同的效果。
4. x 實驗結果
>>> $ make check_lab0 -j10
[100%] Testing Lab 0...
Test project /home/wkl/CS144/lab0/sponge/build
Start 26: t_byte_stream_construction
1/9 Test #26: t_byte_stream_construction ....... Passed 0.00 sec
Start 27: t_byte_stream_one_write
2/9 Test #27: t_byte_stream_one_write .......... Passed 0.00 sec
Start 28: t_byte_stream_two_writes
3/9 Test #28: t_byte_stream_two_writes ......... Passed 0.00 sec
Start 29: t_byte_stream_capacity
4/9 Test #29: t_byte_stream_capacity ........... Passed 0.44 sec
Start 30: t_byte_stream_many_writes
5/9 Test #30: t_byte_stream_many_writes ........ Passed 0.01 sec
Start 31: t_webget
6/9 Test #31: t_webget ......................... Passed 1.04 sec
Start 53: t_address_dt
7/9 Test #53: t_address_dt ..................... Passed 0.04 sec
Start 54: t_parser_dt
8/9 Test #54: t_parser_dt ...................... Passed 0.00 sec
Start 55: t_socket_dt
9/9 Test #55: t_socket_dt ...................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 9
Total Test time (real) = 1.55 sec
[100%] Built target check_lab0
Reference
後記
在轉型 Infra 的路徑,Network 一直是我最弱的地方,這算是第一個外國的 LAB,也是 Dream 中的 Dream 的 Stanford,這輩子估計沒啥機會去四大。按理來說是一週之內完成一個 LAB,但是 LAB0 就做了已經很久了。不過希望會越來越有效率吧。