-------------------------------------------------
本教程由yyc,spirit整理
-------------------------------------------------
網(wǎng)絡(luò)編程
歷史上的網(wǎng)絡(luò)編程都傾向于困難、復(fù)雜,而且極易出錯。
程序員必須掌握與網(wǎng)絡(luò)有關(guān)的大量細節(jié),有時甚至要對硬件有深刻的認識。一般地,我們需要理解連網(wǎng)協(xié)議中不同的“層”(Layer)。而且對于每個連網(wǎng)庫,一般都包含了數(shù)量眾多的函數(shù),分別涉及信息塊的連接、打包和拆包;這些塊的來回運輸;以及握手等等。這是一項令人痛苦的工作。
但是,連網(wǎng)本身的概念并不是很難。我們想獲得位于其他地方某臺機器上的信息,并把它們移到這兒;或者相反。這與讀寫文件非常相似,只是文件存在于遠程機器上,而且遠程機器有權(quán)決定如何處理我們請求或者發(fā)送的數(shù)據(jù)。
Java最出色的一個地方就是它的“無痛苦連網(wǎng)”概念。有關(guān)連網(wǎng)的基層細節(jié)已被盡可能地提取出去,并隱藏在JVM以及Java的本機安裝系統(tǒng)里進行控制。我們使用的編程模型是一個文件的模型;事實上,網(wǎng)絡(luò)連接(一個“套接字”)已被封裝到系統(tǒng)對象里,所以可象對其他數(shù)據(jù)流那樣采用同樣的方法調(diào)用。除此以外,在我們處理另一個連網(wǎng)問題——同時控制多個網(wǎng)絡(luò)連接——的時候,Java內(nèi)建的多線程機制也是十分方便的。
本章將用一系列易懂的例子解釋Java的連網(wǎng)支持。
15.1 機器的標(biāo)識
當(dāng)然,為了分辨來自別處的一臺機器,以及為了保證自己連接的是希望的那臺機器,必須有一種機制能獨一無二地標(biāo)識出網(wǎng)絡(luò)內(nèi)的每臺機器。早期網(wǎng)絡(luò)只解決了如何在本地網(wǎng)絡(luò)環(huán)境中為機器提供唯一的名字。但Java面向的是整個因特網(wǎng),這要求用一種機制對來自世界各地的機器進行標(biāo)識。為達到這個目的,我們采用了IP(互聯(lián)網(wǎng)地址)的概念。IP以兩種形式存在著:
(1) 大家最熟悉的DNS(域名服務(wù))形式。我自己的域名是bruceeckel.com。所以假定我在自己的域內(nèi)有一臺名為Opus的計算機,它的域名就可以是Opus.bruceeckel.com。這正是大家向其他人發(fā)送電子函件時采用的名字,而且通常集成到一個萬維網(wǎng)(WWW)地址里。
(2) 此外,亦可采用“四點”格式,亦即由點號(.)分隔的四組數(shù)字,比如202.98.32.111。
不管哪種情況,IP地址在內(nèi)部都表達成一個由32個二進制位(bit)構(gòu)成的數(shù)字(注釋①),所以IP地址的每一組數(shù)字都不能超過255。利用由java.net提供的static InetAddress.getByName(),我們可以讓一個特定的Java對象表達上述任何一種形式的數(shù)字。結(jié)果是類型為InetAddress的一個對象,可用它構(gòu)成一個“套接字”(Socket),大家在后面會見到這一點。
①:這意味著最多只能得到40億左右的數(shù)字組合,全世界的人很快就會把它用光。但根據(jù)目前正在研究的新IP編址方案,它將采用128 bit的數(shù)字,這樣得到的唯一性IP地址也許在幾百年的時間里都不會用完。
作為運用InetAddress.getByName()一個簡單的例子,請考慮假設(shè)自己有一家撥號連接因特網(wǎng)服務(wù)提供者(ISP),那么會發(fā)生什么情況。每次撥號連接的時候,都會分配得到一個臨時IP地址。但在連接期間,那個IP地址擁有與因特網(wǎng)上其他IP地址一樣的有效性。如果有人按照你的IP地址連接你的機器,他們就有可能使用在你機器上運行的Web或者FTP服務(wù)器程序。當(dāng)然這有個前提,對方必須準(zhǔn)確地知道你目前分配到的IP。由于每次撥號連接獲得的IP都是隨機的,怎樣才能準(zhǔn)確地掌握你的IP呢?
下面這個程序利用InetAddress.getByName()來產(chǎn)生你的IP地址。為了讓它運行起來,事先必須知道計算機的名字。該程序只在Windows 95中進行了測試,但大家可以依次進入自己的“開始”、“設(shè)置”、“控制面板”、“網(wǎng)絡(luò)”,然后進入“標(biāo)識”卡片。其中,“計算機名稱”就是應(yīng)在命令行輸入的內(nèi)容。
//: WhoAmI.java
// Finds out your network address when you're
// connected to the Internet.
package c15;
import java.net.*;
public class WhoAmI {
public static void main(String[] args)
throws Exception {
if(args.length != 1) {
System.err.println(
"Usage: WhoAmI MachineName");
System.exit(1);
}
InetAddress a =
InetAddress.getByName(args[0]);
System.out.println(a);
}
} ///:~
就我自己的情況來說,機器的名字叫作“Colossus”(來自同名電影,“巨人”的意思。我在這臺機器上有一個很大的硬盤)。所以一旦連通我的ISP,就象下面這樣執(zhí)行程序:
java whoAmI Colossus
得到的結(jié)果象下面這個樣子(當(dāng)然,這個地址可能每次都是不同的):
Colossus/202.98.41.151
假如我把這個地址告訴一位朋友,他就可以立即登錄到我的個人Web服務(wù)器,只需指定目標(biāo)地址http://202.98.41.151即可(當(dāng)然,我此時不能斷線)。有些時候,這是向其他人發(fā)送信息或者在自己的Web站點正式出臺以前進行測試的一種方便手段。
15.1.1 服務(wù)器和客戶機
網(wǎng)絡(luò)最基本的精神就是讓兩臺機器連接到一起,并相互“交談”或者“溝通”。一旦兩臺機器都發(fā)現(xiàn)了對方,就可以展開一次令人愉快的雙向?qū)υ挕5鼈冊鯓硬拍堋鞍l(fā)現(xiàn)”對方呢?這就象在游樂園里那樣:一臺機器不得不停留在一個地方,偵聽其他機器說:“嘿,你在哪里呢?”
“停留在一個地方”的機器叫作“服務(wù)器”(Server);到處“找人”的機器則叫作“客戶機”(Client)或者“客戶”。它們之間的區(qū)別只有在客戶機試圖同服務(wù)器連接的時候才顯得非常明顯。一旦連通,就變成了一種雙向通信,誰來扮演服務(wù)器或者客戶機便顯得不那么重要了。
所以服務(wù)器的主要任務(wù)是偵聽建立連接的請求,這是由我們創(chuàng)建的特定服務(wù)器對象完成的。而客戶機的任務(wù)是試著與一臺服務(wù)器建立連接,這是由我們創(chuàng)建的特定客戶機對象完成的。一旦連接建好,那么無論在服務(wù)器端還是客戶機端,連接只是魔術(shù)般地變成了一個IO數(shù)據(jù)流對象。從這時開始,我們可以象讀寫一個普通的文件那樣對待連接。所以一旦建好連接,我們只需象第10章那樣使用自己熟悉的IO命令即可。這正是Java連網(wǎng)最方便的一個地方。
1. 在沒有網(wǎng)絡(luò)的前提下測試程序
由于多種潛在的原因,我們可能沒有一臺客戶機、服務(wù)器以及一個網(wǎng)絡(luò)來測試自己做好的程序。我們也許是在一個課堂環(huán)境中進行練習(xí),或者寫出的是一個不十分可靠的網(wǎng)絡(luò)應(yīng)用,還能拿到網(wǎng)絡(luò)上去。IP的設(shè)計者注意到了這個問題,并建立了一個特殊的地址——localhost——來滿足非網(wǎng)絡(luò)環(huán)境中的測試要求。在Java中產(chǎn)生這個地址最一般的做法是:
InetAddress addr = InetAddress.getByName(null);
如果向getByName()傳遞一個null(空)值,就默認為使用localhost。我們用InetAddress對特定的機器進行索引,而且必須在進行進一步的操作之前得到這個InetAddress(互聯(lián)網(wǎng)地址)。我們不可以操縱一個InetAddress的內(nèi)容(但可把它打印出來,就象下一個例子要演示的那樣)。創(chuàng)建InetAddress的唯一途徑就是那個類的static(靜態(tài))成員方法getByName()(這是最常用的)、getAllByName()或者getLocalHost()。
為得到本地主機地址,亦可向其直接傳遞字串"localhost":
InetAddress.getByName("localhost");
或者使用它的保留IP地址(四點形式),就象下面這樣:
InetAddress.getByName("127.0.0.1");
這三種方法得到的結(jié)果是一樣的。
15.1.2 端口:機器內(nèi)獨一無二的場所
有些時候,一個IP地址并不足以完整標(biāo)識一個服務(wù)器。這是由于在一臺物理性的機器中,往往運行著多個服務(wù)器(程序)。由IP表達的每臺機器也包含了“端口”(Port)。我們設(shè)置一個客戶機或者服務(wù)器的時候,必須選擇一個無論客戶機還是服務(wù)器都認可連接的端口。就象我們?nèi)グ輹橙藭r,IP地址是他居住的房子,而端口是他在的那個房間。
注意端口并不是機器上一個物理上存在的場所,而是一種軟件抽象(主要是為了表述的方便)。客戶程序知道如何通過機器的IP地址同它連接,但怎樣才能同自己真正需要的那種服務(wù)連接呢(一般每個端口都運行著一種服務(wù),一臺機器可能提供了多種服務(wù),比如HTTP和FTP等等)?端口編號在這里扮演了重要的角色,它是必需的一種二級定址措施。也就是說,我們請求一個特定的端口,便相當(dāng)于請求與那個端口編號關(guān)聯(lián)的服務(wù)。“報時”便是服務(wù)的一個典型例子。通常,每個服務(wù)都同一臺特定服務(wù)器機器上的一個獨一無二的端口編號關(guān)聯(lián)在一起。客戶程序必須事先知道自己要求的那項服務(wù)的運行端口號。
系統(tǒng)服務(wù)保留了使用端口1到端口1024的權(quán)力,所以不應(yīng)讓自己設(shè)計的服務(wù)占用這些以及其他任何已知正在使用的端口。本書的第一個例子將使用端口8080(為追憶我的第一臺機器使用的老式8位Intel 8080芯片,那是一部使用CP/M操作系統(tǒng)的機子)。
15.2 套接字
“套接字”或者“插座”(Socket)也是一種軟件形式的抽象,用于表達兩臺機器間一個連接的“終端”。針對一個特定的連接,每臺機器上都有一個“套接字”,可以想象它們之間有一條虛擬的“線纜”。線纜的每一端都插入一個“套接字”或者“插座”里。當(dāng)然,機器之間的物理性硬件以及電纜連接都是完全未知的。抽象的基本宗旨是讓我們盡可能不必知道那些細節(jié)。
在Java中,我們創(chuàng)建一個套接字,用它建立與其他機器的連接。從套接字得到的結(jié)果是一個InputStream以及OutputStream(若使用恰當(dāng)?shù)霓D(zhuǎn)換器,則分別是Reader和Writer),以便將連接作為一個IO流對象對待。有兩個基于數(shù)據(jù)流的套接字類:ServerSocket,服務(wù)器用它“偵聽”進入的連接;以及Socket,客戶用它初始一次連接。一旦客戶(程序)申請建立一個套接字連接,ServerSocket就會返回(通過accept()方法)一個對應(yīng)的服務(wù)器端套接字,以便進行直接通信。從此時起,我們就得到了真正的“套接字-套接字”連接,可以用同樣的方式對待連接的兩端,因為它們本來就是相同的!此時可以利用getInputStream()以及getOutputStream()從每個套接字產(chǎn)生對應(yīng)的InputStream和OutputStream對象。這些數(shù)據(jù)流必須封裝到緩沖區(qū)內(nèi)。可按第10章介紹的方法對類進行格式化,就象對待其他任何流對象那樣。
對于Java庫的命名機制,ServerSocket(服務(wù)器套接字)的使用無疑是容易產(chǎn)生混淆的又一個例證。大家可能認為ServerSocket最好叫作“ServerConnector”(服務(wù)器連接器),或者其他什么名字,只是不要在其中安插一個“Socket”。也可能以為ServerSocket和Socket都應(yīng)從一些通用的基礎(chǔ)類繼承。事實上,這兩種類確實包含了幾個通用的方法,但還不夠資格把它們賦給一個通用的基礎(chǔ)類。相反,ServerSocket的主要任務(wù)是在那里耐心地等候其他機器同它連接,再返回一個實際的Socket。這正是“ServerSocket”這個命名不恰當(dāng)?shù)牡胤剑驗樗哪繕?biāo)不是真的成為一個Socket,而是在其他人同它連接的時候產(chǎn)生一個Socket對象。
然而,ServerSocket確實會在主機上創(chuàng)建一個物理性的“服務(wù)器”或者偵聽用的套接字。這個套接字會偵聽進入的連接,然后利用accept()方法返回一個“已建立”套接字(本地和遠程端點均已定義)。容易混淆的地方是這兩個套接字(偵聽和已建立)都與相同的服務(wù)器套接字關(guān)聯(lián)在一起。偵聽套接字只能接收新的連接請求,不能接收實際的數(shù)據(jù)包。所以盡管ServerSocket對于編程并無太大的意義,但它確實是“物理性”的。
創(chuàng)建一個ServerSocket時,只需為其賦予一個端口編號。不必把一個IP地址分配它,因為它已經(jīng)在自己代表的那臺機器上了。但在創(chuàng)建一個Socket時,卻必須同時賦予IP地址以及要連接的端口編號(另一方面,從ServerSocket.accept()返回的Socket已經(jīng)包含了所有這些信息)。
15.2.1 一個簡單的服務(wù)器和客戶機程序
這個例子將以最簡單的方式運用套接字對服務(wù)器和客戶機進行操作。服務(wù)器的全部工作就是等候建立一個連接,然后用那個連接產(chǎn)生的Socket創(chuàng)建一個InputStream以及一個OutputStream。在這之后,它從InputStream讀入的所有東西都會反饋給OutputStream,直到接收到行中止(END)為止,最后關(guān)閉連接。
客戶機連接與服務(wù)器的連接,然后創(chuàng)建一個OutputStream。文本行通過OutputStream發(fā)送。客戶機也會創(chuàng)建一個InputStream,用它收聽服務(wù)器說些什么(本例只不過是反饋回來的同樣的字句)。
服務(wù)器與客戶機(程序)都使用同樣的端口號,而且客戶機利用本地主機地址連接位于同一臺機器中的服務(wù)器(程序),所以不必在一個物理性的網(wǎng)絡(luò)里完成測試(在某些配置環(huán)境中,可能需要同真正的網(wǎng)絡(luò)建立連接,否則程序不能工作——盡管實際并不通過那個網(wǎng)絡(luò)通信)。
下面是服務(wù)器程序:
//: JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
import java.io.*;
import java.net.*;
public class JabberServer {
// Choose a port outside of the range 1-1024:
public static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Started: " + s);
try {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
System.out.println(
"Connection accepted: "+ socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
// Always close the two sockets...
} finally {
System.out.println("closing...");
socket.close();
}
} finally {
s.close();
}
}
} ///:~
可以看到,ServerSocket需要的只是一個端口編號,不需要IP地址(因為它就在這臺機器上運行)。調(diào)用accept()時,方法會暫時陷入停頓狀態(tài)(堵塞),直到某個客戶嘗試同它建立連接。換言之,盡管它在那里等候連接,但其他進程仍能正常運行(參考第14章)。建好一個連接以后,accept()就會返回一個Socket對象,它是那個連接的代表。
清除套接字的責(zé)任在這里得到了很藝術(shù)的處理。假如ServerSocket構(gòu)建器失敗,則程序簡單地退出(注意必須保證ServerSocket的構(gòu)建器在失敗之后不會留下任何打開的網(wǎng)絡(luò)套接字)。針對這種情況,main()會“擲”出一個IOException違例,所以不必使用一個try塊。若ServerSocket構(gòu)建器成功執(zhí)行,則其他所有方法調(diào)用都必須到一個try-finally代碼塊里尋求保護,以確保無論塊以什么方式留下,ServerSocket都能正確地關(guān)閉。
同樣的道理也適用于由accept()返回的Socket。若accept()失敗,那么我們必須保證Socket不再存在或者含有任何資源,以便不必清除它們。但假若執(zhí)行成功,則后續(xù)的語句必須進入一個try-finally塊內(nèi),以保障在它們失敗的情況下,Socket仍能得到正確的清除。由于套接字使用了重要的非內(nèi)存資源,所以在這里必須特別謹慎,必須自己動手將它們清除(Java中沒有提供“破壞器”來幫助我們做這件事情)。
無論ServerSocket還是由accept()產(chǎn)生的Socket都打印到System.out里。這意味著它們的toString方法會得到自動調(diào)用。這樣便產(chǎn)生了:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]
大家不久就會看到它們?nèi)绾闻c客戶程序做的事情配合。
程序的下一部分看來似乎僅僅是打開文件,以便讀取和寫入,只是InputStream和OutputStream是從Socket對象創(chuàng)建的。利用兩個“轉(zhuǎn)換器”類InputStreamReader和OutputStreamWriter,InputStream和OutputStream對象已經(jīng)分別轉(zhuǎn)換成為Java 1.1的Reader和Writer對象。也可以直接使用Java1.0的InputStream和OutputStream類,但對輸出來說,使用Writer方式具有明顯的優(yōu)勢。這一優(yōu)勢是通過PrintWriter表現(xiàn)出來的,它有一個過載的構(gòu)建器,能獲取第二個參數(shù)——一個布爾值標(biāo)志,指向是否在每一次println()結(jié)束的時候自動刷新輸出(但不適用于print()語句)。每次寫入了輸出內(nèi)容后(寫進out),它的緩沖區(qū)必須刷新,使信息能正式通過網(wǎng)絡(luò)傳遞出去。對目前這個例子來說,刷新顯得尤為重要,因為客戶和服務(wù)器在采取下一步操作之前都要等待一行文本內(nèi)容的到達。若刷新沒有發(fā)生,那么信息不會進入網(wǎng)絡(luò),除非緩沖區(qū)滿(溢出),這會為本例帶來許多問題。
編寫網(wǎng)絡(luò)應(yīng)用程序時,需要特別注意自動刷新機制的使用。每次刷新緩沖區(qū)時,必須創(chuàng)建和發(fā)出一個數(shù)據(jù)包(數(shù)據(jù)封)。就目前的情況來說,這正是我們所希望的,因為假如包內(nèi)包含了還沒有發(fā)出的文本行,服務(wù)器和客戶機之間的相互“握手”就會停止。換句話說,一行的末尾就是一條消息的末尾。但在其他許多情況下,消息并不是用行分隔的,所以不如不用自動刷新機制,而用內(nèi)建的緩沖區(qū)判決機制來決定何時發(fā)送一個數(shù)據(jù)包。這樣一來,我們可以發(fā)出較大的數(shù)據(jù)包,而且處理進程也能加快。
注意和我們打開的幾乎所有數(shù)據(jù)流一樣,它們都要進行緩沖處理。本章末尾有一個練習(xí),清楚展現(xiàn)了假如我們不對數(shù)據(jù)流進行緩沖,那么會得到什么樣的后果(速度會變慢)。
無限while循環(huán)從BufferedReader in內(nèi)讀取文本行,并將信息寫入System.out,然后寫入PrintWriter.out。注意這可以是任何數(shù)據(jù)流,它們只是在表面上同網(wǎng)絡(luò)連接。
客戶程序發(fā)出包含了"END"的行后,程序會中止循環(huán),并關(guān)閉Socket。
下面是客戶程序的源碼:
//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;
public class JabberClient {
public static void main(String[] args)
throws IOException {
// Passing null to getByName() produces the
// special "Local Loopback" IP address, for
// testing on one machine w/o a network:
InetAddress addr =
InetAddress.getByName(null);
// Alternatively, you can use
// the address or name:
// InetAddress addr =
// InetAddress.getByName("127.0.0.1");
// InetAddress addr =
// InetAddress.getByName("localhost");
System.out.println("addr = " + addr);
Socket socket =
new Socket(addr, JabberServer.PORT);
// Guard everything in a try-finally to make
// sure that the socket is closed:
try {
System.out.println("socket = " + socket);
BufferedReader in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Output is automatically flushed
// by PrintWriter:
PrintWriter out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())),true);
for(int i = 0; i < 10; i ++) {
out.println("howdy " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} finally {
System.out.println("closing...");
socket.close();
}
}
} ///:~
在main()中,大家可看到獲得本地主機IP地址的InetAddress的三種途徑:使用null,使用localhost,或者直接使用保留地址127.0.0.1。當(dāng)然,如果想通過網(wǎng)絡(luò)同一臺遠程主機連接,也可以換用那臺機器的IP地址。打印出InetAddress addr后(通過對toString()方法的自動調(diào)用),結(jié)果如下:
localhost/127.0.0.1
通過向getByName()傳遞一個null,它會默認尋找localhost,并生成特殊的保留地址127.0.0.1。注意在名為socket的套接字創(chuàng)建時,同時使用了InetAddress以及端口號。打印這樣的某個Socket對象時,為了真正理解它的含義,請記住一次獨一無二的因特網(wǎng)連接是用下述四種數(shù)據(jù)標(biāo)識的:clientHost(客戶主機)、clientPortNumber(客戶端口號)、serverHost(服務(wù)主機)以及serverPortNumber(服務(wù)端口號)。服務(wù)程序啟動后,會在本地主機(127.0.0.1)上建立為它分配的端口(8080)。一旦客戶程序發(fā)出請求,機器上下一個可用的端口就會分配給它(這種情況下是1077),這一行動也在與服務(wù)程序相同的機器(127.0.0.1)上進行。現(xiàn)在,為了使數(shù)據(jù)能在客戶及服務(wù)程序之間來回傳送,每一端都需要知道把數(shù)據(jù)發(fā)到哪里。所以在同一個“已知”服務(wù)程序連接的時候,客戶會發(fā)出一個“返回地址”,使服務(wù)器程序知道將自己的數(shù)據(jù)發(fā)到哪兒。我們在服務(wù)器端的示范輸出中可以體會到這一情況:
Socket[addr=127.0.0.1,port=1077,localport=8080]
這意味著服務(wù)器剛才已接受了來自127.0.0.1這臺機器的端口1077的連接,同時監(jiān)聽自己的本地端口(8080)。而在客戶端:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
這意味著客戶已用自己的本地端口1077與127.0.0.1機器上的端口8080建立了 連接。
大家會注意到每次重新啟動客戶程序的時候,本地端口的編號都會增加。這個編號從1025(剛好在系統(tǒng)保留的1-1024之外)開始,并會一直增加下去,除非我們重啟機器。若重新啟動機器,端口號仍然會從1025開始增值(在Unix機器中,一旦超過保留的套按字范圍,數(shù)字就會再次從最小的可用數(shù)字開始)。
創(chuàng)建好Socket對象后,將其轉(zhuǎn)換成BufferedReader和PrintWriter的過程便與在服務(wù)器中相同(同樣地,兩種情況下都要從一個Socket開始)。在這里,客戶通過發(fā)出字串"howdy",并在后面跟隨一個數(shù)字,從而初始化通信。注意緩沖區(qū)必須再次刷新(這是自動發(fā)生的,通過傳遞給PrintWriter構(gòu)建器的第二個參數(shù))。若緩沖區(qū)沒有刷新,那么整個會話(通信)都會被掛起,因為用于初始化的“howdy”永遠不會發(fā)送出去(緩沖區(qū)不夠滿,不足以造成發(fā)送動作的自動進行)。從服務(wù)器返回的每一行都會寫入System.out,以驗證一切都在正常運轉(zhuǎn)。為中止會話,需要發(fā)出一個"END"。若客戶程序簡單地掛起,那么服務(wù)器會“擲”出一個違例。
大家在這里可以看到我們采用了同樣的措施來確保由Socket代表的網(wǎng)絡(luò)資源得到正確的清除,這是用一個try-finally塊實現(xiàn)的。
套接字建立了一個“專用”連接,它會一直持續(xù)到明確斷開連接為止(專用連接也可能間接性地斷開,前提是某一端或者中間的某條鏈路出現(xiàn)故障而崩潰)。這意味著參與連接的雙方都被鎖定在通信中,而且無論是否有數(shù)據(jù)傳遞,連接都會連續(xù)處于開放狀態(tài)。從表面看,這似乎是一種合理的連網(wǎng)方式。然而,它也為網(wǎng)絡(luò)帶來了額外的開銷。本章后面會介紹進行連網(wǎng)的另一種方式。采用那種方式,連接的建立只是暫時的。
15.3 服務(wù)多個客戶
JabberServer可以正常工作,但每次只能為一個客戶程序提供服務(wù)。在典型的服務(wù)器中,我們希望同時能處理多個客戶的請求。解決這個問題的關(guān)鍵就是多線程處理機制。而對于那些本身不支持多線程的語言,達到這個要求無疑是異常困難的。通過第14章的學(xué)習(xí),大家已經(jīng)知道Java已對多線程的處理進行了盡可能的簡化。由于Java的線程處理方式非常直接,所以讓服務(wù)器控制多名客戶并不是件難事。
最基本的方法是在服務(wù)器(程序)里創(chuàng)建單個ServerSocket,并調(diào)用accept()來等候一個新連接。一旦accept()返回,我們就取得結(jié)果獲得的Socket,并用它新建一個線程,令其只為那個特定的客戶服務(wù)。然后再調(diào)用accept(),等候下一次新的連接請求。
對于下面這段服務(wù)器代碼,大家可發(fā)現(xiàn)它與JabberServer.java例子非常相似,只是為一個特定的客戶提供服務(wù)的所有操作都已移入一個獨立的線程類中:
//: MultiJabberServer.java
// A server that uses multithreading to handle
// any number of clients.
import java.io.*;
import java.net.*;
class ServeOneJabber extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public ServeOneJabber(Socket s)
throws IOException {
socket = s;
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())), true);
// If any of the above calls throw an
// exception, the caller is responsible for
// closing the socket. Otherwise the thread
// will close it.
start(); // Calls run()
}
public void run() {
try {
while (true) {
String str = in.readLine();
if (str.equals("END")) break;
System.out.println("Echoing: " + str);
out.println(str);
}
System.out.println("closing...");
} catch (IOException e) {
} finally {
try {
socket.close();
} catch(IOException e) {}
}
}
}
public class MultiJabberServer {
static final int PORT = 8080;
public static void main(String[] args)
throws IOException {
ServerSocket s = new ServerSocket(PORT);
System.out.println("Server Started");
try {
while(true) {
// Blocks until a connection occurs:
Socket socket = s.accept();
try {
new ServeOneJabber(socket);
} catch(IOException e) {
// If it fails, close the socket,
// otherwise the thread will close it:
socket.close();
}
}
} finally {
s.close();
}
}
} ///:~
每次有新客戶請求建立一個連接時,ServeOneJabber線程都會取得由accept()在main()中生成的Socket對象。然后和往常一樣,它創(chuàng)建一個BufferedReader,并用Socket自動刷新PrintWriter對象。最后,它調(diào)用Thread的特殊方法start(),令其進行線程的初始化,然后調(diào)用run()。這里采取的操作與前例是一樣的:從套掃字讀入某些東西,然后把它原樣反饋回去,直到遇到一個特殊的"END"結(jié)束標(biāo)志為止。
同樣地,套接字的清除必須進行謹慎的設(shè)計。就目前這種情況來說,套接字是在ServeOneJabber外部創(chuàng)建的,所以清除工作可以“共享”。若ServeOneJabber構(gòu)建器失敗,那么只需向調(diào)用者“擲”出一個違例即可,然后由調(diào)用者負責(zé)線程的清除。但假如構(gòu)建器成功,那么必須由ServeOneJabber對象負責(zé)線程的清除,這是在它的run()里進行的。
請注意MultiJabberServer有多么簡單。和以前一樣,我們創(chuàng)建一個ServerSocket,并調(diào)用accept()允許一個新連接的建立。但這一次,accept()的返回值(一個套接字)將傳遞給用于ServeOneJabber的構(gòu)建器,由它創(chuàng)建一個新線程,并對那個連接進行控制。連接中斷后,線程便可簡單地消失。
如果ServerSocket創(chuàng)建失敗,則再一次通過main()擲出違例。如果成功,則位于外層的try-finally代碼塊可以擔(dān)保正確的清除。位于內(nèi)層的try-catch塊只負責(zé)防范ServeOneJabber構(gòu)建器的失敗;若構(gòu)建器成功,則ServeOneJabber線程會將對應(yīng)的套接字關(guān)掉。
為了證實服務(wù)器代碼確實能為多名客戶提供服務(wù),下面這個程序?qū)?chuàng)建許多客戶(使用線程),并同相同的服務(wù)器建立連接。每個線程的“存在時間”都是有限的。一旦到期,就留出空間以便創(chuàng)建一個新線程。允許創(chuàng)建的線程的最大數(shù)量是由final int maxthreads決定的。大家會注意到這個值非常關(guān)鍵,因為假如把它設(shè)得很大,線程便有可能耗盡資源,并產(chǎn)生不可預(yù)知的程序錯誤。
//: MultiJabberClient.java
// Client that tests the MultiJabberServer
// by starting up multiple clients.
import java.net.*;
import java.io.*;
class JabberClientThread extends Thread {
private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0;
private int id = counter++;
private static int threadcount = 0;
public static int threadCount() {
return threadcount;
}
public JabberClientThread(InetAddress addr) {
System.out.println("Making client " + id);
threadcount++;
try {
socket =
new Socket(addr, MultiJabberServer.PORT);
} catch(IOException e) {
// If the creation of the socket fails,
// nothing needs to be cleaned up.
}
try {
in =
new BufferedReader(
new InputStreamReader(
socket.getInputStream()));
// Enable auto-flush:
out =
new PrintWriter(
new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream())), true);
start();
} catch(IOException e) {
// The socket should be closed on any
// failures other than the socket
// constructor:
try {
socket.close();
} catch(IOException e2) {}
}
// Otherwise the socket will be closed by
// the run() method of the thread.
}
public void run() {
try {
for(int i = 0; i < 25; i++) {
out.println("Client " + id + ": " + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
} catch(IOException e) {
} finally {
// Always close it:
try {
socket.close();
} catch(IOException e) {}
threadcount--; // Ending this thread
}
}
}
public class MultiJabberClient {
static final int MAX_THREADS = 40;
public static void main(String[] args)
throws IOException, InterruptedException {
InetAddress addr =
InetAddress.getByName(null);
while(true) {
if(JabberClientThread.threadCount()
< MAX_THREADS)
new JabberClientThread(addr);
Thread.currentThread().sleep(100);
}
}
} ///:~
JabberClientThread構(gòu)建器獲取一個InetAddress,并用它打開一個套接字。大家可能已看出了這樣的一個套路:Socket肯定用于創(chuàng)建某種Reader以及/或者Writer(或者InputStream和/或OutputStream)對象,這是運用Socket的唯一方式(當(dāng)然,我們可考慮編寫一、兩個類,令其自動完成這些操作,避免大量重復(fù)的代碼編寫工作)。同樣地,start()執(zhí)行線程的初始化,并調(diào)用run()。在這里,消息發(fā)送給服務(wù)器,而來自服務(wù)器的信息則在屏幕上回顯出來。然而,線程的“存在時間”是有限的,最終都會結(jié)束。注意在套接字創(chuàng)建好以后,但在構(gòu)建器完成之前,假若構(gòu)建器失敗,套接字會被清除。否則,為套接字調(diào)用close()的責(zé)任便落到了run()方法的頭上。
threadcount跟蹤計算目前存在的JabberClientThread對象的數(shù)量。它將作為構(gòu)建器的一部分增值,并在run()退出時減值(run()退出意味著線程中止)。在MultiJabberClient.main()中,大家可以看到線程的數(shù)量會得到檢查。若數(shù)量太多,則多余的暫時不創(chuàng)建。方法隨后進入“休眠”狀態(tài)。這樣一來,一旦部分線程最后被中止,多作的那些線程就可以創(chuàng)建了。大家可試驗一下逐漸增大MAX_THREADS,看看對于你使用的系統(tǒng)來說,建立多少線程(連接)才會使您的系統(tǒng)資源降低到危險程度。
15.4 數(shù)據(jù)報
大家迄今看到的例子使用的都是“傳輸控制協(xié)議”(TCP),亦稱作“基于數(shù)據(jù)流的套接字”。根據(jù)該協(xié)議的設(shè)計宗旨,它具有高度的可靠性,而且能保證數(shù)據(jù)順利抵達目的地。換言之,它允許重傳那些由于各種原因半路“走失”的數(shù)據(jù)。而且收到字節(jié)的順序與它們發(fā)出來時是一樣的。當(dāng)然,這種控制與可靠性需要我們付出一些代價:TCP具有非常高的開銷。
還有另一種協(xié)議,名為“用戶數(shù)據(jù)報協(xié)議”(UDP),它并不刻意追求數(shù)據(jù)包會完全發(fā)送出去,也不能擔(dān)保它們抵達的順序與它們發(fā)出時一樣。我們認為這是一種“不可靠協(xié)議”(TCP當(dāng)然是“可靠協(xié)議”)。聽起來似乎很糟,但由于它的速度快得多,所以經(jīng)常還是有用武之地的。對某些應(yīng)用來說,比如聲音信號的傳輸,如果少量數(shù)據(jù)包在半路上丟失了,那么用不著太在意,因為傳輸?shù)乃俣蕊@得更重要一些。大多數(shù)互聯(lián)網(wǎng)游戲,如Diablo,采用的也是UDP協(xié)議通信,因為網(wǎng)絡(luò)通信的快慢是游戲是否流暢的決定性因素。也可以想想一臺報時服務(wù)器,如果某條消息丟失了,那么也真的不必過份緊張。另外,有些應(yīng)用也許能向服務(wù)器傳回一條UDP消息,以便以后能夠恢復(fù)。如果在適當(dāng)?shù)臅r間里沒有響應(yīng),消息就會丟失。
Java對數(shù)據(jù)報的支持與它對TCP套接字的支持大致相同,但也存在一個明顯的區(qū)別。對數(shù)據(jù)報來說,我們在客戶和服務(wù)器程序都可以放置一個DatagramSocket(數(shù)據(jù)報套接字),但與ServerSocket不同,前者不會干巴巴地等待建立一個連接的請求。這是由于不再存在“連接”,取而代之的是一個數(shù)據(jù)報陳列出來。另一項本質(zhì)的區(qū)別的是對TCP套接字來說,一旦我們建好了連接,便不再需要關(guān)心誰向誰“說話”——只需通過會話流來回傳送數(shù)據(jù)即可。但對數(shù)據(jù)報來說,它的數(shù)據(jù)包必須知道自己來自何處,以及打算去哪里。這意味著我們必須知道每個數(shù)據(jù)報包的這些信息,否則信息就不能正常地傳遞。
DatagramSocket用于收發(fā)數(shù)據(jù)包,而DatagramPacket包含了具體的信息。準(zhǔn)備接收一個數(shù)據(jù)報時,只需提供一個緩沖區(qū),以便安置接收到的數(shù)據(jù)。數(shù)據(jù)包抵達時,通過DatagramSocket,作為信息起源地的因特網(wǎng)地址以及端口編號會自動得到初化。所以一個用于接收數(shù)據(jù)報的DatagramPacket構(gòu)建器是:
DatagramPacket(buf, buf.length)
其中,buf是一個字節(jié)數(shù)組。既然buf是個數(shù)組,大家可能會奇怪為什么構(gòu)建器自己不能調(diào)查出數(shù)組的長度呢?實際上我也有同感,唯一能猜到的原因就是C風(fēng)格的編程使然,那里的數(shù)組不能自己告訴我們它有多大。
可以重復(fù)使用數(shù)據(jù)報的接收代碼,不必每次都建一個新的。每次用它的時候(再生),緩沖區(qū)內(nèi)的數(shù)據(jù)都會被覆蓋。
緩沖區(qū)的最大容量僅受限于允許的數(shù)據(jù)報包大小,這個限制位于比64KB稍小的地方。但在許多應(yīng)用程序中,我們都寧愿它變得還要小一些,特別是在發(fā)送數(shù)據(jù)的時候。具體選擇的數(shù)據(jù)包大小取決于應(yīng)用程序的特定要求。
發(fā)出一個數(shù)據(jù)報時,DatagramPacket不僅需要包含正式的數(shù)據(jù),也要包含因特網(wǎng)地址以及端口號,以決定它的目的地。所以用于輸出DatagramPacket的構(gòu)建器是:
DatagramPacket(buf, length, inetAddress, port)
這一次,buf(一個字節(jié)數(shù)組)已經(jīng)包含了我們想發(fā)出的數(shù)據(jù)。length可以是buf的長度,但也可以更短一些,意味著我們只想發(fā)出那么多的字節(jié)。另兩個參數(shù)分別代表數(shù)據(jù)包要到達的因特網(wǎng)地址以及目標(biāo)機器的一個目標(biāo)端口(注釋②)。
②:我們認為TCP和UDP端口是相互獨立的。也就是說,可以在端口8080同時運行一個TCP和UDP服務(wù)程序,兩者之間不會產(chǎn)生沖突。
大家也許認為兩個構(gòu)建器創(chuàng)建了兩個不同的對象:一個用于接收數(shù)據(jù)報,另一個用于發(fā)送它們。如果是好的面向?qū)ο蟮脑O(shè)計方案,會建議把它們創(chuàng)建成兩個不同的類,而不是具有不同的行為的一個類(具體行為取決于我們?nèi)绾螛?gòu)建對象)。這也許會成為一個嚴重的問題,但幸運的是,DatagramPacket的使用相當(dāng)簡單,我們不需要在這個問題上糾纏不清。這一點在下例里將有很明確的說明。該例類似于前面針對TCP套接字的MultiJabberServer和MultiJabberClient例子。多個客戶都會將數(shù)據(jù)報發(fā)給服務(wù)器,后者會將其反饋回最初發(fā)出消息的同樣的客戶。
為簡化從一個String里創(chuàng)建DatagramPacket的工作(或者從DatagramPacket里創(chuàng)建String),這個例子首先用到了一個工具類,名為Dgram:
//: Dgram.java
// A utility class to convert back and forth
// Between Strings and DataGramPackets.
import java.net.*;
public class Dgram {
public static DatagramPacket toDatagram(
String s, InetAddress destIA, int destPort) {
// Deprecated in Java 1.1, but it works:
byte[] buf = new byte[s.length() + 1];
s.getBytes(0, s.length(), buf, 0);
// The correct Java 1.1 approach, but it's
// Broken (it truncates the String):
// byte[] buf = s.getBytes();
return new DatagramPacket(buf, buf.length,
destIA, destPort);
}
public static String toString(DatagramPacket p){
// The Java 1.0 approach:
// return new String(p.getData(),
// 0, 0, p.getLength());
// The Java 1.1 approach:
return
new String(p.getData(), 0, p.getLength());
}
} ///:~
Dgram的第一個方法采用一個String、一個InetAddress以及一個端口號作為自己的參數(shù),將String的內(nèi)容復(fù)制到一個字節(jié)緩沖區(qū),再將緩沖區(qū)傳遞進入DatagramPacket構(gòu)建器,從而構(gòu)建一個DatagramPacket。注意緩沖區(qū)分配時的"+1"——這對防止截尾現(xiàn)象是非常重要的。String的getByte()方法屬于一種特殊操作,能將一個字串包含的char復(fù)制進入一個字節(jié)緩沖。該方法現(xiàn)在已被“反對”使用;Java 1.1有一個“更好”的辦法來做這個工作,但在這里卻被當(dāng)作注釋屏蔽掉了,因為它會截掉String的部分內(nèi)容。所以盡管我們在Java 1.1下編譯該程序時會得到一條“反對”消息,但它的行為仍然是正確無誤的(這個錯誤應(yīng)該在你讀到這里的時候修正了)。
Dgram.toString()方法同時展示了Java 1.0的方法和Java 1.1的方法(兩者是不同的,因為有一種新類型的String構(gòu)建器)。
下面是用于數(shù)據(jù)報演示的服務(wù)器代碼:
//: ChatterServer.java
// A server that echoes datagrams
import java.net.*;
import java.io.*;
import java.util.*;
public class ChatterServer {
static final int INPORT = 1711;
private byte[] buf = new byte[1000];
private DatagramPacket dp =
new DatagramPacket(buf, buf.length);
// Can listen & send on the same socket:
private DatagramSocket socket;
public ChatterServer() {
try {
socket = new DatagramSocket(INPORT);
System.out.println("Server started");
while(true) {
// Block until a datagram appears:
socket.receive(dp);
String rcvd = Dgram.toString(dp) +
", from address: " + dp.getAddress() +
", port: " + dp.getPort();
System.out.println(rcvd);
String echoString =
"Echoed: " + rcvd;
// Extract the address and port from the
// received datagram to find out where to
// send it back:
DatagramPacket echo =
Dgram.toDatagram(echoString,
dp.getAddress(), dp.getPort());
socket.send(echo);
}
} catch(SocketException e) {
System.err.println("Can't open socket");
System.exit(1);
} catch(IOException e) {
System.err.println("Communication error");
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatterServer();
}
} ///:~
ChatterServer創(chuàng)建了一個用來接收消息的DatagramSocket(數(shù)據(jù)報套接字),而不是在我們每次準(zhǔn)備接收一條新消息時都新建一個。這個單一的DatagramSocket可以重復(fù)使用。它有一個端口號,因為這屬于服務(wù)器,客戶必須確切知道自己把數(shù)據(jù)報發(fā)到哪個地址。盡管有一個端口號,但沒有為它分配因特網(wǎng)地址,因為它就駐留在“這”臺機器內(nèi),所以知道自己的因特網(wǎng)地址是什么(目前是默認的localhost)。在無限while循環(huán)中,套接字被告知接收數(shù)據(jù)(receive())。然后暫時掛起,直到一個數(shù)據(jù)報出現(xiàn),再把它反饋回我們希望的接收人——DatagramPacket dp——里面。數(shù)據(jù)包(Packet)會被轉(zhuǎn)換成一個字串,同時插入的還有數(shù)據(jù)包的起源因特網(wǎng)地址及套接字。這些信息會顯示出來,然后添加一個額外的字串,指出自己已從服務(wù)器反饋回來了。
大家可能會覺得有點兒迷惑。正如大家會看到的那樣,許多不同的因特網(wǎng)地址和端口號都可能是消息的起源地——換言之,客戶程序可能駐留在任何一臺機器里(就這一次演示來說,它們都駐留在localhost里,但每個客戶使用的端口編號是不同的)。為了將一條消息送回它真正的始發(fā)客戶,需要知道那個客戶的因特網(wǎng)地址以及端口號。幸運的是,所有這些資料均已非常周到地封裝到發(fā)出消息的DatagramPacket內(nèi)部,所以我們要做的全部事情就是用getAddress()和getPort()把它們?nèi)〕鰜怼@眠@些資料,可以構(gòu)建DatagramPacket echo——它通過與接收用的相同的套接字發(fā)送回來。除此以外,一旦套接字發(fā)出數(shù)據(jù)報,就會添加“這”臺機器的因特網(wǎng)地址及端口信息,所以當(dāng)客戶接收消息時,它可以利用getAddress()和getPort()了解數(shù)據(jù)報來自何處。事實上,getAddress()和getPort()唯一不能告訴我們數(shù)據(jù)報來自何處的前提是:我們創(chuàng)建一個待發(fā)送的數(shù)據(jù)報,并在正式發(fā)出之前調(diào)用了getAddress()和getPort()。到數(shù)據(jù)報正式發(fā)送的時候,這臺機器的地址以及端口才會寫入數(shù)據(jù)報。所以我們得到了運用數(shù)據(jù)報時一項重要的原則:不必跟蹤一條消息的來源地!因為它肯定保存在數(shù)據(jù)報里。事實上,對程序來說,最可靠的做法是我們不要試圖跟蹤,而是無論如何都從目標(biāo)數(shù)據(jù)報里提取出地址以及端口信息(就象這里做的那樣)。
為測試服務(wù)器的運轉(zhuǎn)是否正常,下面這程序?qū)?chuàng)建大量客戶(線程),它們都會將數(shù)據(jù)報包發(fā)給服務(wù)器,并等候服務(wù)器把它們原樣反饋回來。
//: ChatterServer.java
// A server that echoes datagrams
import java.net.*;
import java.io.*;
import java.util.*;
public class ChatterServer {
static final int INPORT = 1711;
private byte[] buf = new byte[1000];
private DatagramPacket dp =
new DatagramPacket(buf, buf.length);
// Can listen & send on the same socket:
private DatagramSocket socket;
public ChatterServer() {
try {
socket = new DatagramSocket(INPORT);
System.out.println("Server started");
while(true) {
// Block until a datagram appears:
socket.receive(dp);
String rcvd = Dgram.toString(dp) +
", from address: " + dp.getAddress() +
", port: " + dp.getPort();
System.out.println(rcvd);
String echoString =
"Echoed: " + rcvd;
// Extract the address and port from the
// received datagram to find out where to
// send it back:
DatagramPacket echo =
Dgram.toDatagram(echoString,
dp.getAddress(), dp.getPort());
socket.send(echo);
}
} catch(SocketException e) {
System.err.println("Can't open socket");
System.exit(1);
} catch(IOException e) {
System.err.println("Communication error");
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatterServer();
}
} ///:~
ChatterClient被創(chuàng)建成一個線程(Thread),所以可以用多個客戶來“騷擾”服務(wù)器。從中可以看到,用于接收的DatagramPacket和用于ChatterServer的那個是相似的。在構(gòu)建器中,創(chuàng)建DatagramPacket時沒有附帶任何參數(shù)(自變量),因為它不需要明確指出自己位于哪個特定編號的端口里。用于這個套接字的因特網(wǎng)地址將成為“這臺機器”(比如localhost),而且會自動分配端口編號,這從輸出結(jié)果即可看出。同用于服務(wù)器的那個一樣,這個DatagramPacket將同時用于發(fā)送和接收。
hostAddress是我們想與之通信的那臺機器的因特網(wǎng)地址。在程序中,如果需要創(chuàng)建一個準(zhǔn)備傳出去的DatagramPacket,那么必須知道一個準(zhǔn)確的因特網(wǎng)地址和端口號。可以肯定的是,主機必須位于一個已知的地址和端口號上,使客戶能啟動與主機的“會話”。
每個線程都有自己獨一無二的標(biāo)識號(盡管自動分配給線程的端口號是也會提供一個唯一的標(biāo)識符)。在run()中,我們創(chuàng)建了一個String消息,其中包含了線程的標(biāo)識編號以及該線程準(zhǔn)備發(fā)送的消息編號。我們用這個字串創(chuàng)建一個數(shù)據(jù)報,發(fā)到主機上的指定地址;端口編號則直接從ChatterServer內(nèi)的一個常數(shù)取得。一旦消息發(fā)出,receive()就會暫時被“堵塞”起來,直到服務(wù)器回復(fù)了這條消息。與消息附在一起的所有信息使我們知道回到這個特定線程的東西正是從始發(fā)消息中投遞出去的。在這個例子中,盡管是一種“不可靠”協(xié)議,但仍然能夠檢查數(shù)據(jù)報是否到去過了它們該去的地方(這在localhost和LAN環(huán)境中是成立的,但在非本地連接中卻可能出現(xiàn)一些錯誤)。
運行該程序時,大家會發(fā)現(xiàn)每個線程都會結(jié)束。這意味著發(fā)送到服務(wù)器的每個數(shù)據(jù)報包都會回轉(zhuǎn),并反饋回正確的接收者。如果不是這樣,一個或更多的線程就會掛起并進入“堵塞”狀態(tài),直到它們的輸入被顯露出來。
大家或許認為將文件從一臺機器傳到另一臺的唯一正確方式是通過TCP套接字,因為它們是“可靠”的。然而,由于數(shù)據(jù)報的速度非常快,所以它才是一種更好的選擇。我們只需將文件分割成多個數(shù)據(jù)報,并為每個包編號。接收機器會取得這些數(shù)據(jù)包,并重新“組裝”它們;一個“標(biāo)題包”會告訴機器應(yīng)該接收多少個包,以及組裝所需的另一些重要信息。如果一個包在半路“走丟”了,接收機器會返回一個數(shù)據(jù)報,告訴發(fā)送者重傳。
15.5 一個Web應(yīng)用
現(xiàn)在讓我們想想如何創(chuàng)建一個應(yīng)用,令其在真實的Web環(huán)境中運行,它將把Java的優(yōu)勢表現(xiàn)得淋漓盡致。這個應(yīng)用的一部分是在Web服務(wù)器上運行的一個Java程序,另一部分則是一個“程序片”或“小應(yīng)用程序”(Applet),從服務(wù)器下載至瀏覽器(即“客戶”)。這個程序片從用戶那里收集信息,并將其傳回Web服務(wù)器上運行的應(yīng)用程序。程序的任務(wù)非常簡單:程序片會詢問用戶的E-mail地址,并在驗證這個地址合格后(沒有包含空格,而且有一個@符號),將該E-mail發(fā)送給Web服務(wù)器。服務(wù)器上運行的程序則會捕獲傳回的數(shù)據(jù),檢查一個包含了所有E-mail地址的數(shù)據(jù)文件。如果那個地址已包含在文件里,則向瀏覽器反饋一條消息,說明這一情況。該消息由程序片負責(zé)顯示。若是一個新地址,則將其置入列表,并通知程序片已成功添加了電子函件地址。
若采用傳統(tǒng)方式來解決這個問題,我們要創(chuàng)建一個包含了文本字段及一個“提交”(Submit)按鈕的HTML頁。用戶可在文本字段里鍵入自己喜歡的任何內(nèi)容,并毫無阻礙地提交給服務(wù)器(在客戶端不進行任何檢查)。提交數(shù)據(jù)的同時,Web頁也會告訴服務(wù)器應(yīng)對數(shù)據(jù)采取什么樣的操作——知會“通用網(wǎng)關(guān)接口”(CGI)程序,收到這些數(shù)據(jù)后立即運行服務(wù)器。這種CGI程序通常是用Perl或C寫的(有時也用C++,但要求服務(wù)器支持),而且必須能控制一切可能出現(xiàn)的情況。它首先會檢查數(shù)據(jù),判斷是否采用了正確的格式。若答案是否定的,則CGI程序必須創(chuàng)建一個HTML頁,對遇到的問題進行描述。這個頁會轉(zhuǎn)交給服務(wù)器,再由服務(wù)器反饋回用戶。用戶看到出錯提示后,必須再試一遍提交,直到通過為止。若數(shù)據(jù)正確,CGI程序會打開數(shù)據(jù)文件,要么把電子函件地址加入文件,要么指出該地址已在數(shù)據(jù)文件里了。無論哪種情況,都必須格式化一個恰當(dāng)?shù)腍TML頁,以便服務(wù)器返回給用戶。
作為Java程序員,上述解決問題的方法顯得非常笨拙。而且很自然地,我們希望一切工作都用Java完成。首先,我們會用一個Java程序片負責(zé)客戶端的數(shù)據(jù)有效性校驗,避免數(shù)據(jù)在服務(wù)器和客戶之間傳來傳去,浪費時間和帶寬,同時減輕服務(wù)器額外構(gòu)建HTML頁的負擔(dān)。然后跳過Perl CGI腳本,換成在服務(wù)器上運行一個Java應(yīng)用。事實上,我們在這兒已完全跳過了Web服務(wù)器,僅僅需要從程序片到服務(wù)器上運行的Java應(yīng)用之間建立一個連接即可。
正如大家不久就會體驗到的那樣,盡管看起來非常簡單,但實際上有一些意想不到的問題使局面顯得稍微有些復(fù)雜。用Java 1.1寫程序片是最理想的,但實際上卻經(jīng)常行不通。到本書寫作的時候,擁有Java 1.1能力的瀏覽器仍為數(shù)不多,而且即使這類瀏覽器現(xiàn)在非常流行,仍需考慮照顧一下那些升級緩慢的人。所以從安全的角度看,程序片代碼最好只用Java 1.0編寫。基于這一前提,我們不能用JAR文件來合并(壓縮)程序片中的.class文件。所以,我們應(yīng)盡可能減少.class文件的使用數(shù)量,以縮短下載時間。
好了,再來說說我用的Web服務(wù)器(寫這個示范程序時用的就是它)。它確實支持Java,但僅限于Java 1.0!所以服務(wù)器應(yīng)用也必須用Java 1.0編寫。
15.5.1 服務(wù)器應(yīng)用
現(xiàn)在討論一下服務(wù)器應(yīng)用(程序)的問題,我把它叫作NameCollecor(名字收集器)。假如多名用戶同時嘗試提交他們的E-mail地址,那么會發(fā)生什么情況呢?若NameCollector使用TCP/IP套接字,那么必須運用早先介紹的多線程機制來實現(xiàn)對多個客戶的并發(fā)控制。但所有這些線程都試圖把數(shù)據(jù)寫到同一個文件里,其中保存了所有E-mail地址。這便要求我們設(shè)立一種鎖定機制,保證多個線程不會同時訪問那個文件。一個“信號機”可在這里幫助我們達到目的,但或許還有一種更簡單的方式。
如果我們換用數(shù)據(jù)報,就不必使用多線程了。用單個數(shù)據(jù)報即可“偵聽”進入的所有數(shù)據(jù)報。一旦監(jiān)視到有進入的消息,程序就會進行適當(dāng)?shù)奶幚恚⒋饛?fù)數(shù)據(jù)作為一個數(shù)據(jù)報傳回原先發(fā)出請求的那名接收者。若數(shù)據(jù)報半路上丟失了,則用戶會注意到?jīng)]有答復(fù)數(shù)據(jù)傳回,所以可以重新提交請求。
服務(wù)器應(yīng)用收到一個數(shù)據(jù)報,并對它進行解讀的時候,必須提取出其中的電子函件地址,并檢查本機保存的數(shù)據(jù)文件,看看里面是否已經(jīng)包含了那個地址(如果沒有,則添加之)。所以我們現(xiàn)在遇到了一個新的問題。Java 1.0似乎沒有足夠的能力來方便地處理包含了電子函件地址的文件(Java 1.1則不然)。但是,用C輕易就可以解決這個問題。因此,我們在這兒有機會學(xué)習(xí)將一個非Java程序同Java程序連接的最簡便方式。程序使用的Runtime對象包含了一個名為exec()的方法,它會獨立機器上一個獨立的程序,并返回一個Process(進程)對象。我們可以取得一個OutputStream,它同這個單獨程序的標(biāo)準(zhǔn)輸入連接在一起;并取得一個InputStream,它則同標(biāo)準(zhǔn)輸出連接到一起。要做的全部事情就是用任何語言寫一個程序,只要它能從標(biāo)準(zhǔn)輸入中取得自己的輸入數(shù)據(jù),并將輸出結(jié)果寫入標(biāo)準(zhǔn)輸出即可。如果有些問題不能用Java簡便與快速地解決(或者想利用原有代碼,不想改寫),就可以考慮采用這種方法。亦可使用Java的“固有方法”(Native Method),但那要求更多的技巧,大家可以參考一下附錄A。
1. C程序
這個非Java應(yīng)用是用C寫成,因為Java不適合作CGI編程;起碼啟動的時間不能讓人滿意。它的任務(wù)是管理電子函件(E-mail)地址的一個列表。標(biāo)準(zhǔn)輸入會接受一個E-mail地址,程序會檢查列表中的名字,判斷是否存在那個地址。若不存在,就將其加入,并報告操作成功。但假如名字已在列表里了,就需要指出這一點,避免重復(fù)加入。大家不必擔(dān)心自己不能完全理解下列代碼的含義。它僅僅是一個演示程序,告訴你如何用其他語言寫一個程序,并從Java中調(diào)用它。在這里具體采用何種語言并不重要,只要能夠從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù),并能寫入標(biāo)準(zhǔn)輸出即可。
//: Listmgr.c
// Used by NameCollector.java to manage
// the email list file on the server
#include
#include
#include
#define BSIZE 250
int alreadyInList(FILE* list, char* name) {
char lbuf[BSIZE];
// Go to the beginning of the list:
fseek(list, 0, SEEK_SET);
// Read each line in the list:
while(fgets(lbuf, BSIZE, list)) {
// Strip off the newline:
char * newline = strchr(lbuf, '\n');
if(newline != 0)
*newline = '\0';
if(strcmp(lbuf, name) == 0)
return 1;
}
return 0;
}
int main() {
char buf[BSIZE];
FILE* list = fopen("emlist.txt", "a+t");
if(list == 0) {
perror("could not open emlist.txt");
exit(1);
}
while(1) {
gets(buf); /* From stdin */
if(alreadyInList(list, buf)) {
printf("Already in list: %s", buf);
fflush(stdout);
}
else {
fseek(list, 0, SEEK_END);
fprintf(list, "%s\n", buf);
fflush(list);
printf("%s added to list", buf);
fflush(stdout);
}
}
} ///:~
該程序假設(shè)C編譯器能接受'//'樣式注釋(許多編譯器都能,亦可換用一個C++編譯器來編譯這個程序)。如果你的編譯器不能接受,則簡單地將那些注釋刪掉即可。
文件中的第一個函數(shù)檢查我們作為第二個參數(shù)(指向一個char的指針)傳遞給它的名字是否已在文件中。在這兒,我們將文件作為一個FILE指針傳遞,它指向一個已打開的文件(文件是在main()中打開的)。函數(shù)fseek()在文件中遍歷;我們在這兒用它移至文件開頭。fgets()從文件list中讀入一行內(nèi)容,并將其置入緩沖區(qū)lbuf——不會超過規(guī)定的緩沖區(qū)長度BSIZE。所有這些工作都在一個while循環(huán)中進行,所以文件中的每一行都會讀入。接下來,用strchr()找到新行字符,以便將其刪掉。最后,用strcmp()比較我們傳遞給函數(shù)的名字與文件中的當(dāng)前行。若找到一致的內(nèi)容,strcmp()會返回0。函數(shù)隨后會退出,并返回一個1,指出該名字已經(jīng)在文件里了(注意這個函數(shù)找到相符內(nèi)容后會立即返回,不會把時間浪費在檢查列表剩余內(nèi)容的上面)。如果找遍列表都沒有發(fā)現(xiàn)相符的內(nèi)容,則函數(shù)返回0。
在main()中,我們用fopen()打開文件。第一個參數(shù)是文件名,第二個是打開文件的方式;a+表示“追加”,以及“打開”(或“創(chuàng)建”,假若文件尚不存在),以便到文件的末尾進行更新。fopen()函數(shù)返回的是一個FILE指針;若為0,表示打開操作失敗。此時需要用perror()打印一條出錯提示消息,并用exit()中止程序運行。
如果文件成功打開,程序就會進入一個無限循環(huán)。調(diào)用gets(buf)的函數(shù)會從標(biāo)準(zhǔn)輸入中取出一行(記住標(biāo)準(zhǔn)輸入會與Java程序連接到一起),并將其置入緩沖區(qū)buf中。緩沖區(qū)的內(nèi)容隨后會簡單地傳遞給alreadyInList()函數(shù),如內(nèi)容已在列表中,printf()就會將那條消息發(fā)給標(biāo)準(zhǔn)輸出(Java程序正在監(jiān)視它)。fflush()用于對輸出緩沖區(qū)進行刷新。
如果名字不在列表中,就用fseek()移到列表末尾,并用fprintf()將名字“打印”到列表末尾。隨后,用printf()指出名字已成功加入列表(同樣需要刷新標(biāo)準(zhǔn)輸出),無限循環(huán)返回,繼續(xù)等候一個新名字的進入。
記住一般不能先在自己的計算機上編譯此程序,再把編譯好的內(nèi)容上載到Web服務(wù)器,因為那臺機器使用的可能是不同類的處理器和操作系統(tǒng)。例如,我的Web服務(wù)器安裝的是Intel的CPU,但操作系統(tǒng)是Linux,所以必須先下載源碼,再用遠程命令(通過telnet)指揮Linux自帶的C編譯器,令其在服務(wù)器端編譯好程序。
2. Java程序
這個程序先啟動上述的C程序,再建立必要的連接,以便同它“交談”。隨后,它創(chuàng)建一個數(shù)據(jù)報套接字,用它“監(jiān)視”或者“偵聽”來自程序片的數(shù)據(jù)報包。
//: NameCollector.java
// Extracts email names from datagrams and stores
// them inside a file, using Java 1.02.
import java.net.*;
import java.io.*;
import java.util.*;
public class NameCollector {
final static int COLLECTOR_PORT = 8080;
final static int BUFFER_SIZE = 1000;
byte[] buf = new byte[BUFFER_SIZE];
DatagramPacket dp =
new DatagramPacket(buf, buf.length);
// Can listen & send on the same socket:
DatagramSocket socket;
Process listmgr;
PrintStream nameList;
DataInputStream addResult;
public NameCollector() {
try {
listmgr =
Runtime.getRuntime().exec("listmgr.exe");
nameList = new PrintStream(
new BufferedOutputStream(
listmgr.getOutputStream()));
addResult = new DataInputStream(
new BufferedInputStream(
listmgr.getInputStream()));
} catch(IOException e) {
System.err.println(
"Cannot start listmgr.exe");
System.exit(1);
}
try {
socket =
new DatagramSocket(COLLECTOR_PORT);
System.out.println(
"NameCollector Server started");
while(true) {
// Block until a datagram appears:
socket.receive(dp);
String rcvd = new String(dp.getData(),
0, 0, dp.getLength());
// Send to listmgr.exe standard input:
nameList.println(rcvd.trim());
nameList.flush();
byte[] resultBuf = new byte[BUFFER_SIZE];
int byteCount =
addResult.read(resultBuf);
if(byteCount != -1) {
String result =
new String(resultBuf, 0).trim();
// Extract the address and port from
// the received datagram to find out
// where to send the reply:
InetAddress senderAddress =
dp.getAddress();
int senderPort = dp.getPort();
byte[] echoBuf = new byte[BUFFER_SIZE];
result.getBytes(
0, byteCount, echoBuf, 0);
DatagramPacket echo =
new DatagramPacket(
echoBuf, echoBuf.length,
senderAddress, senderPort);
socket.send(echo);
}
else
System.out.println(
"Unexpected lack of result from " +
"listmgr.exe");
}
} catch(SocketException e) {
System.err.println("Can't open socket");
System.exit(1);
} catch(IOException e) {
System.err.println("Communication error");
e.printStackTrace();
}
}
public static void main(String[] args) {
new NameCollector();
}
} ///:~
NameCollector中的第一個定義應(yīng)該是大家所熟悉的:選定端口,創(chuàng)建一個數(shù)據(jù)報包,然后創(chuàng)建指向一個DatagramSocket的句柄。接下來的三個定義負責(zé)與C程序的連接:一個Process對象是C程序由Java程序啟動之后返回的,而且那個Process對象產(chǎn)生了InputStream和OutputStream,分別代表C程序的標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)輸入。和Java IO一樣,它們理所當(dāng)然地需要“封裝”起來,所以我們最后得到的是一個PrintStream和DataInputStream。
這個程序的所有工作都是在構(gòu)建器內(nèi)進行的。為啟動C程序,需要取得當(dāng)前的Runtime對象。我們用它調(diào)用exec(),再由后者返回Process對象。在Process對象中,大家可看到通過一簡單的調(diào)用即可生成數(shù)據(jù)流:getOutputStream()和getInputStream()。從這個時候開始,我們需要考慮的全部事情就是將數(shù)據(jù)傳給數(shù)據(jù)流nameList,并從addResult中取得結(jié)果。
和往常一樣,我們將DatagramSocket同一個端口連接到一起。在無限while循環(huán)中,程序會調(diào)用receive()——除非一個數(shù)據(jù)報到來,否則receive()會一起處于“堵塞”狀態(tài)。數(shù)據(jù)報出現(xiàn)以后,它的內(nèi)容會提取到String rcvd里。我們首先將該字串兩頭的空格剔除(trim),再將其發(fā)給C程序。如下所示:
nameList.println(rcvd.trim());
之所以能這樣編碼,是因為Java的exec()允許我們訪問任何可執(zhí)行模塊,只要它能從標(biāo)準(zhǔn)輸入中讀,并能向標(biāo)準(zhǔn)輸出中寫。還有另一些方式可與非Java代碼“交談”,這將在附錄A中討論。
從C程序中捕獲結(jié)果就顯得稍微麻煩一些。我們必須調(diào)用read(),并提供一個緩沖區(qū),以便保存結(jié)果。read()的返回值是來自C程序的字節(jié)數(shù)。若這個值為-1,意味著某個地方出現(xiàn)了問題。否則,我們就將resultBuf(結(jié)果緩沖區(qū))轉(zhuǎn)換成一個字串,然后同樣清除多余的空格。隨后,這個字串會象往常一樣進入一個DatagramPacket,并傳回當(dāng)初發(fā)出請求的那個同樣的地址。注意發(fā)送方的地址也是我們接收到的DatagramPacket的一部分。
記住盡管C程序必須在Web服務(wù)器上編譯,但Java程序的編譯場所可以是任意的。這是由于不管使用的是什么硬件平臺和操作系統(tǒng),編譯得到的字節(jié)碼都是一樣的。就就是Java的“跨平臺”兼容能力。
15.5.2 NameSender程序片
正如早先指出的那樣,程序片必須用Java 1.0編寫,使其能與絕大多數(shù)的瀏覽器適應(yīng)。也正是由于這個原因,我們產(chǎn)生的類數(shù)量應(yīng)盡可能地少。所以我們在這兒不考慮使用前面設(shè)計好的Dgram類,而將數(shù)據(jù)報的所有維護工作都轉(zhuǎn)到代碼行中進行。此外,程序片要用一個線程監(jiān)視由服務(wù)器傳回的響應(yīng)信息,而非實現(xiàn)Runnable接口,用集成到程序片的一個獨立線程來做這件事情。當(dāng)然,這樣做對代碼的可讀性不利,但卻能產(chǎn)生一個單類(以及單個服務(wù)器請求)程序片:
//: NameSender.java
// An applet that sends an email address
// as a datagram, using Java 1.02.
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class NameSender extends Applet
implements Runnable {
private Thread pl = null;
private Button send = new Button(
"Add email address to mailing list");
private TextField t = new TextField(
"type your email address here", 40);
private String str = new String();
private Label
l = new Label(), l2 = new Label();
private DatagramSocket s;
private InetAddress hostAddress;
private byte[] buf =
new byte[NameCollector.BUFFER_SIZE];
private DatagramPacket dp =
new DatagramPacket(buf, buf.length);
private int vcount = 0;
public void init() {
setLayout(new BorderLayout());
Panel p = new Panel();
p.setLayout(new GridLayout(2, 1));
p.add(t);
p.add(send);
add("North", p);
Panel labels = new Panel();
labels.setLayout(new GridLayout(2, 1));
labels.add(l);
labels.add(l2);
add("Center", labels);
try {
// Auto-assign port number:
s = new DatagramSocket();
hostAddress = InetAddress.getByName(
getCodeBase().getHost());
} catch(UnknownHostException e) {
l.setText("Cannot find host");
} catch(SocketException e) {
l.setText("Can't open socket");
}
l.setText("Ready to send your email address");
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(send)) {
if(pl != null) {
// pl.stop(); Deprecated in Java 1.2
Thread remove = pl;
pl = null;
remove.interrupt();
}
l2.setText("");
// Check for errors in email name:
str = t.getText().toLowerCase().trim();
if(str.indexOf(' ') != -1) {
l.setText("Spaces not allowed in name");
return true;
}
if(str.indexOf(',') != -1) {
l.setText("Commas not allowed in name");
return true;
}
if(str.indexOf('@') == -1) {
l.setText("Name must include '@'");
l2.setText("");
return true;
}
if(str.indexOf('@') == 0) {
l.setText("Name must preceed '@'");
l2.setText("");
return true;
}
String end =
str.substring(str.indexOf('@'));
if(end.indexOf('.') == -1) {
l.setText("Portion after '@' must " +
"have an extension, such as '.com'");
l2.setText("");
return true;
}
// Everything's OK, so send the name. Get a
// fresh buffer, so it's zeroed. For some
// reason you must use a fixed size rather
// than calculating the size dynamically:
byte[] sbuf =
new byte[NameCollector.BUFFER_SIZE];
str.getBytes(0, str.length(), sbuf, 0);
DatagramPacket toSend =
new DatagramPacket(
sbuf, 100, hostAddress,
NameCollector.COLLECTOR_PORT);
try {
s.send(toSend);
} catch(Exception e) {
l.setText("Couldn't send datagram");
return true;
}
l.setText("Sent: " + str);
send.setLabel("Re-send");
pl = new Thread(this);
pl.start();
l2.setText(
"Waiting for verification " + ++vcount);
}
else return super.action(evt, arg);
return true;
}
// The thread portion of the applet watches for
// the reply to come back from the server:
public void run() {
try {
s.receive(dp);
} catch(Exception e) {
l2.setText("Couldn't receive datagram");
return;
}
l2.setText(new String(dp.getData(),
0, 0, dp.getLength()));
}
} ///:~
程序片的UI(用戶界面)非常簡單。它包含了一個TestField(文本字段),以便我們鍵入一個電子函件地址;以及一個Button(按鈕),用于將地址發(fā)給服務(wù)器。兩個Label(標(biāo)簽)用于向用戶報告狀態(tài)信息。
到現(xiàn)在為止,大家已能判斷出DatagramSocket、InetAddress、緩沖區(qū)以及DatagramPacket都屬于網(wǎng)絡(luò)連接中比較麻煩的部分。最后,大家可看到run()方法實現(xiàn)了線程部分,使程序片能夠“偵聽”由服務(wù)器傳回的響應(yīng)信息。
init()方法用大家熟悉的布局工具設(shè)置GUI,然后創(chuàng)建DatagramSocket,它將同時用于數(shù)據(jù)報的收發(fā)。
action()方法只負責(zé)監(jiān)視我們是否按下了“發(fā)送”(send)按鈕。記住,我們已被限制在Java 1.0上面,所以不能再用較靈活的內(nèi)部類了。按鈕按下以后,采取的第一項行動便是檢查線程pl,看看它是否為null(空)。如果不為null,表明有一個活動線程正在運行。消息首次發(fā)出時,會啟動一個新線程,用它監(jiān)視來自服務(wù)器的回應(yīng)。所以假若有個線程正在運行,就意味著這并非用戶第一次發(fā)送消息。pl句柄被設(shè)為null,同時中止原來的監(jiān)視者(這是最合理的一種做法,因為stop()已被Java 1.2“反對”,這在前一章已解釋過了)。
無論這是否按鈕被第一次按下,I2中的文字都會清除。
下一組語句將檢查E-mail名字是否合格。String.indexOf()方法的作用是搜索其中的非法字符。如果找到一個,就把情況報告給用戶。注意進行所有這些工作時,都不必涉及網(wǎng)絡(luò)通信,所以速度非常快,而且不會影響帶寬和服務(wù)器的性能。
名字校驗通過以后,它會打包到一個數(shù)據(jù)報里,然后采用與前面那個數(shù)據(jù)報示例一樣的方式發(fā)到主機地址和端口編號。第一個標(biāo)簽會發(fā)生變化,指出已成功發(fā)送出去。而且按鈕上的文字也會改變,變成“重發(fā)”(resend)。這時會啟動線程,第二個標(biāo)簽則會告訴我們程序片正在等候來自服務(wù)器的回應(yīng)。
線程的run()方法會利用NameSender中包含的DatagramSocket來接收數(shù)據(jù)(receive()),除非出現(xiàn)來自服務(wù)器的數(shù)據(jù)報包,否則receive()會暫時處于“堵塞”或者“暫停”狀態(tài)。結(jié)果得到的數(shù)據(jù)包會放進NameSender的DatagramPacketdp中。數(shù)據(jù)會從包中提取出來,并置入NameSender的第二個標(biāo)簽。隨后,線程的執(zhí)行將中斷,成為一個“死”線程。若某段時間里沒有收到來自服務(wù)器的回應(yīng),用戶可能變得不耐煩,再次按下按鈕。這樣做會中斷當(dāng)前線程(數(shù)據(jù)發(fā)出以后,會再建一個新的)。由于用一個線程來監(jiān)視回應(yīng)數(shù)據(jù),所以用戶在監(jiān)視期間仍然可以自由使用UI。
1. Web頁
當(dāng)然,程序片必須放到一個Web頁里。下面列出完整的Web頁源碼;稍微研究一下就可看出,我用它從自己開辦的郵寄列表(Mailling List)里自動收集名字。
Add Yourself to Bruce Eckel's Java Mailing List
Add Yourself to Bruce Eckel's Java Mailing List
The applet on this page will automatically add your email address to the mailing list, so you will receive update information about changes to the online version of "Thinking in Java," notification when the book is in print, information about upcoming Java seminars, and notification about the “Hands-on Java Seminar” Multimedia CD. Type in your email address and press the button to automatically add yourself to this mailing list.
If after several tries, you do not get verification it means that the Java application on the server is having problems. In this case, you can add yourself to the list by sending email to
Bruce@EckelObjects.com
程序片標(biāo)記(