?? chapter15.htm
字號:
8080芯片,那是一部使用CP/M操作系統的機子)。<br>
<br>
15.2 套接字<br>
“套接字”或者“插座”(Socket)也是一種軟件形式的抽象,用于表達兩臺機器間一個連接的“終端”。針對一個特定的連接,每臺機器上都有一個“套接字”,可以想象它們之間有一條虛擬的“線纜”。線纜的每一端都插入一個“套接字”或者“插座”里。當然,機器之間的物理性硬件以及電纜連接都是完全未知的。抽象的基本宗旨是讓我們盡可能不必知道那些細節。<br>
在Java中,我們創建一個套接字,用它建立與其他機器的連接。從套接字得到的結果是一個InputStream以及OutputStream(若使用恰當的轉換器,則分別是Reader和Writer),以便將連接作為一個IO流對象對待。有兩個基于數據流的套接字類:ServerSocket,服務器用它“偵聽”進入的連接;以及Socket,客戶用它初始一次連接。一旦客戶(程序)申請建立一個套接字連接,ServerSocket就會返回(通過accept()方法)一個對應的服務器端套接字,以便進行直接通信。從此時起,我們就得到了真正的“套接字-套接字”連接,可以用同樣的方式對待連接的兩端,因為它們本來就是相同的!此時可以利用getInputStream()以及getOutputStream()從每個套接字產生對應的InputStream和OutputStream對象。這些數據流必須封裝到緩沖區內。可按第10章介紹的方法對類進行格式化,就象對待其他任何流對象那樣。<br>
對于Java庫的命名機制,ServerSocket(服務器套接字)的使用無疑是容易產生混淆的又一個例證。大家可能認為ServerSocket最好叫作“ServerConnector”(服務器連接器),或者其他什么名字,只是不要在其中安插一個“Socket”。也可能以為ServerSocket和Socket都應從一些通用的基礎類繼承。事實上,這兩種類確實包含了幾個通用的方法,但還不夠資格把它們賦給一個通用的基礎類。相反,ServerSocket的主要任務是在那里耐心地等候其他機器同它連接,再返回一個實際的Socket。這正是“ServerSocket”這個命名不恰當的地方,因為它的目標不是真的成為一個Socket,而是在其他人同它連接的時候產生一個Socket對象。<br>
然而,ServerSocket確實會在主機上創建一個物理性的“服務器”或者偵聽用的套接字。這個套接字會偵聽進入的連接,然后利用accept()方法返回一個“已建立”套接字(本地和遠程端點均已定義)。容易混淆的地方是這兩個套接字(偵聽和已建立)都與相同的服務器套接字關聯在一起。偵聽套接字只能接收新的連接請求,不能接收實際的數據包。所以盡管ServerSocket對于編程并無太大的意義,但它確實是“物理性”的。<br>
創建一個ServerSocket時,只需為其賦予一個端口編號。不必把一個IP地址分配它,因為它已經在自己代表的那臺機器上了。但在創建一個Socket時,卻必須同時賦予IP地址以及要連接的端口編號(另一方面,從ServerSocket.accept()返回的Socket已經包含了所有這些信息)。<br>
<br>
15.2.1 一個簡單的服務器和客戶機程序<br>
這個例子將以最簡單的方式運用套接字對服務器和客戶機進行操作。服務器的全部工作就是等候建立一個連接,然后用那個連接產生的Socket創建一個InputStream以及一個OutputStream。在這之后,它從InputStream讀入的所有東西都會反饋給OutputStream,直到接收到行中止(END)為止,最后關閉連接。<br>
客戶機連接與服務器的連接,然后創建一個OutputStream。文本行通過OutputStream發送。客戶機也會創建一個InputStream,用它收聽服務器說些什么(本例只不過是反饋回來的同樣的字句)。<br>
服務器與客戶機(程序)都使用同樣的端口號,而且客戶機利用本地主機地址連接位于同一臺機器中的服務器(程序),所以不必在一個物理性的網絡里完成測試(在某些配置環境中,可能需要同真正的網絡建立連接,否則程序不能工作——盡管實際并不通過那個網絡通信)。<br>
下面是服務器程序:<br>
<br>
831-832頁程序<br>
<br>
可以看到,ServerSocket需要的只是一個端口編號,不需要IP地址(因為它就在這臺機器上運行)。調用accept()時,方法會暫時陷入停頓狀態(堵塞),直到某個客戶嘗試同它建立連接。換言之,盡管它在那里等候連接,但其他進程仍能正常運行(參考第14章)。建好一個連接以后,accept()就會返回一個Socket對象,它是那個連接的代表。<br>
清除套接字的責任在這里得到了很藝術的處理。假如ServerSocket構建器失敗,則程序簡單地退出(注意必須保證ServerSocket的構建器在失敗之后不會留下任何打開的網絡套接字)。針對這種情況,main()會“擲”出一個IOException違例,所以不必使用一個try塊。若ServerSocket構建器成功執行,則其他所有方法調用都必須到一個try-finally代碼塊里尋求保護,以確保無論塊以什么方式留下,ServerSocket都能正確地關閉。<br>
同樣的道理也適用于由accept()返回的Socket。若accept()失敗,那么我們必須保證Socket不再存在或者含有任何資源,以便不必清除它們。但假若執行成功,則后續的語句必須進入一個try-finally塊內,以保障在它們失敗的情況下,Socket仍能得到正確的清除。由于套接字使用了重要的非內存資源,所以在這里必須特別謹慎,必須自己動手將它們清除(Java中沒有提供“破壞器”來幫助我們做這件事情)。<br>
無論ServerSocket還是由accept()產生的Socket都打印到System.out里。這意味著它們的toString方法會得到自動調用。這樣便產生了:<br>
<br>
833頁中程序<br>
<br>
大家不久就會看到它們如何與客戶程序做的事情配合。<br>
程序的下一部分看來似乎僅僅是打開文件,以便讀取和寫入,只是InputStream和OutputStream是從Socket對象創建的。利用兩個“轉換器”類InputStreamReader和OutputStreamWriter,InputStream和OutputStream對象已經分別轉換成為Java
1.1的Reader和Writer對象。也可以直接使用Java1.0的InputStream和OutputStream類,但對輸出來說,使用Writer方式具有明顯的優勢。這一優勢是通過PrintWriter表現出來的,它有一個過載的構建器,能獲取第二個參數——一個布爾值標志,指向是否在每一次println()結束的時候自動刷新輸出(但不適用于print()語句)。每次寫入了輸出內容后(寫進out),它的緩沖區必須刷新,使信息能正式通過網絡傳遞出去。對目前這個例子來說,刷新顯得尤為重要,因為客戶和服務器在采取下一步操作之前都要等待一行文本內容的到達。若刷新沒有發生,那么信息不會進入網絡,除非緩沖區滿(溢出),這會為本例帶來許多問題。<br>
編寫網絡應用程序時,需要特別注意自動刷新機制的使用。每次刷新緩沖區時,必須創建和發出一個數據包(數據封)。就目前的情況來說,這正是我們所希望的,因為假如包內包含了還沒有發出的文本行,服務器和客戶機之間的相互“握手”就會停止。換句話說,一行的末尾就是一條消息的末尾。但在其他許多情況下,消息并不是用行分隔的,所以不如不用自動刷新機制,而用內建的緩沖區判決機制來決定何時發送一個數據包。這樣一來,我們可以發出較大的數據包,而且處理進程也能加快。<br>
注意和我們打開的幾乎所有數據流一樣,它們都要進行緩沖處理。本章末尾有一個練習,清楚展現了假如我們不對數據流進行緩沖,那么會得到什么樣的后果(速度會變慢)。<br>
無限while循環從BufferedReader in內讀取文本行,并將信息寫入System.out,然后寫入PrintWriter.out。注意這可以是任何數據流,它們只是在表面上同網絡連接。<br>
客戶程序發出包含了"END"的行后,程序會中止循環,并關閉Socket。<br>
下面是客戶程序的源碼:<br>
<br>
834-835頁程序<br>
<br>
在main()中,大家可看到獲得本地主機IP地址的InetAddress的三種途徑:使用null,使用localhost,或者直接使用保留地址127.0.0.1。當然,如果想通過網絡同一臺遠程主機連接,也可以換用那臺機器的IP地址。打印出InetAddress
addr后(通過對toString()方法的自動調用),結果如下:<br>
localhost/127.0.0.1<br>
通過向getByName()傳遞一個null,它會默認尋找localhost,并生成特殊的保留地址127.0.0.1。注意在名為socket的套接字創建時,同時使用了InetAddress以及端口號。打印這樣的某個Socket對象時,為了真正理解它的含義,請記住一次獨一無二的因特網連接是用下述四種數據標識的:clientHost(客戶主機)、clientPortNumber(客戶端口號)、serverHost(服務主機)以及serverPortNumber(服務端口號)。服務程序啟動后,會在本地主機(127.0.0.1)上建立為它分配的端口(8080)。一旦客戶程序發出請求,機器上下一個可用的端口就會分配給它(這種情況下是1077),這一行動也在與服務程序相同的機器(127.0.0.1)上進行。現在,為了使數據能在客戶及服務程序之間來回傳送,每一端都需要知道把數據發到哪里。所以在同一個“已知”服務程序連接的時候,客戶會發出一個“返回地址”,使服務器程序知道將自己的數據發到哪兒。我們在服務器端的示范輸出中可以體會到這一情況:<br>
Socket[addr=127.0.0.1,port=1077,localport=8080]<br>
這意味著服務器剛才已接受了來自127.0.0.1這臺機器的端口1077的連接,同時監聽自己的本地端口(8080)。而在客戶端:<br>
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]<br>
這意味著客戶已用自己的本地端口1077與127.0.0.1機器上的端口8080建立了
連接。<br>
大家會注意到每次重新啟動客戶程序的時候,本地端口的編號都會增加。這個編號從1025(剛好在系統保留的1-1024之外)開始,并會一直增加下去,除非我們重啟機器。若重新啟動機器,端口號仍然會從1025開始增值(在Unix機器中,一旦超過保留的套按字范圍,數字就會再次從最小的可用數字開始)。<br>
創建好Socket對象后,將其轉換成BufferedReader和PrintWriter的過程便與在服務器中相同(同樣地,兩種情況下都要從一個Socket開始)。在這里,客戶通過發出字串"howdy",并在后面跟隨一個數字,從而初始化通信。注意緩沖區必須再次刷新(這是自動發生的,通過傳遞給PrintWriter構建器的第二個參數)。若緩沖區沒有刷新,那么整個會話(通信)都會被掛起,因為用于初始化的“howdy”永遠不會發送出去(緩沖區不夠滿,不足以造成發送動作的自動進行)。從服務器返回的每一行都會寫入System.out,以驗證一切都在正常運轉。為中止會話,需要發出一個"END"。若客戶程序簡單地掛起,那么服務器會“擲”出一個違例。<br>
大家在這里可以看到我們采用了同樣的措施來確保由Socket代表的網絡資源得到正確的清除,這是用一個try-finally塊實現的。<br>
套接字建立了一個“專用”連接,它會一直持續到明確斷開連接為止(專用連接也可能間接性地斷開,前提是某一端或者中間的某條鏈路出現故障而崩潰)。這意味著參與連接的雙方都被鎖定在通信中,而且無論是否有數據傳遞,連接都會連續處于開放狀態。從表面看,這似乎是一種合理的連網方式。然而,它也為網絡帶來了額外的開銷。本章后面會介紹進行連網的另一種方式。采用那種方式,連接的建立只是暫時的。<br>
<br>
15.3 服務多個客戶<br>
JabberServer可以正常工作,但每次只能為一個客戶程序提供服務。在典型的服務器中,我們希望同時能處理多個客戶的請求。解決這個問題的關鍵就是多線程處理機制。而對于那些本身不支持多線程的語言,達到這個要求無疑是異常困難的。通過第14章的學習,大家已經知道Java已對多線程的處理進行了盡可能的簡化。由于Java的線程處理方式非常直接,所以讓服務器控制多名客戶并不是件難事。<br>
最基本的方法是在服務器(程序)里創建單個ServerSocket,并調用accept()來等候一個新連接。一旦accept()返回,我們就取得結果獲得的Socket,并用它新建一個線程,令其只為那個特定的客戶服務。然后再調用accept(),等候下一次新的連接請求。<br>
對于下面這段服務器代碼,大家可發現它與JabberServer.java例子非常相似,只是為一個特定的客戶提供服務的所有操作都已移入一個獨立的線程類中:<br>
<br>
837-839頁程序<br>
<br>
每次有新客戶請求建立一個連接時,ServeOneJabber線程都會取得由accept()在main()中生成的Socket對象。然后和往常一樣,它創建一個BufferedReader,并用Socket自動刷新PrintWriter對象。最后,它調用Thread的特殊方法start(),令其進行線程的初始化,然后調用run()。這里采取的操作與前例是一樣的:從套掃字讀入某些東西,然后把它原樣反饋回去,直到遇到一個特殊的"END"結束標志為止。<br>
同樣地,套接字的清除必須進行謹慎的設計。就目前這種情況來說,套接字是在ServeOneJabber外部創建的,所以清除工作可以“共享”。若ServeOneJabber構建器失敗,那么只需向調用者“擲”出一個違例即可,然后由調用者負責線程的清除。但假如構建器成功,那么必須由ServeOneJabber對象負責線程的清除,這是在它的run()里進行的。<br>
請注意MultiJabberServer有多么簡單。和以前一樣,我們創建一個ServerSocket,并調用accept()允許一個新連接的建立。但這一次,accept()的返回值(一個套接字)將傳遞給用于ServeOneJabber的構建器,由它創建一個新線程,并對那個連接進行控制。連接中斷后,線程便可簡單地消失。<br>
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -