套接字和网络
一、客戶端/服務器設計模式
客戶端/服務器設計模式是用于與消息傳遞進行通信,再此模式中,有兩種類型的進程:客戶端和服務器。
- 客戶端通過連接到服務器來啟動通信。
- 客戶端向服務器發送請求,服務器發回答復。
- 最后,客戶端斷開連接。一臺服務器可以同時處理來自多個客戶端的連接,客戶端也可能連接到多個服務器。
許多互聯網應用程序都是這樣工作的:Web瀏覽器是Web服務器的客戶端,像Outlook這樣的電子郵件程序是郵件服務器的客戶端,等等。
在 Internet 上,客戶端和服務器進程通常在不同的計算機上運行,僅通過網絡連接。服務器也可以是與客戶端在同一臺計算機上運行的進程。
二、套接字和流
我們從一些與網絡通信相關的重要概念開始,以及一般的輸入/輸出。輸入/輸出 (I/O) 是指與進程之間的通信 。可能通過網絡,或與文件之間的通信,或在命令行或圖形用戶界面上與用戶進行通信。
(一)IP地址
網絡接口由IP地址標識。IP版本 4 地址是 32 位數字,由四個 8 位部分組成。
- 18.9.22.69是 MIT Web 服務器的 IP 地址。
- 173.194.193.99是谷歌網絡服務器的地址。
- 104.47.42.36是 Microsoft Outlook 電子郵件處理程序的地址。
- 127.0.0.1是環回或本地主機地址:它始終引用本地計算機。從技術上講,第一個八位字節為環回地址的任何地址都是環回地址,但都是標準的。127``127.0.0.1
(二)端口號
一臺計算機可能具有客戶端希望連接到的多個服務器應用程序,因此我們需要一種方法將同一網絡接口上的流量定向到不同的進程。
網絡接口具有由 16 位數字標識的多個端口。端口 0 是保留的,因此端口號有效地從 1 運行到 65535。
服務器進程綁定到特定端口 — 它現在正在偵聽該端口。客戶端必須知道服務器正在偵聽的端口號。有一些已知端口是為系統級進程保留的,并為某些服務提供標準端口。例如
- 端口 22 是標準的 SSH 端口。當您使用 SSH 連接到 時,軟件會自動使用端口 22。athena.dialup.mit.edu
- 端口 25 是標準電子郵件服務器端口。
- 端口 80 是標準的 Web 服務器端口。當您在 Web 瀏覽器中連接到 URL 時,它將連接到端口 80 上的 URL。http://web.mit.edu``18.9.22.69
當端口不是標準端口時,將其指定為地址的一部分。例如,URL 引用計算機上的端口 9000。。http://128.2.39.10:9000``128.2.39.10
當客戶端連接到服務器時,該傳出連接還使用客戶端網絡接口上的端口號,該端口號通常從可用的非已知端口中隨機選擇。
(三)網絡套接字
套接字表示客戶端和服務器之間連接的一端。
- 服務器進程使用偵聽套接字來等待來自遠程客戶端的連接。
- 連接的套接字可以向連接另一端的進程發送和接收消息。它由本地IP地址和端口號以及遠程地址和端口標識,這允許服務器區分來自不同IP的并發連接,或者來自不同遠程端口上的同一IP的并發連接。
(四)緩沖區
客戶端和服務器通過網絡交換的數據以塊的形式發送。這些很少只是字節大小的塊,盡管它們可能是。發送方(發送請求的客戶端或發送響應的服務器)通常寫入一大塊(可能是整個字符串,如“HELLO,WORLD!”,也可能是20兆字節的視頻數據)。
網絡將該塊切成數據包,并且每個數據包都通過網絡單獨路由。在另一端,接收器將數據包重新組合成字節流。
結果是一種突發的數據傳輸,當您想要讀取它們時,數據可能已經存在,或者您可能必須等待它們到達并重新組裝。
當數據到達時,它們進入緩沖區,緩沖區是內存中的一個數組,用于保存數據,直到您讀取數據為止。
(五)字節流
進入或流出套接字的數據是字節流。
在 Java中, [InputStream]對象表示流入程序的數據源。例如:
- 使用 [FileInputStream]從磁盤上的文件讀取
- [來自 System.in]的用戶輸入
- 來自網絡套接字的輸入
[OutputStream]對象表示數據接收器,我們可以將數據寫入的位置。例如:
- [用于保存到文件的文件輸出流]
- [系統輸出],用于向用戶正常輸出
- [系統錯誤]輸出
- 輸出到網絡套接字
(六)字符流
我們可能需要將字節流解釋為Unicode字符流,因為Unicode可以表示各種各樣的人類語言(更不用說表情符號了)。A是Unicode字符的序列,而不是字節序列,因此,如果我們想使用字符串來操作程序中的數據,那么我們需要將傳入的字節轉換為Unicode,并在寫出時將Unicode轉換回字節。InputStream``OutputStream``String
在 Java [中,讀取器]和[寫入器]表示 Unicode 字符的傳入和傳出流。例如:
- [FileReader]和 [FileWriter] 將文件視為字符序列而不是字節
- 包裝器 [InputStreamReader]和 [OutputStreamWriter] 將字節流改編為字符流
I/O的一個陷阱是確保程序使用正確的字符編碼,這意味著字節序列表示字符序列的方式。Unicode 字符最常見的字符編碼是 UTF-8。對于網絡通信,UTF-8 是正確的選擇。通常,當您創建或 時,Java 將默認為 UTF-8 編碼。但是,當計算機上的其他程序使用不同的字符編碼來讀取和寫入文件時,就會出現問題,這意味著您的Java程序無法與它們進行互操作。為了彌補這種文件兼容性問題,您平臺上的Java可能會默認使用不同的字符編碼,從系統設置中獲取它 - 然后弄亂執行網絡通信的Java代碼,其更好的默認值是UTF-8。例如,Microsoft Windows有一種名為CP-1252的非標準編碼,對于在Windows上運行的Java程序,這可能是默認設置。Reader``Writer
字符編碼錯誤可能難以檢測。UTF-8、CP-1252 和大多數其他字符編碼恰好是最古老的標準化字符編碼之一ASCII的超集。ASCII 足夠大,可以表示英語,因此英語文本往往不受字符編碼錯誤的影響。但是這個錯誤在于等待重音拉丁字符,或拉丁字母以外的腳本,或表情符號,甚至只是“花哨的”“彎曲”引號。當存在字符編碼分歧時,這些字符會變成垃圾。
若要避免字符編碼問題,請確保在構造 或 對象時顯式指定字符編碼。此讀數中的示例代碼始終指定 UTF-8。Reader``Writer
(七)阻塞
輸入/輸出流表現出阻塞行為。例如,對于套接字流:
- 當傳入套接字的緩沖區為空時,調用塊直到數據可用。read
- 當目標套接字的緩沖區已滿時,調用塊直到空間可用。write
從程序員的角度來看,阻塞非常方便,因為程序員可以編寫代碼,就好像(或)調用總是有效一樣,無論數據到達的時間如何。如果緩沖區中已有數據(或 for 的空間),則調用可能會很快返回。但是,如果讀取或寫入無法成功,則調用會阻塞。操作系統負責延遲該線程直到或可以成功為止的詳細信息。read``write``write``read``write
正如我們所看到的,阻塞發生在整個并發編程中,而不僅僅是在I / O中,并發模塊不像順序程序那樣以鎖步方式工作,因此當需要協調操作時,它們通常必須等待彼此趕上。
三、在Java中使用網絡套接字
讓我們來看看Java中套接字編程的具體細節。為了便于介紹,我們將看一個簡單方法,它只回顯客戶端發送的所有內容,以及一個從用戶獲取控制臺輸入并將其發送到 EchoServer``EchoClient``EchoServer
(一)客戶端代碼
首先,我們將從客戶的角度來看。客戶端通過構造 [Socket]對象打開與主機名和端口的連接:
String hostname = "localhost"; int port = 4589; Socket socket = new Socket(hostname, port);如果有一個服務器進程在給定的主機名上運行(在本例中,表示運行客戶端進程的同一臺計算機)并偵聽與指定端口的連接,則此構造函數將成功并生成一個打開的對象。如果沒有服務器進程偵聽該端口,則連接將失敗并引發 .localhost``Socket``new Socket()``IOException
假設連接成功,客戶端現在可以獲得與服務器通信的兩個字節流:
OutputStream outToServer = socket.getOutputStream(); InputStream inFromServer = socket.getInputStream();使用套接字時,請記住,一個進程的輸出是另一個進程的輸入。如果客戶端和服務器具有套接字連接,則該客戶端具有流向服務器輸入流的輸出流,反之亦然。
客戶端通常需要比簡單和接口提供的更強大的操作。對于 ,我們希望使用前面描述的 和 接口來使用字符流而不是字節。我們還希望讀取和寫入整行字符,以換行符終止,這是 和 提供的功能。因此,我們將流包裝在提供這些操作的類中:InputStream``OutputStream``EchoClient``Reader``Writer``BufferedReader``PrintWriter
PrintWriter writeToServer =new PrintWriter(new OutputStreamWriter(outToServer, StandardCharsets.UTF_8)); BufferedReader readFromServer =new BufferedReader(new InputStreamReader(inFromServer, StandardCharsets.UTF_8));請注意 UTF-8 字符編碼的明確規范,這是便攜式網絡通信的最佳選擇。
的基本循環通過讓用戶在鍵盤上鍵入消息,然后將消息發送到服務器,然后等待回復,為服務器準備消息:EchoClient
BufferedReader readFromUser =new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));while (true) {String message = readFromUser.readLine();...writeToServer.println(message);...String reply = readFromServer.readLine();... }請注意,所有這三個方法調用都可能被阻止。首先,客戶端進程將阻塞,直到用戶在控制臺上鍵入某些內容并按 Enter。然后,它會將其與 一起發送到服務器,但如果服務器的緩沖區恰好已滿,則此調用將阻塞,直到它可以將消息放入緩沖區。然后,該進程將阻塞,直到服務器發送其答復。println()``println()
我們不應該感到驚訝的是,此代碼與用于實現具有阻塞隊列的消息傳遞的代碼非常相似。為了向服務器發送請求,我們將請求寫入套接字輸出流,就像我們在隊列范例中使用的一樣。為了接收回復,我們從輸入流中讀取它,我們將使用 。發送和接收都使用阻止呼叫。BlockingQueue.put()``BlockingQueue.take()
上面的代碼草圖部分隱藏了兩個重要細節。首先,如果它正在讀取的流已被另一端關閉,則返回。對于套接字流,這表示服務器已關閉其連接端。 通過退出循環來響應:...``readLine()``null``EchoClient
String reply = readFromServer.readLine(); if (reply == null) break; // server closed the connection其次,最初將消息放入對象內的緩沖區中,即連接的客戶端端。在緩沖區填滿或客戶端關閉其連接端之前,不會將消息內容發送到服務器。因此,在寫入后刷新緩沖區,強制發送所有緩沖區內容至關重要:println()``PrintWriter
writeToServer.println(message); writeToServer.flush(); // important! otherwise the line may just sit in a buffer, unsent PrintWriter`具有[啟用自動刷新的構造函數](http://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/io/PrintWriter.html#(java.io.Writer,boolean)),但它僅適用于某些操作。打開自動刷新后,會自動刷新緩沖區,但看似等效的緩沖區可能會位于緩沖區中,未發送。`println(message)``print(message + "\n")最后,在退出循環后,我們關閉流和套接字,它們都向服務器發出客戶端已完成的信號,并釋放與流關聯的緩沖區內存和其他資源:
readFromServer.close(); writeToServer.close(); socket.close();(二)服務器代碼
服務器從由對象表示的偵聽套接字開始。服務器創建一個對象來偵聽特定端口號上的傳入客戶端連接:ServerSocket``ServerSocket
int port = 4589; ServerSocket serverSocket = new ServerSocket(port);如果另一個偵聽套接字已經在偵聽此端口,可能在另一個進程中,則此構造函數將拋出 a 以告訴您該地址已在使用中。BindException
請注意,創建此套接字不需要主機名,因為默認情況下,服務器套接字偵聽與運行服務器進程的計算機的任何網絡接口的連接。
與普通人不同,不提供字節流來讀取或寫入。相反,它會生成一系列新的客戶端連接。每次客戶端打開與指定端口的連接時,都會為新連接生成一個新對象。下一個客戶端連接可以使用以下方法獲得:Socket``ServerSocket``ServerSocket``Socket``accept()
Socket socket = serverSocket.accept();該方法正在阻塞。如果沒有客戶端連接處于掛起狀態,則等到客戶端到達后再返回該客戶端連接的對象。accept()``accept()``Socket
一旦服務器連接到客戶端,它就會以與客戶端大致相同的方式使用套接字的輸入和輸出流:從客戶端接收消息,并準備和發送回信。
PrintWriter writeToClient =new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)); BufferedReader readFromClient =new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));while (true) {// read a message from the clientString message = readFromClient.readLine();if (message == null) break; // client closed its side of the connectionif (message.equals("quit")) break; // client sent a quit message// prepare a reply, in this case just echoing the messageString reply = "echo: " + message;// write the replywriteToClient.println(reply);writeToClient.flush(); // important! otherwise the reply may just sit in a buffer, unsent }// close the streams and socket readFromClient.close(); writeToClient.close(); socket.close();服務器實現兩種不同的方法來停止客戶端和服務器之間的通信。我們已經在隊列消息傳遞中看到的一種方式:客戶端發送毒丸消息,在本例中。但是客戶端可以停止的另一種方法是簡單地關閉其連接的末端。 將此事件識別為套接字輸入流的結束,并通過返回 來發出信號。"quit"``readLine()``null
(三)多線程服務器代碼
我們編寫的服務器代碼具有一次只能處理一個客戶端的限制。服務器循環專用于單個客戶端,阻止并重復讀取和回復來自該客戶端的消息,直到客戶端斷開連接。只有這樣,服務器才會從排隊等待的下一個客戶端返回到其連接。readFromClient.readLine()``ServerSocket``accept
如果我們想使用阻塞 I/O 同時處理多個客戶端,則服務器需要一個新線程來處理每個新客戶端的 I/O。當每個特定于客戶端的線程使用自己的客戶端時,另一個線程(可能是主線程)已準備好進行新連接。accept
以下是多線程的工作原理。連接接受循環由主線程運行:EchoServer
while (true) {// get the next client connectionSocket socket = serverSocket.accept();// handle the client in a new thread, so that the main thread// can resume waiting for another clientnew Thread(new Runnable() {public void run() {handleClient(socket);}}).start(); }然后,客戶端處理循環由為每個新連接創建的新線程運行:
private static void handleClient(Socket socket) {// same server loop code as above:// open readFromClient and writeToClient streams// while (true) {// read message from client// prepare reply// write reply to client// }// close streams and socket }(四)使用資源試用關閉流和套接字
Java 語法的一個新位對于處理流和套接字特別有用:try-with-resources語句。此語句自動調用在其括號前導碼中聲明的變量:close()
try (// preamble: declare variables initialized to objects that need closing after use ) {// body: runs with those variables in scope } catch(...) {// catch clauses: optional, handles exceptions thrown by the preamble or body } finally {// finally clause: optional, runs after the body and any catch clause } // no matter how the try statement exits, it automatically calls // close() on all variables declared in the preamble例如,下面介紹如何使用它來確保關閉客戶端套接字連接:
try (Socket socket = new Socket(hostname, port); ) {// read and write to the socket } catch (IOException ioe) {ioe.printStackTrace(); } // socket.close() is automatically called heretry-with-resources 語句對于使用后應關閉的任何對象都很有用:
- 字節流:InputStream``OutputStream
- 字符流: Reader``Writer
- 文件:FileInputStream``FileOutputStream``FileReader``FileWriter
- 插座:Socket``ServerSocket
Python with 語句具有類似的語義
四、有線協議
(一)遠程登錄客戶端
telnet是一個實用程序,允許您與偵聽服務器建立直接網絡連接,并通過終端接口與其通信。Windows,Linux和Mac OS X都可以運行,盡管默認情況下較新的操作系統不再安裝它。
您應首先通過在命令行上運行命令來檢查是否安裝了 telnet。如果您沒有它,請查找有關如何安裝它的說明([Linux],[Mac OS])。在Windows上,另一個telnet客戶端是[PuTTY],它具有圖形用戶界面。telnet
讓我們看一些有線協議的例子。
(二)斷續器
1.超文本傳輸協議(HTTP)
超文本傳輸協議(HTTP)是萬維網的語言。我們已經知道端口 80 是眾所周知的端口,用于向 Web 服務器講 HTTP ,因此讓我們在命令行上與一個端談。
嘗試通過以下命令使用遠程登錄客戶端。用戶輸入顯示為綠色,對于 telnet 連接的輸入,換行符(按 Enter)顯示為 ?。(如果您在Windows上使用PuTTY,您將在PuTTY的連接對話框中輸入主機名和端口,并且還應該選擇連接類型:原始,并在退出時關閉窗口:從不。最后一個選項將防止窗口在服務器關閉其連接結束后立即消失。
$ telnet www.eecs.mit.edu 80 Trying 18.62.0.96... Connected to eecsweb.mit.edu. Escape character is '^]'. GET /? <!DOCTYPE html> ... lots of output ... <title>Homepage | MIT EECS</title> ... lots more output ...該命令獲取網頁。是要在網站上顯示的頁面的路徑。因此,此命令將獲取位于 的頁面。由于 80 是 HTTP 的默認端口,因此這等效于在 Web 瀏覽器中訪問 http://www.eecs.mit.edu/。結果是瀏覽器呈現的 HTML 代碼以顯示 EECS 主頁。GET``/``http://www.eecs.mit.edu:80/
互聯網協議由RFC規范定義(RFC代表“征求意見”,一些RFC最終被采用為標準)。[RFC 1945]定義了 HTTP 版本 1.0,并在 [RFC 2616 中被 HTTP 1.1 取代]。因此,對于許多網站,如果您想與它們交談,則可能需要使用HTTP 1.1。例如:
$ telnet web.mit.edu 80 Trying 18.9.22.69... Connected to web.mit.edu. Escape character is '^]'. GET /about/ HTTP/1.1? Host: web.mit.edu? ? HTTP/1.1 200 OK Date: Tue, 18 Apr 2017 15:25:23 GMT ... more headers ...9b7 <!DOCTYPE html> ... more HTML ... <title>About MIT | MIT - Massachusetts Institute of Technology</title> ... lots more HTML ... </html>0這一次,您的請求必須以空行結尾。HTTP 版本 1.1 要求客戶端在請求中指定一些額外的信息(稱為標頭),空行表示標頭的結束。
您還很可能會發現 telnet 在發出此請求后不會退出 - 這一次,服務器保持連接打開,以便您可以立即發出另一個請求。要手動退出 Telnet,請鍵入轉義字符(可能是 -)以顯示提示符,然后鍵入 :Ctrl``]``telnet>``quit
... lots more HTML ... </html> 0 Ctrl-] telnet> quit? Connection closed.2.簡單郵件傳輸協議 (SMTP)
簡單郵件傳輸協議 (SMTP)是用于發送電子郵件的協議(不同的協議用于從收件箱中檢索電子郵件的客戶端程序)。由于電子郵件系統是在垃圾郵件出現之前的時代設計的,因此現代電子郵件通信充滿了旨在防止濫用的陷阱和啟發式方法。但是我們仍然可以嘗試使用SMTP。回想一下,眾所周知的SMTP端口是25,MIT的傳入電子郵件處理程序是。mit-edu.mail.protection.outlook.com
您需要在此處填寫您的 IP 地址和在此處填寫您的用戶名,為清楚起見,? 表示換行符。這只有在您在MITnet上時才有效,即使這樣,您的郵件也可能因為看起來可疑而被拒絕:
$ telnet mit-edu.mail.protection.outlook.com 25 Trying 104.47.40.36... Connected to mit-edu.mail.protection.outlook.com. Escape character is '^]'. 220 ABC123000.mail.protection.outlook.com Microsoft ESMTP MAIL Service HELO your-IP-address-here? 250 ABC123000.mail.protection.outlook.com Hello [your-ip-address] MAIL FROM: <your-username-here@mit.edu>? 250 2.1.0 Sender OK RCPT TO: <your-username-here@mit.edu>? 250 2.1.5 Recipient OK DATA? 354 Start mail input; end with <CRLF>.<CRLF> From: <your-username-here@mit.edu>? To: <your-username-here@mit.edu>? Subject: testing? ? This is a hand-crafted artisanal email.? .? 250 2.6.0 <111111-22-33-44-55555555@ABC.eop-123.prod.protection.outlook.com> QUIT? 221 2.0.0 Service closing transmission channel Connection closed by foreign host.與HTTP相比,SMTP非常健談,甚至包括人類可讀的說明,告訴客戶端如何提交他們的消息。
(三)設計有線協議
在設計連線協議時,應用與設計抽象數據類型的操作相同的經驗法則:
- 保持較小的不同消息的數量。最好有一些可以組合的命令和響應,而不是許多復雜的消息。
- 每條消息都應該有明確的目的和連貫的行為。
- 這組消息必須足以讓客戶端發出他們需要發出的請求,并且服務器必須能夠提供結果。
正如我們要求代表獨立于我們的類型一樣,我們應該在協議中實現平臺獨立性。HTTP可以被任何操作系統上的任何Web服務器和任何Web瀏覽器使用。該協議沒有說明網頁如何存儲在磁盤上,服務器如何準備或生成網頁,客戶端將使用什么算法來呈現它們等。
我們還可以應用這門課中的三個大想法:
-
免受錯誤侵害
-
該協議應該易于客戶端和服務器生成和解析。用于讀取和編寫協議的更簡單代碼(例如,從語法自動生成的解析器,或具有正則表達式匹配庫的簡單正則表達式)將減少錯誤的機會。
-
考慮一下損壞或惡意的客戶端或服務器可能將垃圾數據填充到協議中以破壞另一端的進程的方式。
垃圾郵件就是一個例子:當我們在上面說SMTP時,郵件服務器要求我們說出誰在發送電子郵件,SMTP中沒有任何東西可以阻止我們直接撒謊。我們不得不在SMTP之上構建系統,以試圖阻止在地址上撒謊的垃圾郵件發送者。From:
安全漏洞是一個更嚴重的例子。例如,允許客戶端發送包含任意數據量的請求的協議需要在服務器上進行仔細處理,以避免緩沖區空間不足.
-
-
易于理解:例如,選擇基于文本的協議意味著我們可以通過讀取客戶端/服務器交換的文本來調試通信錯誤。它甚至允許我們“手”說出協議,正如我們上面看到的那樣。
-
準備更改:例如,HTTP包括指定版本號的功能,因此客戶端和服務器可以相互同意它們將使用哪個版本的協議。如果我們將來需要對協議進行更改,較舊的客戶端或服務器可以通過宣布它們將使用的版本來繼續工作。
[序列化]是將內存中的數據結構轉換為可以輕松存儲或傳輸的格式的過程(與[線程安全中的可序列化性](不同)。與其發明一種在客戶端和服務器之間序列化數據的新格式,不如使用現有的格式。例如,[JSON(JavaScript 對象表示法]是一種簡單且廣泛使用的格式,用于序列化基本值、數組和具有字符串鍵的映射。
(四)指定連線協議
為了精確地為客戶端和服務器定義協議允許哪些消息,請使用語法。
request ::= request-line((general-header | request-header | entity-header) CRLF)*CRLFmessage-body? request-line ::= method SPACE request-uri SPACE http-version CRLF method ::= "OPTIONS" | "GET" | "HEAD" | "POST" | ... ...使用語法,我們可以看到,在前面的此示例請求中:
GET /about/ HTTP/1.1 Host: web.mit.edu- GET是:我們要求服務器為我們獲取一個頁面。method
- /about/是:我們想要得到什么的描述。request-uri
- HTTP/1.1是 .http-version
- Host: web.mit.edu是某種標頭 — 我們必須檢查每個選項的規則才能發現哪個選項。...-header
- 我們可以看到為什么我們必須用空行結束請求:由于單個可以有多個以CRLF(換行符)結尾的標頭,因此我們在末尾有另一個CRLF來完成.request``request
- 我們沒有任何東西 - 而且由于服務器沒有等待我們是否會發送一個,大概這僅適用于其他類型的請求。message-body
語法是不夠的:在定義 ADT 時,它扮演的角色與方法簽名類似。我們仍然需要以下規范:
- 消息的前提條件是什么?
例如,如果消息中的特定字段是一串數字,那么任何數字是否有效?或者它必須是服務器已知的記錄的 ID 號?在什么情況下可以發送消息?某些消息是否僅在按特定順序發送時才有效?
- 后置條件是什么?服務器將根據郵件執行什么操作?哪些服務器端數據將被更改?服務器將向客戶端發送回什么回復?
五、測試客戶端/服務器代碼
(一)將網絡代碼與數據結構和算法分開
客戶端/服務器程序中的大多數 ADT 不需要依賴網絡。確保將它們指定、測試和實現為單獨的組件,這些組件可以免受錯誤、易于理解并隨時進行更改 — 部分原因是它們不涉及任何網絡代碼。
如果需要從多個線程(例如,處理不同客戶端連接的線程)并發使用這些 ADT,請盡可能[將消息傳遞與線程安全隊列]結合使用,如有必要,還可以[使用同步]或[限制、不可變性和現有線程安全數據類型的線程安全策略]。
(二)將套接字代碼與流代碼分開
需要讀取和寫入套接字的函數或模塊可能只需要訪問輸入/輸出流,而不需要訪問套接字本身。此設計允許您通過將模塊連接到不是來自套接字的流來測試模塊。
兩個有用的Java類是[ByteArrayInputStream]和[ByteArrayOutputStream]。假設我們要測試此方法:
void upperCaseLine(BufferedReader input, PrintWriter output) throws IOException-
需要:
input并且是開放的output
-
影響:
嘗試從中讀取一行,并嘗試將該行(大寫)寫入input``output
該方法通常與套接字一起使用:
Socket sock = ...// read a stream of characters from the socket input stream BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream(), StandardCharsets.UTF_8));// write characters to the socket output stream, with autoflushing set to true PrintWriter out = new PrintWriter(new OutputStreamWriter(sock.getOutputStream(), StandardCharsets.UTF_8), true /* autoflush */);upperCaseLine(in, out);如果基礎到大寫的轉換是我們實現的函數,則應該已經單獨指定,測試和實現它。但現在我們還可以測試以下各項的讀/寫行為:upperCaseLine
// fixed input stream of "dog" (line 1) and "cat" (line 2) String inString = "dog\ncat\n"; ByteArrayInputStream inBytes = new ByteArrayInputStream(inString.getBytes()); ByteArrayOutputStream outBytes = new ByteArrayOutputStream();// read a stream of characters from the fixed input string BufferedReader in = new BufferedReader(new InputStreamReader(inBytes, StandardCharsets.UTF_8)); // write characters to temporary storage, with autoflushing PrintWriter out = new PrintWriter(new OutputStreamWriter(outBytes, StandardCharsets.UTF_8), true);upperCaseLine(in, out);// check that it read the expected amount of input assertEquals("cat", in.readLine(), "expected input line 2 remaining"); // check that it wrote the expected output assertEquals("DOG\n", outBytes.toString(), "expected upper case of input line 1");在此測試中,并且是測試存根。為了隔離和測試,我們將它通常依賴的組件(來自套接字的輸入/輸出流)替換為滿足相同規范但具有固定行為的組件:具有固定輸入的輸入流和將輸出存儲在內存中的輸出流。inBytes``outBytes``upperCaseLine
更復雜模塊的測試策略可能使用模擬對象來模擬真實客戶端或服務器的行為,方法是生成整個固定的交互序列并斷言從其他組件接收的每條消息的正確性。
六、總結
在客戶端/服務器設計模式中,并發是不可避免的:多個客戶端和多個服務器在網絡上連接,同時發送和接收消息,并期望及時回復。當有其他客戶端等待連接到某個慢速客戶端或接收答復時,如果服務器阻止等待該客戶端,則不會使這些客戶端滿意。同時,由于不同客戶端對共享可變數據的并發修改而執行不正確計算或返回虛假結果的服務器不會讓任何人滿意。
當我們設計網絡客戶端和服務器時,使我們的多線程代碼免受錯誤,易于理解并準備更改的所有挑戰都適用。這些進程彼此并發運行(通常在不同的計算機上),任何想要同時與多個客戶端通信的服務器(或想要與多個服務器通信的客戶端)都必須管理該多線程通信。
總結
- 上一篇: 读东野圭吾《白夜行》有感
- 下一篇: 更简单获取到Bean对象(1)