<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    空空也

    IT技術(shù)交流平臺(tái)

    常用鏈接

    統(tǒng)計(jì)

    最新評(píng)論

    網(wǎng)絡(luò)編程(Thinking in Java)

    ------------------------------------------------- 本教程由yyc,spirit整理 ------------------------------------------------- 網(wǎng)絡(luò)編程 歷史上的網(wǎng)絡(luò)編程都傾向于困難、復(fù)雜,而且極易出錯(cuò)。 程序員必須掌握與網(wǎng)絡(luò)有關(guān)的大量細(xì)節(jié),有時(shí)甚至要對(duì)硬件有深刻的認(rèn)識(shí)。一般地,我們需要理解連網(wǎng)協(xié)議中不同的“層”(Layer)。而且對(duì)于每個(gè)連網(wǎng)庫(kù),一般都包含了數(shù)量眾多的函數(shù),分別涉及信息塊的連接、打包和拆包;這些塊的來回運(yùn)輸;以及握手等等。這是一項(xiàng)令人痛苦的工作。 但是,連網(wǎng)本身的概念并不是很難。我們想獲得位于其他地方某臺(tái)機(jī)器上的信息,并把它們移到這兒;或者相反。這與讀寫文件非常相似,只是文件存在于遠(yuǎn)程機(jī)器上,而且遠(yuǎn)程機(jī)器有權(quán)決定如何處理我們請(qǐng)求或者發(fā)送的數(shù)據(jù)。 Java最出色的一個(gè)地方就是它的“無(wú)痛苦連網(wǎng)”概念。有關(guān)連網(wǎng)的基層細(xì)節(jié)已被盡可能地提取出去,并隱藏在JVM以及Java的本機(jī)安裝系統(tǒng)里進(jìn)行控制。我們使用的編程模型是一個(gè)文件的模型;事實(shí)上,網(wǎng)絡(luò)連接(一個(gè)“套接字”)已被封裝到系統(tǒng)對(duì)象里,所以可象對(duì)其他數(shù)據(jù)流那樣采用同樣的方法調(diào)用。除此以外,在我們處理另一個(gè)連網(wǎng)問題——同時(shí)控制多個(gè)網(wǎng)絡(luò)連接——的時(shí)候,Java內(nèi)建的多線程機(jī)制也是十分方便的。 本章將用一系列易懂的例子解釋Java的連網(wǎng)支持。 15.1 機(jī)器的標(biāo)識(shí) 當(dāng)然,為了分辨來自別處的一臺(tái)機(jī)器,以及為了保證自己連接的是希望的那臺(tái)機(jī)器,必須有一種機(jī)制能獨(dú)一無(wú)二地標(biāo)識(shí)出網(wǎng)絡(luò)內(nèi)的每臺(tái)機(jī)器。早期網(wǎng)絡(luò)只解決了如何在本地網(wǎng)絡(luò)環(huán)境中為機(jī)器提供唯一的名字。但Java面向的是整個(gè)因特網(wǎng),這要求用一種機(jī)制對(duì)來自世界各地的機(jī)器進(jìn)行標(biāo)識(shí)。為達(dá)到這個(gè)目的,我們采用了IP(互聯(lián)網(wǎng)地址)的概念。IP以兩種形式存在著: (1) 大家最熟悉的DNS(域名服務(wù))形式。我自己的域名是bruceeckel.com。所以假定我在自己的域內(nèi)有一臺(tái)名為Opus的計(jì)算機(jī),它的域名就可以是Opus.bruceeckel.com。這正是大家向其他人發(fā)送電子函件時(shí)采用的名字,而且通常集成到一個(gè)萬(wàn)維網(wǎng)(WWW)地址里。 (2) 此外,亦可采用“四點(diǎn)”格式,亦即由點(diǎn)號(hào)(.)分隔的四組數(shù)字,比如202.98.32.111。 不管哪種情況,IP地址在內(nèi)部都表達(dá)成一個(gè)由32個(gè)二進(jìn)制位(bit)構(gòu)成的數(shù)字(注釋①),所以IP地址的每一組數(shù)字都不能超過255。利用由java.net提供的static InetAddress.getByName(),我們可以讓一個(gè)特定的Java對(duì)象表達(dá)上述任何一種形式的數(shù)字。結(jié)果是類型為InetAddress的一個(gè)對(duì)象,可用它構(gòu)成一個(gè)“套接字”(Socket),大家在后面會(huì)見到這一點(diǎn)。 ①:這意味著最多只能得到40億左右的數(shù)字組合,全世界的人很快就會(huì)把它用光。但根據(jù)目前正在研究的新IP編址方案,它將采用128 bit的數(shù)字,這樣得到的唯一性IP地址也許在幾百年的時(shí)間里都不會(huì)用完。 作為運(yùn)用InetAddress.getByName()一個(gè)簡(jiǎn)單的例子,請(qǐng)考慮假設(shè)自己有一家撥號(hào)連接因特網(wǎng)服務(wù)提供者(ISP),那么會(huì)發(fā)生什么情況。每次撥號(hào)連接的時(shí)候,都會(huì)分配得到一個(gè)臨時(shí)IP地址。但在連接期間,那個(gè)IP地址擁有與因特網(wǎng)上其他IP地址一樣的有效性。如果有人按照你的IP地址連接你的機(jī)器,他們就有可能使用在你機(jī)器上運(yùn)行的Web或者FTP服務(wù)器程序。當(dāng)然這有個(gè)前提,對(duì)方必須準(zhǔn)確地知道你目前分配到的IP。由于每次撥號(hào)連接獲得的IP都是隨機(jī)的,怎樣才能準(zhǔn)確地掌握你的IP呢? 下面這個(gè)程序利用InetAddress.getByName()來產(chǎn)生你的IP地址。為了讓它運(yùn)行起來,事先必須知道計(jì)算機(jī)的名字。該程序只在Windows 95中進(jìn)行了測(cè)試,但大家可以依次進(jìn)入自己的“開始”、“設(shè)置”、“控制面板”、“網(wǎng)絡(luò)”,然后進(jìn)入“標(biāo)識(shí)”卡片。其中,“計(jì)算機(jī)名稱”就是應(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); } } ///:~ 就我自己的情況來說,機(jī)器的名字叫作“Colossus”(來自同名電影,“巨人”的意思。我在這臺(tái)機(jī)器上有一個(gè)很大的硬盤)。所以一旦連通我的ISP,就象下面這樣執(zhí)行程序: java whoAmI Colossus 得到的結(jié)果象下面這個(gè)樣子(當(dāng)然,這個(gè)地址可能每次都是不同的): Colossus/202.98.41.151 假如我把這個(gè)地址告訴一位朋友,他就可以立即登錄到我的個(gè)人Web服務(wù)器,只需指定目標(biāo)地址http://202.98.41.151即可(當(dāng)然,我此時(shí)不能斷線)。有些時(shí)候,這是向其他人發(fā)送信息或者在自己的Web站點(diǎn)正式出臺(tái)以前進(jìn)行測(cè)試的一種方便手段。 15.1.1 服務(wù)器和客戶機(jī) 網(wǎng)絡(luò)最基本的精神就是讓兩臺(tái)機(jī)器連接到一起,并相互“交談”或者“溝通”。一旦兩臺(tái)機(jī)器都發(fā)現(xiàn)了對(duì)方,就可以展開一次令人愉快的雙向?qū)υ挕5鼈冊(cè)鯓硬拍堋鞍l(fā)現(xiàn)”對(duì)方呢?這就象在游樂園里那樣:一臺(tái)機(jī)器不得不停留在一個(gè)地方,偵聽其他機(jī)器說:“嘿,你在哪里呢?” “停留在一個(gè)地方”的機(jī)器叫作“服務(wù)器”(Server);到處“找人”的機(jī)器則叫作“客戶機(jī)”(Client)或者“客戶”。它們之間的區(qū)別只有在客戶機(jī)試圖同服務(wù)器連接的時(shí)候才顯得非常明顯。一旦連通,就變成了一種雙向通信,誰(shuí)來扮演服務(wù)器或者客戶機(jī)便顯得不那么重要了。 所以服務(wù)器的主要任務(wù)是偵聽建立連接的請(qǐng)求,這是由我們創(chuàng)建的特定服務(wù)器對(duì)象完成的。而客戶機(jī)的任務(wù)是試著與一臺(tái)服務(wù)器建立連接,這是由我們創(chuàng)建的特定客戶機(jī)對(duì)象完成的。一旦連接建好,那么無(wú)論在服務(wù)器端還是客戶機(jī)端,連接只是魔術(shù)般地變成了一個(gè)IO數(shù)據(jù)流對(duì)象。從這時(shí)開始,我們可以象讀寫一個(gè)普通的文件那樣對(duì)待連接。所以一旦建好連接,我們只需象第10章那樣使用自己熟悉的IO命令即可。這正是Java連網(wǎng)最方便的一個(gè)地方。 1. 在沒有網(wǎng)絡(luò)的前提下測(cè)試程序 由于多種潛在的原因,我們可能沒有一臺(tái)客戶機(jī)、服務(wù)器以及一個(gè)網(wǎng)絡(luò)來測(cè)試自己做好的程序。我們也許是在一個(gè)課堂環(huán)境中進(jìn)行練習(xí),或者寫出的是一個(gè)不十分可靠的網(wǎng)絡(luò)應(yīng)用,還能拿到網(wǎng)絡(luò)上去。IP的設(shè)計(jì)者注意到了這個(gè)問題,并建立了一個(gè)特殊的地址——localhost——來滿足非網(wǎng)絡(luò)環(huán)境中的測(cè)試要求。在Java中產(chǎn)生這個(gè)地址最一般的做法是: InetAddress addr = InetAddress.getByName(null); 如果向getByName()傳遞一個(gè)null(空)值,就默認(rèn)為使用localhost。我們用InetAddress對(duì)特定的機(jī)器進(jìn)行索引,而且必須在進(jìn)行進(jìn)一步的操作之前得到這個(gè)InetAddress(互聯(lián)網(wǎng)地址)。我們不可以操縱一個(gè)InetAddress的內(nèi)容(但可把它打印出來,就象下一個(gè)例子要演示的那樣)。創(chuàng)建InetAddress的唯一途徑就是那個(gè)類的static(靜態(tài))成員方法getByName()(這是最常用的)、getAllByName()或者getLocalHost()。 為得到本地主機(jī)地址,亦可向其直接傳遞字串"localhost": InetAddress.getByName("localhost"); 或者使用它的保留IP地址(四點(diǎn)形式),就象下面這樣: InetAddress.getByName("127.0.0.1"); 這三種方法得到的結(jié)果是一樣的。 15.1.2 端口:機(jī)器內(nèi)獨(dú)一無(wú)二的場(chǎng)所 有些時(shí)候,一個(gè)IP地址并不足以完整標(biāo)識(shí)一個(gè)服務(wù)器。這是由于在一臺(tái)物理性的機(jī)器中,往往運(yùn)行著多個(gè)服務(wù)器(程序)。由IP表達(dá)的每臺(tái)機(jī)器也包含了“端口”(Port)。我們?cè)O(shè)置一個(gè)客戶機(jī)或者服務(wù)器的時(shí)候,必須選擇一個(gè)無(wú)論客戶機(jī)還是服務(wù)器都認(rèn)可連接的端口。就象我們?nèi)グ輹?huì)某人時(shí),IP地址是他居住的房子,而端口是他在的那個(gè)房間。 注意端口并不是機(jī)器上一個(gè)物理上存在的場(chǎng)所,而是一種軟件抽象(主要是為了表述的方便)。客戶程序知道如何通過機(jī)器的IP地址同它連接,但怎樣才能同自己真正需要的那種服務(wù)連接呢(一般每個(gè)端口都運(yùn)行著一種服務(wù),一臺(tái)機(jī)器可能提供了多種服務(wù),比如HTTP和FTP等等)?端口編號(hào)在這里扮演了重要的角色,它是必需的一種二級(jí)定址措施。也就是說,我們請(qǐng)求一個(gè)特定的端口,便相當(dāng)于請(qǐng)求與那個(gè)端口編號(hào)關(guān)聯(lián)的服務(wù)。“報(bào)時(shí)”便是服務(wù)的一個(gè)典型例子。通常,每個(gè)服務(wù)都同一臺(tái)特定服務(wù)器機(jī)器上的一個(gè)獨(dú)一無(wú)二的端口編號(hào)關(guān)聯(lián)在一起。客戶程序必須事先知道自己要求的那項(xiàng)服務(wù)的運(yùn)行端口號(hào)。 系統(tǒng)服務(wù)保留了使用端口1到端口1024的權(quán)力,所以不應(yīng)讓自己設(shè)計(jì)的服務(wù)占用這些以及其他任何已知正在使用的端口。本書的第一個(gè)例子將使用端口8080(為追憶我的第一臺(tái)機(jī)器使用的老式8位Intel 8080芯片,那是一部使用CP/M操作系統(tǒng)的機(jī)子)。 15.2 套接字 “套接字”或者“插座”(Socket)也是一種軟件形式的抽象,用于表達(dá)兩臺(tái)機(jī)器間一個(gè)連接的“終端”。針對(duì)一個(gè)特定的連接,每臺(tái)機(jī)器上都有一個(gè)“套接字”,可以想象它們之間有一條虛擬的“線纜”。線纜的每一端都插入一個(gè)“套接字”或者“插座”里。當(dāng)然,機(jī)器之間的物理性硬件以及電纜連接都是完全未知的。抽象的基本宗旨是讓我們盡可能不必知道那些細(xì)節(jié)。 在Java中,我們創(chuàng)建一個(gè)套接字,用它建立與其他機(jī)器的連接。從套接字得到的結(jié)果是一個(gè)InputStream以及OutputStream(若使用恰當(dāng)?shù)霓D(zhuǎn)換器,則分別是Reader和Writer),以便將連接作為一個(gè)IO流對(duì)象對(duì)待。有兩個(gè)基于數(shù)據(jù)流的套接字類:ServerSocket,服務(wù)器用它“偵聽”進(jìn)入的連接;以及Socket,客戶用它初始一次連接。一旦客戶(程序)申請(qǐng)建立一個(gè)套接字連接,ServerSocket就會(huì)返回(通過accept()方法)一個(gè)對(duì)應(yīng)的服務(wù)器端套接字,以便進(jìn)行直接通信。從此時(shí)起,我們就得到了真正的“套接字-套接字”連接,可以用同樣的方式對(duì)待連接的兩端,因?yàn)樗鼈儽緛砭褪窍嗤模〈藭r(shí)可以利用getInputStream()以及getOutputStream()從每個(gè)套接字產(chǎn)生對(duì)應(yīng)的InputStream和OutputStream對(duì)象。這些數(shù)據(jù)流必須封裝到緩沖區(qū)內(nèi)。可按第10章介紹的方法對(duì)類進(jìn)行格式化,就象對(duì)待其他任何流對(duì)象那樣。 對(duì)于Java庫(kù)的命名機(jī)制,ServerSocket(服務(wù)器套接字)的使用無(wú)疑是容易產(chǎn)生混淆的又一個(gè)例證。大家可能認(rèn)為ServerSocket最好叫作“ServerConnector”(服務(wù)器連接器),或者其他什么名字,只是不要在其中安插一個(gè)“Socket”。也可能以為ServerSocket和Socket都應(yīng)從一些通用的基礎(chǔ)類繼承。事實(shí)上,這兩種類確實(shí)包含了幾個(gè)通用的方法,但還不夠資格把它們賦給一個(gè)通用的基礎(chǔ)類。相反,ServerSocket的主要任務(wù)是在那里耐心地等候其他機(jī)器同它連接,再返回一個(gè)實(shí)際的Socket。這正是“ServerSocket”這個(gè)命名不恰當(dāng)?shù)牡胤剑驗(yàn)樗哪繕?biāo)不是真的成為一個(gè)Socket,而是在其他人同它連接的時(shí)候產(chǎn)生一個(gè)Socket對(duì)象。 然而,ServerSocket確實(shí)會(huì)在主機(jī)上創(chuàng)建一個(gè)物理性的“服務(wù)器”或者偵聽用的套接字。這個(gè)套接字會(huì)偵聽進(jìn)入的連接,然后利用accept()方法返回一個(gè)“已建立”套接字(本地和遠(yuǎn)程端點(diǎn)均已定義)。容易混淆的地方是這兩個(gè)套接字(偵聽和已建立)都與相同的服務(wù)器套接字關(guān)聯(lián)在一起。偵聽套接字只能接收新的連接請(qǐng)求,不能接收實(shí)際的數(shù)據(jù)包。所以盡管ServerSocket對(duì)于編程并無(wú)太大的意義,但它確實(shí)是“物理性”的。 創(chuàng)建一個(gè)ServerSocket時(shí),只需為其賦予一個(gè)端口編號(hào)。不必把一個(gè)IP地址分配它,因?yàn)樗呀?jīng)在自己代表的那臺(tái)機(jī)器上了。但在創(chuàng)建一個(gè)Socket時(shí),卻必須同時(shí)賦予IP地址以及要連接的端口編號(hào)(另一方面,從ServerSocket.accept()返回的Socket已經(jīng)包含了所有這些信息)。 15.2.1 一個(gè)簡(jiǎn)單的服務(wù)器和客戶機(jī)程序 這個(gè)例子將以最簡(jiǎn)單的方式運(yùn)用套接字對(duì)服務(wù)器和客戶機(jī)進(jìn)行操作。服務(wù)器的全部工作就是等候建立一個(gè)連接,然后用那個(gè)連接產(chǎn)生的Socket創(chuàng)建一個(gè)InputStream以及一個(gè)OutputStream。在這之后,它從InputStream讀入的所有東西都會(huì)反饋給OutputStream,直到接收到行中止(END)為止,最后關(guān)閉連接。 客戶機(jī)連接與服務(wù)器的連接,然后創(chuàng)建一個(gè)OutputStream。文本行通過OutputStream發(fā)送。客戶機(jī)也會(huì)創(chuàng)建一個(gè)InputStream,用它收聽服務(wù)器說些什么(本例只不過是反饋回來的同樣的字句)。 服務(wù)器與客戶機(jī)(程序)都使用同樣的端口號(hào),而且客戶機(jī)利用本地主機(jī)地址連接位于同一臺(tái)機(jī)器中的服務(wù)器(程序),所以不必在一個(gè)物理性的網(wǎng)絡(luò)里完成測(cè)試(在某些配置環(huán)境中,可能需要同真正的網(wǎng)絡(luò)建立連接,否則程序不能工作——盡管實(shí)際并不通過那個(gè)網(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需要的只是一個(gè)端口編號(hào),不需要IP地址(因?yàn)樗驮谶@臺(tái)機(jī)器上運(yùn)行)。調(diào)用accept()時(shí),方法會(huì)暫時(shí)陷入停頓狀態(tài)(堵塞),直到某個(gè)客戶嘗試同它建立連接。換言之,盡管它在那里等候連接,但其他進(jìn)程仍能正常運(yùn)行(參考第14章)。建好一個(gè)連接以后,accept()就會(huì)返回一個(gè)Socket對(duì)象,它是那個(gè)連接的代表。 清除套接字的責(zé)任在這里得到了很藝術(shù)的處理。假如ServerSocket構(gòu)建器失敗,則程序簡(jiǎn)單地退出(注意必須保證ServerSocket的構(gòu)建器在失敗之后不會(huì)留下任何打開的網(wǎng)絡(luò)套接字)。針對(duì)這種情況,main()會(huì)“擲”出一個(gè)IOException違例,所以不必使用一個(gè)try塊。若ServerSocket構(gòu)建器成功執(zhí)行,則其他所有方法調(diào)用都必須到一個(gè)try-finally代碼塊里尋求保護(hù),以確保無(wú)論塊以什么方式留下,ServerSocket都能正確地關(guān)閉。 同樣的道理也適用于由accept()返回的Socket。若accept()失敗,那么我們必須保證Socket不再存在或者含有任何資源,以便不必清除它們。但假若執(zhí)行成功,則后續(xù)的語(yǔ)句必須進(jìn)入一個(gè)try-finally塊內(nèi),以保障在它們失敗的情況下,Socket仍能得到正確的清除。由于套接字使用了重要的非內(nèi)存資源,所以在這里必須特別謹(jǐn)慎,必須自己動(dòng)手將它們清除(Java中沒有提供“破壞器”來幫助我們做這件事情)。 無(wú)論ServerSocket還是由accept()產(chǎn)生的Socket都打印到System.out里。這意味著它們的toString方法會(huì)得到自動(dòng)調(diào)用。這樣便產(chǎn)生了: ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080] 大家不久就會(huì)看到它們?nèi)绾闻c客戶程序做的事情配合。 程序的下一部分看來似乎僅僅是打開文件,以便讀取和寫入,只是InputStream和OutputStream是從Socket對(duì)象創(chuàng)建的。利用兩個(gè)“轉(zhuǎn)換器”類InputStreamReader和OutputStreamWriter,InputStream和OutputStream對(duì)象已經(jīng)分別轉(zhuǎn)換成為Java 1.1的Reader和Writer對(duì)象。也可以直接使用Java1.0的InputStream和OutputStream類,但對(duì)輸出來說,使用Writer方式具有明顯的優(yōu)勢(shì)。這一優(yōu)勢(shì)是通過PrintWriter表現(xiàn)出來的,它有一個(gè)過載的構(gòu)建器,能獲取第二個(gè)參數(shù)——一個(gè)布爾值標(biāo)志,指向是否在每一次println()結(jié)束的時(shí)候自動(dòng)刷新輸出(但不適用于print()語(yǔ)句)。每次寫入了輸出內(nèi)容后(寫進(jìn)out),它的緩沖區(qū)必須刷新,使信息能正式通過網(wǎng)絡(luò)傳遞出去。對(duì)目前這個(gè)例子來說,刷新顯得尤為重要,因?yàn)榭蛻艉头?wù)器在采取下一步操作之前都要等待一行文本內(nèi)容的到達(dá)。若刷新沒有發(fā)生,那么信息不會(huì)進(jìn)入網(wǎng)絡(luò),除非緩沖區(qū)滿(溢出),這會(huì)為本例帶來許多問題。 編寫網(wǎng)絡(luò)應(yīng)用程序時(shí),需要特別注意自動(dòng)刷新機(jī)制的使用。每次刷新緩沖區(qū)時(shí),必須創(chuàng)建和發(fā)出一個(gè)數(shù)據(jù)包(數(shù)據(jù)封)。就目前的情況來說,這正是我們所希望的,因?yàn)榧偃绨鼉?nèi)包含了還沒有發(fā)出的文本行,服務(wù)器和客戶機(jī)之間的相互“握手”就會(huì)停止。換句話說,一行的末尾就是一條消息的末尾。但在其他許多情況下,消息并不是用行分隔的,所以不如不用自動(dòng)刷新機(jī)制,而用內(nèi)建的緩沖區(qū)判決機(jī)制來決定何時(shí)發(fā)送一個(gè)數(shù)據(jù)包。這樣一來,我們可以發(fā)出較大的數(shù)據(jù)包,而且處理進(jìn)程也能加快。 注意和我們打開的幾乎所有數(shù)據(jù)流一樣,它們都要進(jìn)行緩沖處理。本章末尾有一個(gè)練習(xí),清楚展現(xiàn)了假如我們不對(duì)數(shù)據(jù)流進(jìn)行緩沖,那么會(huì)得到什么樣的后果(速度會(huì)變慢)。 無(wú)限while循環(huán)從BufferedReader in內(nèi)讀取文本行,并將信息寫入System.out,然后寫入PrintWriter.out。注意這可以是任何數(shù)據(jù)流,它們只是在表面上同網(wǎng)絡(luò)連接。 客戶程序發(fā)出包含了"END"的行后,程序會(huì)中止循環(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()中,大家可看到獲得本地主機(jī)IP地址的InetAddress的三種途徑:使用null,使用localhost,或者直接使用保留地址127.0.0.1。當(dāng)然,如果想通過網(wǎng)絡(luò)同一臺(tái)遠(yuǎn)程主機(jī)連接,也可以換用那臺(tái)機(jī)器的IP地址。打印出InetAddress addr后(通過對(duì)toString()方法的自動(dòng)調(diào)用),結(jié)果如下: localhost/127.0.0.1 通過向getByName()傳遞一個(gè)null,它會(huì)默認(rèn)尋找localhost,并生成特殊的保留地址127.0.0.1。注意在名為socket的套接字創(chuàng)建時(shí),同時(shí)使用了InetAddress以及端口號(hào)。打印這樣的某個(gè)Socket對(duì)象時(shí),為了真正理解它的含義,請(qǐng)記住一次獨(dú)一無(wú)二的因特網(wǎng)連接是用下述四種數(shù)據(jù)標(biāo)識(shí)的:clientHost(客戶主機(jī))、clientPortNumber(客戶端口號(hào))、serverHost(服務(wù)主機(jī))以及serverPortNumber(服務(wù)端口號(hào))。服務(wù)程序啟動(dòng)后,會(huì)在本地主機(jī)(127.0.0.1)上建立為它分配的端口(8080)。一旦客戶程序發(fā)出請(qǐng)求,機(jī)器上下一個(gè)可用的端口就會(huì)分配給它(這種情況下是1077),這一行動(dòng)也在與服務(wù)程序相同的機(jī)器(127.0.0.1)上進(jìn)行。現(xiàn)在,為了使數(shù)據(jù)能在客戶及服務(wù)程序之間來回傳送,每一端都需要知道把數(shù)據(jù)發(fā)到哪里。所以在同一個(gè)“已知”服務(wù)程序連接的時(shí)候,客戶會(huì)發(fā)出一個(gè)“返回地址”,使服務(wù)器程序知道將自己的數(shù)據(jù)發(fā)到哪兒。我們?cè)诜?wù)器端的示范輸出中可以體會(huì)到這一情況: Socket[addr=127.0.0.1,port=1077,localport=8080] 這意味著服務(wù)器剛才已接受了來自127.0.0.1這臺(tái)機(jī)器的端口1077的連接,同時(shí)監(jiān)聽自己的本地端口(8080)。而在客戶端: Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] 這意味著客戶已用自己的本地端口1077與127.0.0.1機(jī)器上的端口8080建立了 連接。 大家會(huì)注意到每次重新啟動(dòng)客戶程序的時(shí)候,本地端口的編號(hào)都會(huì)增加。這個(gè)編號(hào)從1025(剛好在系統(tǒng)保留的1-1024之外)開始,并會(huì)一直增加下去,除非我們重啟機(jī)器。若重新啟動(dòng)機(jī)器,端口號(hào)仍然會(huì)從1025開始增值(在Unix機(jī)器中,一旦超過保留的套按字范圍,數(shù)字就會(huì)再次從最小的可用數(shù)字開始)。 創(chuàng)建好Socket對(duì)象后,將其轉(zhuǎn)換成BufferedReader和PrintWriter的過程便與在服務(wù)器中相同(同樣地,兩種情況下都要從一個(gè)Socket開始)。在這里,客戶通過發(fā)出字串"howdy",并在后面跟隨一個(gè)數(shù)字,從而初始化通信。注意緩沖區(qū)必須再次刷新(這是自動(dòng)發(fā)生的,通過傳遞給PrintWriter構(gòu)建器的第二個(gè)參數(shù))。若緩沖區(qū)沒有刷新,那么整個(gè)會(huì)話(通信)都會(huì)被掛起,因?yàn)橛糜诔跏蓟摹癶owdy”永遠(yuǎn)不會(huì)發(fā)送出去(緩沖區(qū)不夠滿,不足以造成發(fā)送動(dòng)作的自動(dòng)進(jìn)行)。從服務(wù)器返回的每一行都會(huì)寫入System.out,以驗(yàn)證一切都在正常運(yùn)轉(zhuǎn)。為中止會(huì)話,需要發(fā)出一個(gè)"END"。若客戶程序簡(jiǎn)單地掛起,那么服務(wù)器會(huì)“擲”出一個(gè)違例。 大家在這里可以看到我們采用了同樣的措施來確保由Socket代表的網(wǎng)絡(luò)資源得到正確的清除,這是用一個(gè)try-finally塊實(shí)現(xiàn)的。 套接字建立了一個(gè)“專用”連接,它會(huì)一直持續(xù)到明確斷開連接為止(專用連接也可能間接性地?cái)嚅_,前提是某一端或者中間的某條鏈路出現(xiàn)故障而崩潰)。這意味著參與連接的雙方都被鎖定在通信中,而且無(wú)論是否有數(shù)據(jù)傳遞,連接都會(huì)連續(xù)處于開放狀態(tài)。從表面看,這似乎是一種合理的連網(wǎng)方式。然而,它也為網(wǎng)絡(luò)帶來了額外的開銷。本章后面會(huì)介紹進(jìn)行連網(wǎng)的另一種方式。采用那種方式,連接的建立只是暫時(shí)的。 15.3 服務(wù)多個(gè)客戶 JabberServer可以正常工作,但每次只能為一個(gè)客戶程序提供服務(wù)。在典型的服務(wù)器中,我們希望同時(shí)能處理多個(gè)客戶的請(qǐng)求。解決這個(gè)問題的關(guān)鍵就是多線程處理機(jī)制。而對(duì)于那些本身不支持多線程的語(yǔ)言,達(dá)到這個(gè)要求無(wú)疑是異常困難的。通過第14章的學(xué)習(xí),大家已經(jīng)知道Java已對(duì)多線程的處理進(jìn)行了盡可能的簡(jiǎn)化。由于Java的線程處理方式非常直接,所以讓服務(wù)器控制多名客戶并不是件難事。 最基本的方法是在服務(wù)器(程序)里創(chuàng)建單個(gè)ServerSocket,并調(diào)用accept()來等候一個(gè)新連接。一旦accept()返回,我們就取得結(jié)果獲得的Socket,并用它新建一個(gè)線程,令其只為那個(gè)特定的客戶服務(wù)。然后再調(diào)用accept(),等候下一次新的連接請(qǐng)求。 對(duì)于下面這段服務(wù)器代碼,大家可發(fā)現(xiàn)它與JabberServer.java例子非常相似,只是為一個(gè)特定的客戶提供服務(wù)的所有操作都已移入一個(gè)獨(dú)立的線程類中: //: 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(); } } } ///:~ 每次有新客戶請(qǐng)求建立一個(gè)連接時(shí),ServeOneJabber線程都會(huì)取得由accept()在main()中生成的Socket對(duì)象。然后和往常一樣,它創(chuàng)建一個(gè)BufferedReader,并用Socket自動(dòng)刷新PrintWriter對(duì)象。最后,它調(diào)用Thread的特殊方法start(),令其進(jìn)行線程的初始化,然后調(diào)用run()。這里采取的操作與前例是一樣的:從套掃字讀入某些東西,然后把它原樣反饋回去,直到遇到一個(gè)特殊的"END"結(jié)束標(biāo)志為止。 同樣地,套接字的清除必須進(jìn)行謹(jǐn)慎的設(shè)計(jì)。就目前這種情況來說,套接字是在ServeOneJabber外部創(chuàng)建的,所以清除工作可以“共享”。若ServeOneJabber構(gòu)建器失敗,那么只需向調(diào)用者“擲”出一個(gè)違例即可,然后由調(diào)用者負(fù)責(zé)線程的清除。但假如構(gòu)建器成功,那么必須由ServeOneJabber對(duì)象負(fù)責(zé)線程的清除,這是在它的run()里進(jìn)行的。 請(qǐng)注意MultiJabberServer有多么簡(jiǎn)單。和以前一樣,我們創(chuàng)建一個(gè)ServerSocket,并調(diào)用accept()允許一個(gè)新連接的建立。但這一次,accept()的返回值(一個(gè)套接字)將傳遞給用于ServeOneJabber的構(gòu)建器,由它創(chuàng)建一個(gè)新線程,并對(duì)那個(gè)連接進(jìn)行控制。連接中斷后,線程便可簡(jiǎn)單地消失。 如果ServerSocket創(chuàng)建失敗,則再一次通過main()擲出違例。如果成功,則位于外層的try-finally代碼塊可以擔(dān)保正確的清除。位于內(nèi)層的try-catch塊只負(fù)責(zé)防范ServeOneJabber構(gòu)建器的失敗;若構(gòu)建器成功,則ServeOneJabber線程會(huì)將對(duì)應(yīng)的套接字關(guān)掉。 為了證實(shí)服務(wù)器代碼確實(shí)能為多名客戶提供服務(wù),下面這個(gè)程序?qū)?chuàng)建許多客戶(使用線程),并同相同的服務(wù)器建立連接。每個(gè)線程的“存在時(shí)間”都是有限的。一旦到期,就留出空間以便創(chuàng)建一個(gè)新線程。允許創(chuàng)建的線程的最大數(shù)量是由final int maxthreads決定的。大家會(huì)注意到這個(gè)值非常關(guān)鍵,因?yàn)榧偃绨阉O(shè)得很大,線程便有可能耗盡資源,并產(chǎn)生不可預(yù)知的程序錯(cuò)誤。 //: 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)建器獲取一個(gè)InetAddress,并用它打開一個(gè)套接字。大家可能已看出了這樣的一個(gè)套路:Socket肯定用于創(chuàng)建某種Reader以及/或者Writer(或者InputStream和/或OutputStream)對(duì)象,這是運(yùn)用Socket的唯一方式(當(dāng)然,我們可考慮編寫一、兩個(gè)類,令其自動(dòng)完成這些操作,避免大量重復(fù)的代碼編寫工作)。同樣地,start()執(zhí)行線程的初始化,并調(diào)用run()。在這里,消息發(fā)送給服務(wù)器,而來自服務(wù)器的信息則在屏幕上回顯出來。然而,線程的“存在時(shí)間”是有限的,最終都會(huì)結(jié)束。注意在套接字創(chuàng)建好以后,但在構(gòu)建器完成之前,假若構(gòu)建器失敗,套接字會(huì)被清除。否則,為套接字調(diào)用close()的責(zé)任便落到了run()方法的頭上。 threadcount跟蹤計(jì)算目前存在的JabberClientThread對(duì)象的數(shù)量。它將作為構(gòu)建器的一部分增值,并在run()退出時(shí)減值(run()退出意味著線程中止)。在MultiJabberClient.main()中,大家可以看到線程的數(shù)量會(huì)得到檢查。若數(shù)量太多,則多余的暫時(shí)不創(chuàng)建。方法隨后進(jìn)入“休眠”狀態(tài)。這樣一來,一旦部分線程最后被中止,多作的那些線程就可以創(chuàng)建了。大家可試驗(yàn)一下逐漸增大MAX_THREADS,看看對(duì)于你使用的系統(tǒng)來說,建立多少線程(連接)才會(huì)使您的系統(tǒng)資源降低到危險(xiǎn)程度。 15.4 數(shù)據(jù)報(bào) 大家迄今看到的例子使用的都是“傳輸控制協(xié)議”(TCP),亦稱作“基于數(shù)據(jù)流的套接字”。根據(jù)該協(xié)議的設(shè)計(jì)宗旨,它具有高度的可靠性,而且能保證數(shù)據(jù)順利抵達(dá)目的地。換言之,它允許重傳那些由于各種原因半路“走失”的數(shù)據(jù)。而且收到字節(jié)的順序與它們發(fā)出來時(shí)是一樣的。當(dāng)然,這種控制與可靠性需要我們付出一些代價(jià):TCP具有非常高的開銷。 還有另一種協(xié)議,名為“用戶數(shù)據(jù)報(bào)協(xié)議”(UDP),它并不刻意追求數(shù)據(jù)包會(huì)完全發(fā)送出去,也不能擔(dān)保它們抵達(dá)的順序與它們發(fā)出時(shí)一樣。我們認(rèn)為這是一種“不可靠協(xié)議”(TCP當(dāng)然是“可靠協(xié)議”)。聽起來似乎很糟,但由于它的速度快得多,所以經(jīng)常還是有用武之地的。對(duì)某些應(yīng)用來說,比如聲音信號(hào)的傳輸,如果少量數(shù)據(jù)包在半路上丟失了,那么用不著太在意,因?yàn)閭鬏數(shù)乃俣蕊@得更重要一些。大多數(shù)互聯(lián)網(wǎng)游戲,如Diablo,采用的也是UDP協(xié)議通信,因?yàn)榫W(wǎng)絡(luò)通信的快慢是游戲是否流暢的決定性因素。也可以想想一臺(tái)報(bào)時(shí)服務(wù)器,如果某條消息丟失了,那么也真的不必過份緊張。另外,有些應(yīng)用也許能向服務(wù)器傳回一條UDP消息,以便以后能夠恢復(fù)。如果在適當(dāng)?shù)臅r(shí)間里沒有響應(yīng),消息就會(huì)丟失。 Java對(duì)數(shù)據(jù)報(bào)的支持與它對(duì)TCP套接字的支持大致相同,但也存在一個(gè)明顯的區(qū)別。對(duì)數(shù)據(jù)報(bào)來說,我們?cè)诳蛻艉头?wù)器程序都可以放置一個(gè)DatagramSocket(數(shù)據(jù)報(bào)套接字),但與ServerSocket不同,前者不會(huì)干巴巴地等待建立一個(gè)連接的請(qǐng)求。這是由于不再存在“連接”,取而代之的是一個(gè)數(shù)據(jù)報(bào)陳列出來。另一項(xiàng)本質(zhì)的區(qū)別的是對(duì)TCP套接字來說,一旦我們建好了連接,便不再需要關(guān)心誰(shuí)向誰(shuí)“說話”——只需通過會(huì)話流來回傳送數(shù)據(jù)即可。但對(duì)數(shù)據(jù)報(bào)來說,它的數(shù)據(jù)包必須知道自己來自何處,以及打算去哪里。這意味著我們必須知道每個(gè)數(shù)據(jù)報(bào)包的這些信息,否則信息就不能正常地傳遞。 DatagramSocket用于收發(fā)數(shù)據(jù)包,而DatagramPacket包含了具體的信息。準(zhǔn)備接收一個(gè)數(shù)據(jù)報(bào)時(shí),只需提供一個(gè)緩沖區(qū),以便安置接收到的數(shù)據(jù)。數(shù)據(jù)包抵達(dá)時(shí),通過DatagramSocket,作為信息起源地的因特網(wǎng)地址以及端口編號(hào)會(huì)自動(dòng)得到初化。所以一個(gè)用于接收數(shù)據(jù)報(bào)的DatagramPacket構(gòu)建器是: DatagramPacket(buf, buf.length) 其中,buf是一個(gè)字節(jié)數(shù)組。既然buf是個(gè)數(shù)組,大家可能會(huì)奇怪為什么構(gòu)建器自己不能調(diào)查出數(shù)組的長(zhǎng)度呢?實(shí)際上我也有同感,唯一能猜到的原因就是C風(fēng)格的編程使然,那里的數(shù)組不能自己告訴我們它有多大。 可以重復(fù)使用數(shù)據(jù)報(bào)的接收代碼,不必每次都建一個(gè)新的。每次用它的時(shí)候(再生),緩沖區(qū)內(nèi)的數(shù)據(jù)都會(huì)被覆蓋。 緩沖區(qū)的最大容量?jī)H受限于允許的數(shù)據(jù)報(bào)包大小,這個(gè)限制位于比64KB稍小的地方。但在許多應(yīng)用程序中,我們都寧愿它變得還要小一些,特別是在發(fā)送數(shù)據(jù)的時(shí)候。具體選擇的數(shù)據(jù)包大小取決于應(yīng)用程序的特定要求。 發(fā)出一個(gè)數(shù)據(jù)報(bào)時(shí),DatagramPacket不僅需要包含正式的數(shù)據(jù),也要包含因特網(wǎng)地址以及端口號(hào),以決定它的目的地。所以用于輸出DatagramPacket的構(gòu)建器是: DatagramPacket(buf, length, inetAddress, port) 這一次,buf(一個(gè)字節(jié)數(shù)組)已經(jīng)包含了我們想發(fā)出的數(shù)據(jù)。length可以是buf的長(zhǎng)度,但也可以更短一些,意味著我們只想發(fā)出那么多的字節(jié)。另兩個(gè)參數(shù)分別代表數(shù)據(jù)包要到達(dá)的因特網(wǎng)地址以及目標(biāo)機(jī)器的一個(gè)目標(biāo)端口(注釋②)。 ②:我們認(rèn)為TCP和UDP端口是相互獨(dú)立的。也就是說,可以在端口8080同時(shí)運(yùn)行一個(gè)TCP和UDP服務(wù)程序,兩者之間不會(huì)產(chǎn)生沖突。 大家也許認(rèn)為兩個(gè)構(gòu)建器創(chuàng)建了兩個(gè)不同的對(duì)象:一個(gè)用于接收數(shù)據(jù)報(bào),另一個(gè)用于發(fā)送它們。如果是好的面向?qū)ο蟮脑O(shè)計(jì)方案,會(huì)建議把它們創(chuàng)建成兩個(gè)不同的類,而不是具有不同的行為的一個(gè)類(具體行為取決于我們?nèi)绾螛?gòu)建對(duì)象)。這也許會(huì)成為一個(gè)嚴(yán)重的問題,但幸運(yùn)的是,DatagramPacket的使用相當(dāng)簡(jiǎn)單,我們不需要在這個(gè)問題上糾纏不清。這一點(diǎn)在下例里將有很明確的說明。該例類似于前面針對(duì)TCP套接字的MultiJabberServer和MultiJabberClient例子。多個(gè)客戶都會(huì)將數(shù)據(jù)報(bào)發(fā)給服務(wù)器,后者會(huì)將其反饋回最初發(fā)出消息的同樣的客戶。 為簡(jiǎn)化從一個(gè)String里創(chuàng)建DatagramPacket的工作(或者從DatagramPacket里創(chuàng)建String),這個(gè)例子首先用到了一個(gè)工具類,名為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的第一個(gè)方法采用一個(gè)String、一個(gè)InetAddress以及一個(gè)端口號(hào)作為自己的參數(shù),將String的內(nèi)容復(fù)制到一個(gè)字節(jié)緩沖區(qū),再將緩沖區(qū)傳遞進(jìn)入DatagramPacket構(gòu)建器,從而構(gòu)建一個(gè)DatagramPacket。注意緩沖區(qū)分配時(shí)的"+1"——這對(duì)防止截尾現(xiàn)象是非常重要的。String的getByte()方法屬于一種特殊操作,能將一個(gè)字串包含的char復(fù)制進(jìn)入一個(gè)字節(jié)緩沖。該方法現(xiàn)在已被“反對(duì)”使用;Java 1.1有一個(gè)“更好”的辦法來做這個(gè)工作,但在這里卻被當(dāng)作注釋屏蔽掉了,因?yàn)樗鼤?huì)截掉String的部分內(nèi)容。所以盡管我們?cè)贘ava 1.1下編譯該程序時(shí)會(huì)得到一條“反對(duì)”消息,但它的行為仍然是正確無(wú)誤的(這個(gè)錯(cuò)誤應(yīng)該在你讀到這里的時(shí)候修正了)。 Dgram.toString()方法同時(shí)展示了Java 1.0的方法和Java 1.1的方法(兩者是不同的,因?yàn)橛幸环N新類型的String構(gòu)建器)。 下面是用于數(shù)據(jù)報(bào)演示的服務(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)建了一個(gè)用來接收消息的DatagramSocket(數(shù)據(jù)報(bào)套接字),而不是在我們每次準(zhǔn)備接收一條新消息時(shí)都新建一個(gè)。這個(gè)單一的DatagramSocket可以重復(fù)使用。它有一個(gè)端口號(hào),因?yàn)檫@屬于服務(wù)器,客戶必須確切知道自己把數(shù)據(jù)報(bào)發(fā)到哪個(gè)地址。盡管有一個(gè)端口號(hào),但沒有為它分配因特網(wǎng)地址,因?yàn)樗婉v留在“這”臺(tái)機(jī)器內(nèi),所以知道自己的因特網(wǎng)地址是什么(目前是默認(rèn)的localhost)。在無(wú)限while循環(huán)中,套接字被告知接收數(shù)據(jù)(receive())。然后暫時(shí)掛起,直到一個(gè)數(shù)據(jù)報(bào)出現(xiàn),再把它反饋回我們希望的接收人——DatagramPacket dp——里面。數(shù)據(jù)包(Packet)會(huì)被轉(zhuǎn)換成一個(gè)字串,同時(shí)插入的還有數(shù)據(jù)包的起源因特網(wǎng)地址及套接字。這些信息會(huì)顯示出來,然后添加一個(gè)額外的字串,指出自己已從服務(wù)器反饋回來了。 大家可能會(huì)覺得有點(diǎn)兒迷惑。正如大家會(huì)看到的那樣,許多不同的因特網(wǎng)地址和端口號(hào)都可能是消息的起源地——換言之,客戶程序可能駐留在任何一臺(tái)機(jī)器里(就這一次演示來說,它們都駐留在localhost里,但每個(gè)客戶使用的端口編號(hào)是不同的)。為了將一條消息送回它真正的始發(fā)客戶,需要知道那個(gè)客戶的因特網(wǎng)地址以及端口號(hào)。幸運(yùn)的是,所有這些資料均已非常周到地封裝到發(fā)出消息的DatagramPacket內(nèi)部,所以我們要做的全部事情就是用getAddress()和getPort()把它們?nèi)〕鰜怼@眠@些資料,可以構(gòu)建DatagramPacket echo——它通過與接收用的相同的套接字發(fā)送回來。除此以外,一旦套接字發(fā)出數(shù)據(jù)報(bào),就會(huì)添加“這”臺(tái)機(jī)器的因特網(wǎng)地址及端口信息,所以當(dāng)客戶接收消息時(shí),它可以利用getAddress()和getPort()了解數(shù)據(jù)報(bào)來自何處。事實(shí)上,getAddress()和getPort()唯一不能告訴我們數(shù)據(jù)報(bào)來自何處的前提是:我們創(chuàng)建一個(gè)待發(fā)送的數(shù)據(jù)報(bào),并在正式發(fā)出之前調(diào)用了getAddress()和getPort()。到數(shù)據(jù)報(bào)正式發(fā)送的時(shí)候,這臺(tái)機(jī)器的地址以及端口才會(huì)寫入數(shù)據(jù)報(bào)。所以我們得到了運(yùn)用數(shù)據(jù)報(bào)時(shí)一項(xiàng)重要的原則:不必跟蹤一條消息的來源地!因?yàn)樗隙ū4嬖跀?shù)據(jù)報(bào)里。事實(shí)上,對(duì)程序來說,最可靠的做法是我們不要試圖跟蹤,而是無(wú)論如何都從目標(biāo)數(shù)據(jù)報(bào)里提取出地址以及端口信息(就象這里做的那樣)。 為測(cè)試服務(wù)器的運(yùn)轉(zhuǎn)是否正常,下面這程序?qū)?chuàng)建大量客戶(線程),它們都會(huì)將數(shù)據(jù)報(bào)包發(fā)給服務(wù)器,并等候服務(wù)器把它們?cè)瓨臃答伝貋怼? //: 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)建成一個(gè)線程(Thread),所以可以用多個(gè)客戶來“騷擾”服務(wù)器。從中可以看到,用于接收的DatagramPacket和用于ChatterServer的那個(gè)是相似的。在構(gòu)建器中,創(chuàng)建DatagramPacket時(shí)沒有附帶任何參數(shù)(自變量),因?yàn)樗恍枰鞔_指出自己位于哪個(gè)特定編號(hào)的端口里。用于這個(gè)套接字的因特網(wǎng)地址將成為“這臺(tái)機(jī)器”(比如localhost),而且會(huì)自動(dòng)分配端口編號(hào),這從輸出結(jié)果即可看出。同用于服務(wù)器的那個(gè)一樣,這個(gè)DatagramPacket將同時(shí)用于發(fā)送和接收。 hostAddress是我們想與之通信的那臺(tái)機(jī)器的因特網(wǎng)地址。在程序中,如果需要?jiǎng)?chuàng)建一個(gè)準(zhǔn)備傳出去的DatagramPacket,那么必須知道一個(gè)準(zhǔn)確的因特網(wǎng)地址和端口號(hào)。可以肯定的是,主機(jī)必須位于一個(gè)已知的地址和端口號(hào)上,使客戶能啟動(dòng)與主機(jī)的“會(huì)話”。 每個(gè)線程都有自己獨(dú)一無(wú)二的標(biāo)識(shí)號(hào)(盡管自動(dòng)分配給線程的端口號(hào)是也會(huì)提供一個(gè)唯一的標(biāo)識(shí)符)。在run()中,我們創(chuàng)建了一個(gè)String消息,其中包含了線程的標(biāo)識(shí)編號(hào)以及該線程準(zhǔn)備發(fā)送的消息編號(hào)。我們用這個(gè)字串創(chuàng)建一個(gè)數(shù)據(jù)報(bào),發(fā)到主機(jī)上的指定地址;端口編號(hào)則直接從ChatterServer內(nèi)的一個(gè)常數(shù)取得。一旦消息發(fā)出,receive()就會(huì)暫時(shí)被“堵塞”起來,直到服務(wù)器回復(fù)了這條消息。與消息附在一起的所有信息使我們知道回到這個(gè)特定線程的東西正是從始發(fā)消息中投遞出去的。在這個(gè)例子中,盡管是一種“不可靠”協(xié)議,但仍然能夠檢查數(shù)據(jù)報(bào)是否到去過了它們?cè)撊サ牡胤剑ㄟ@在localhost和LAN環(huán)境中是成立的,但在非本地連接中卻可能出現(xiàn)一些錯(cuò)誤)。 運(yùn)行該程序時(shí),大家會(huì)發(fā)現(xiàn)每個(gè)線程都會(huì)結(jié)束。這意味著發(fā)送到服務(wù)器的每個(gè)數(shù)據(jù)報(bào)包都會(huì)回轉(zhuǎn),并反饋回正確的接收者。如果不是這樣,一個(gè)或更多的線程就會(huì)掛起并進(jìn)入“堵塞”狀態(tài),直到它們的輸入被顯露出來。 大家或許認(rèn)為將文件從一臺(tái)機(jī)器傳到另一臺(tái)的唯一正確方式是通過TCP套接字,因?yàn)樗鼈兪恰翱煽俊钡摹H欢捎跀?shù)據(jù)報(bào)的速度非常快,所以它才是一種更好的選擇。我們只需將文件分割成多個(gè)數(shù)據(jù)報(bào),并為每個(gè)包編號(hào)。接收機(jī)器會(huì)取得這些數(shù)據(jù)包,并重新“組裝”它們;一個(gè)“標(biāo)題包”會(huì)告訴機(jī)器應(yīng)該接收多少個(gè)包,以及組裝所需的另一些重要信息。如果一個(gè)包在半路“走丟”了,接收機(jī)器會(huì)返回一個(gè)數(shù)據(jù)報(bào),告訴發(fā)送者重傳。 15.5 一個(gè)Web應(yīng)用 現(xiàn)在讓我們想想如何創(chuàng)建一個(gè)應(yīng)用,令其在真實(shí)的Web環(huán)境中運(yùn)行,它將把Java的優(yōu)勢(shì)表現(xiàn)得淋漓盡致。這個(gè)應(yīng)用的一部分是在Web服務(wù)器上運(yùn)行的一個(gè)Java程序,另一部分則是一個(gè)“程序片”或“小應(yīng)用程序”(Applet),從服務(wù)器下載至瀏覽器(即“客戶”)。這個(gè)程序片從用戶那里收集信息,并將其傳回Web服務(wù)器上運(yùn)行的應(yīng)用程序。程序的任務(wù)非常簡(jiǎn)單:程序片會(huì)詢問用戶的E-mail地址,并在驗(yàn)證這個(gè)地址合格后(沒有包含空格,而且有一個(gè)@符號(hào)),將該E-mail發(fā)送給Web服務(wù)器。服務(wù)器上運(yùn)行的程序則會(huì)捕獲傳回的數(shù)據(jù),檢查一個(gè)包含了所有E-mail地址的數(shù)據(jù)文件。如果那個(gè)地址已包含在文件里,則向?yàn)g覽器反饋一條消息,說明這一情況。該消息由程序片負(fù)責(zé)顯示。若是一個(gè)新地址,則將其置入列表,并通知程序片已成功添加了電子函件地址。 若采用傳統(tǒng)方式來解決這個(gè)問題,我們要?jiǎng)?chuàng)建一個(gè)包含了文本字段及一個(gè)“提交”(Submit)按鈕的HTML頁(yè)。用戶可在文本字段里鍵入自己喜歡的任何內(nèi)容,并毫無(wú)阻礙地提交給服務(wù)器(在客戶端不進(jìn)行任何檢查)。提交數(shù)據(jù)的同時(shí),Web頁(yè)也會(huì)告訴服務(wù)器應(yīng)對(duì)數(shù)據(jù)采取什么樣的操作——知會(huì)“通用網(wǎng)關(guān)接口”(CGI)程序,收到這些數(shù)據(jù)后立即運(yùn)行服務(wù)器。這種CGI程序通常是用Perl或C寫的(有時(shí)也用C++,但要求服務(wù)器支持),而且必須能控制一切可能出現(xiàn)的情況。它首先會(huì)檢查數(shù)據(jù),判斷是否采用了正確的格式。若答案是否定的,則CGI程序必須創(chuàng)建一個(gè)HTML頁(yè),對(duì)遇到的問題進(jìn)行描述。這個(gè)頁(yè)會(huì)轉(zhuǎn)交給服務(wù)器,再由服務(wù)器反饋回用戶。用戶看到出錯(cuò)提示后,必須再試一遍提交,直到通過為止。若數(shù)據(jù)正確,CGI程序會(huì)打開數(shù)據(jù)文件,要么把電子函件地址加入文件,要么指出該地址已在數(shù)據(jù)文件里了。無(wú)論哪種情況,都必須格式化一個(gè)恰當(dāng)?shù)腍TML頁(yè),以便服務(wù)器返回給用戶。 作為Java程序員,上述解決問題的方法顯得非常笨拙。而且很自然地,我們希望一切工作都用Java完成。首先,我們會(huì)用一個(gè)Java程序片負(fù)責(zé)客戶端的數(shù)據(jù)有效性校驗(yàn),避免數(shù)據(jù)在服務(wù)器和客戶之間傳來傳去,浪費(fèi)時(shí)間和帶寬,同時(shí)減輕服務(wù)器額外構(gòu)建HTML頁(yè)的負(fù)擔(dān)。然后跳過Perl CGI腳本,換成在服務(wù)器上運(yùn)行一個(gè)Java應(yīng)用。事實(shí)上,我們?cè)谶@兒已完全跳過了Web服務(wù)器,僅僅需要從程序片到服務(wù)器上運(yùn)行的Java應(yīng)用之間建立一個(gè)連接即可。 正如大家不久就會(huì)體驗(yàn)到的那樣,盡管看起來非常簡(jiǎn)單,但實(shí)際上有一些意想不到的問題使局面顯得稍微有些復(fù)雜。用Java 1.1寫程序片是最理想的,但實(shí)際上卻經(jīng)常行不通。到本書寫作的時(shí)候,擁有Java 1.1能力的瀏覽器仍為數(shù)不多,而且即使這類瀏覽器現(xiàn)在非常流行,仍需考慮照顧一下那些升級(jí)緩慢的人。所以從安全的角度看,程序片代碼最好只用Java 1.0編寫。基于這一前提,我們不能用JAR文件來合并(壓縮)程序片中的.class文件。所以,我們應(yīng)盡可能減少.class文件的使用數(shù)量,以縮短下載時(shí)間。 好了,再來說說我用的Web服務(wù)器(寫這個(gè)示范程序時(shí)用的就是它)。它確實(shí)支持Java,但僅限于Java 1.0!所以服務(wù)器應(yīng)用也必須用Java 1.0編寫。 15.5.1 服務(wù)器應(yīng)用 現(xiàn)在討論一下服務(wù)器應(yīng)用(程序)的問題,我把它叫作NameCollecor(名字收集器)。假如多名用戶同時(shí)嘗試提交他們的E-mail地址,那么會(huì)發(fā)生什么情況呢?若NameCollector使用TCP/IP套接字,那么必須運(yùn)用早先介紹的多線程機(jī)制來實(shí)現(xiàn)對(duì)多個(gè)客戶的并發(fā)控制。但所有這些線程都試圖把數(shù)據(jù)寫到同一個(gè)文件里,其中保存了所有E-mail地址。這便要求我們?cè)O(shè)立一種鎖定機(jī)制,保證多個(gè)線程不會(huì)同時(shí)訪問那個(gè)文件。一個(gè)“信號(hào)機(jī)”可在這里幫助我們達(dá)到目的,但或許還有一種更簡(jiǎn)單的方式。 如果我們換用數(shù)據(jù)報(bào),就不必使用多線程了。用單個(gè)數(shù)據(jù)報(bào)即可“偵聽”進(jìn)入的所有數(shù)據(jù)報(bào)。一旦監(jiān)視到有進(jìn)入的消息,程序就會(huì)進(jìn)行適當(dāng)?shù)奶幚恚⒋饛?fù)數(shù)據(jù)作為一個(gè)數(shù)據(jù)報(bào)傳回原先發(fā)出請(qǐng)求的那名接收者。若數(shù)據(jù)報(bào)半路上丟失了,則用戶會(huì)注意到?jīng)]有答復(fù)數(shù)據(jù)傳回,所以可以重新提交請(qǐng)求。 服務(wù)器應(yīng)用收到一個(gè)數(shù)據(jù)報(bào),并對(duì)它進(jìn)行解讀的時(shí)候,必須提取出其中的電子函件地址,并檢查本機(jī)保存的數(shù)據(jù)文件,看看里面是否已經(jīng)包含了那個(gè)地址(如果沒有,則添加之)。所以我們現(xiàn)在遇到了一個(gè)新的問題。Java 1.0似乎沒有足夠的能力來方便地處理包含了電子函件地址的文件(Java 1.1則不然)。但是,用C輕易就可以解決這個(gè)問題。因此,我們?cè)谶@兒有機(jī)會(huì)學(xué)習(xí)將一個(gè)非Java程序同Java程序連接的最簡(jiǎn)便方式。程序使用的Runtime對(duì)象包含了一個(gè)名為exec()的方法,它會(huì)獨(dú)立機(jī)器上一個(gè)獨(dú)立的程序,并返回一個(gè)Process(進(jìn)程)對(duì)象。我們可以取得一個(gè)OutputStream,它同這個(gè)單獨(dú)程序的標(biāo)準(zhǔn)輸入連接在一起;并取得一個(gè)InputStream,它則同標(biāo)準(zhǔn)輸出連接到一起。要做的全部事情就是用任何語(yǔ)言寫一個(gè)程序,只要它能從標(biāo)準(zhǔn)輸入中取得自己的輸入數(shù)據(jù),并將輸出結(jié)果寫入標(biāo)準(zhǔn)輸出即可。如果有些問題不能用Java簡(jiǎn)便與快速地解決(或者想利用原有代碼,不想改寫),就可以考慮采用這種方法。亦可使用Java的“固有方法”(Native Method),但那要求更多的技巧,大家可以參考一下附錄A。 1. C程序 這個(gè)非Java應(yīng)用是用C寫成,因?yàn)镴ava不適合作CGI編程;起碼啟動(dòng)的時(shí)間不能讓人滿意。它的任務(wù)是管理電子函件(E-mail)地址的一個(gè)列表。標(biāo)準(zhǔn)輸入會(huì)接受一個(gè)E-mail地址,程序會(huì)檢查列表中的名字,判斷是否存在那個(gè)地址。若不存在,就將其加入,并報(bào)告操作成功。但假如名字已在列表里了,就需要指出這一點(diǎn),避免重復(fù)加入。大家不必?fù)?dān)心自己不能完全理解下列代碼的含義。它僅僅是一個(gè)演示程序,告訴你如何用其他語(yǔ)言寫一個(gè)程序,并從Java中調(diào)用它。在這里具體采用何種語(yǔ)言并不重要,只要能夠從標(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編譯器能接受'//'樣式注釋(許多編譯器都能,亦可換用一個(gè)C++編譯器來編譯這個(gè)程序)。如果你的編譯器不能接受,則簡(jiǎn)單地將那些注釋刪掉即可。 文件中的第一個(gè)函數(shù)檢查我們作為第二個(gè)參數(shù)(指向一個(gè)char的指針)傳遞給它的名字是否已在文件中。在這兒,我們將文件作為一個(gè)FILE指針傳遞,它指向一個(gè)已打開的文件(文件是在main()中打開的)。函數(shù)fseek()在文件中遍歷;我們?cè)谶@兒用它移至文件開頭。fgets()從文件list中讀入一行內(nèi)容,并將其置入緩沖區(qū)lbuf——不會(huì)超過規(guī)定的緩沖區(qū)長(zhǎng)度BSIZE。所有這些工作都在一個(gè)while循環(huán)中進(jìn)行,所以文件中的每一行都會(huì)讀入。接下來,用strchr()找到新行字符,以便將其刪掉。最后,用strcmp()比較我們傳遞給函數(shù)的名字與文件中的當(dāng)前行。若找到一致的內(nèi)容,strcmp()會(huì)返回0。函數(shù)隨后會(huì)退出,并返回一個(gè)1,指出該名字已經(jīng)在文件里了(注意這個(gè)函數(shù)找到相符內(nèi)容后會(huì)立即返回,不會(huì)把時(shí)間浪費(fèi)在檢查列表剩余內(nèi)容的上面)。如果找遍列表都沒有發(fā)現(xiàn)相符的內(nèi)容,則函數(shù)返回0。 在main()中,我們用fopen()打開文件。第一個(gè)參數(shù)是文件名,第二個(gè)是打開文件的方式;a+表示“追加”,以及“打開”(或“創(chuàng)建”,假若文件尚不存在),以便到文件的末尾進(jìn)行更新。fopen()函數(shù)返回的是一個(gè)FILE指針;若為0,表示打開操作失敗。此時(shí)需要用perror()打印一條出錯(cuò)提示消息,并用exit()中止程序運(yùn)行。 如果文件成功打開,程序就會(huì)進(jìn)入一個(gè)無(wú)限循環(huán)。調(diào)用gets(buf)的函數(shù)會(huì)從標(biāo)準(zhǔn)輸入中取出一行(記住標(biāo)準(zhǔn)輸入會(huì)與Java程序連接到一起),并將其置入緩沖區(qū)buf中。緩沖區(qū)的內(nèi)容隨后會(huì)簡(jiǎn)單地傳遞給alreadyInList()函數(shù),如內(nèi)容已在列表中,printf()就會(huì)將那條消息發(fā)給標(biāo)準(zhǔn)輸出(Java程序正在監(jiān)視它)。fflush()用于對(duì)輸出緩沖區(qū)進(jìn)行刷新。 如果名字不在列表中,就用fseek()移到列表末尾,并用fprintf()將名字“打印”到列表末尾。隨后,用printf()指出名字已成功加入列表(同樣需要刷新標(biāo)準(zhǔn)輸出),無(wú)限循環(huán)返回,繼續(xù)等候一個(gè)新名字的進(jìn)入。 記住一般不能先在自己的計(jì)算機(jī)上編譯此程序,再把編譯好的內(nèi)容上載到Web服務(wù)器,因?yàn)槟桥_(tái)機(jī)器使用的可能是不同類的處理器和操作系統(tǒng)。例如,我的Web服務(wù)器安裝的是Intel的CPU,但操作系統(tǒng)是Linux,所以必須先下載源碼,再用遠(yuǎn)程命令(通過telnet)指揮Linux自帶的C編譯器,令其在服務(wù)器端編譯好程序。 2. Java程序 這個(gè)程序先啟動(dòng)上述的C程序,再建立必要的連接,以便同它“交談”。隨后,它創(chuàng)建一個(gè)數(shù)據(jù)報(bào)套接字,用它“監(jiān)視”或者“偵聽”來自程序片的數(shù)據(jù)報(bào)包。 //: 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中的第一個(gè)定義應(yīng)該是大家所熟悉的:選定端口,創(chuàng)建一個(gè)數(shù)據(jù)報(bào)包,然后創(chuàng)建指向一個(gè)DatagramSocket的句柄。接下來的三個(gè)定義負(fù)責(zé)與C程序的連接:一個(gè)Process對(duì)象是C程序由Java程序啟動(dòng)之后返回的,而且那個(gè)Process對(duì)象產(chǎn)生了InputStream和OutputStream,分別代表C程序的標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)輸入。和Java IO一樣,它們理所當(dāng)然地需要“封裝”起來,所以我們最后得到的是一個(gè)PrintStream和DataInputStream。 這個(gè)程序的所有工作都是在構(gòu)建器內(nèi)進(jìn)行的。為啟動(dòng)C程序,需要取得當(dāng)前的Runtime對(duì)象。我們用它調(diào)用exec(),再由后者返回Process對(duì)象。在Process對(duì)象中,大家可看到通過一簡(jiǎn)單的調(diào)用即可生成數(shù)據(jù)流:getOutputStream()和getInputStream()。從這個(gè)時(shí)候開始,我們需要考慮的全部事情就是將數(shù)據(jù)傳給數(shù)據(jù)流nameList,并從addResult中取得結(jié)果。 和往常一樣,我們將DatagramSocket同一個(gè)端口連接到一起。在無(wú)限while循環(huán)中,程序會(huì)調(diào)用receive()——除非一個(gè)數(shù)據(jù)報(bào)到來,否則receive()會(huì)一起處于“堵塞”狀態(tài)。數(shù)據(jù)報(bào)出現(xiàn)以后,它的內(nèi)容會(huì)提取到String rcvd里。我們首先將該字串兩頭的空格剔除(trim),再將其發(fā)給C程序。如下所示: nameList.println(rcvd.trim()); 之所以能這樣編碼,是因?yàn)镴ava的exec()允許我們?cè)L問任何可執(zhí)行模塊,只要它能從標(biāo)準(zhǔn)輸入中讀,并能向標(biāo)準(zhǔn)輸出中寫。還有另一些方式可與非Java代碼“交談”,這將在附錄A中討論。 從C程序中捕獲結(jié)果就顯得稍微麻煩一些。我們必須調(diào)用read(),并提供一個(gè)緩沖區(qū),以便保存結(jié)果。read()的返回值是來自C程序的字節(jié)數(shù)。若這個(gè)值為-1,意味著某個(gè)地方出現(xiàn)了問題。否則,我們就將resultBuf(結(jié)果緩沖區(qū))轉(zhuǎn)換成一個(gè)字串,然后同樣清除多余的空格。隨后,這個(gè)字串會(huì)象往常一樣進(jìn)入一個(gè)DatagramPacket,并傳回當(dāng)初發(fā)出請(qǐng)求的那個(gè)同樣的地址。注意發(fā)送方的地址也是我們接收到的DatagramPacket的一部分。 記住盡管C程序必須在Web服務(wù)器上編譯,但Java程序的編譯場(chǎng)所可以是任意的。這是由于不管使用的是什么硬件平臺(tái)和操作系統(tǒng),編譯得到的字節(jié)碼都是一樣的。就就是Java的“跨平臺(tái)”兼容能力。 15.5.2 NameSender程序片 正如早先指出的那樣,程序片必須用Java 1.0編寫,使其能與絕大多數(shù)的瀏覽器適應(yīng)。也正是由于這個(gè)原因,我們產(chǎn)生的類數(shù)量應(yīng)盡可能地少。所以我們?cè)谶@兒不考慮使用前面設(shè)計(jì)好的Dgram類,而將數(shù)據(jù)報(bào)的所有維護(hù)工作都轉(zhuǎn)到代碼行中進(jìn)行。此外,程序片要用一個(gè)線程監(jiān)視由服務(wù)器傳回的響應(yīng)信息,而非實(shí)現(xiàn)Runnable接口,用集成到程序片的一個(gè)獨(dú)立線程來做這件事情。當(dāng)然,這樣做對(duì)代碼的可讀性不利,但卻能產(chǎn)生一個(gè)單類(以及單個(gè)服務(wù)器請(qǐng)求)程序片: //: 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(用戶界面)非常簡(jiǎn)單。它包含了一個(gè)TestField(文本字段),以便我們鍵入一個(gè)電子函件地址;以及一個(gè)Button(按鈕),用于將地址發(fā)給服務(wù)器。兩個(gè)Label(標(biāo)簽)用于向用戶報(bào)告狀態(tài)信息。 到現(xiàn)在為止,大家已能判斷出DatagramSocket、InetAddress、緩沖區(qū)以及DatagramPacket都屬于網(wǎng)絡(luò)連接中比較麻煩的部分。最后,大家可看到run()方法實(shí)現(xiàn)了線程部分,使程序片能夠“偵聽”由服務(wù)器傳回的響應(yīng)信息。 init()方法用大家熟悉的布局工具設(shè)置GUI,然后創(chuàng)建DatagramSocket,它將同時(shí)用于數(shù)據(jù)報(bào)的收發(fā)。 action()方法只負(fù)責(zé)監(jiān)視我們是否按下了“發(fā)送”(send)按鈕。記住,我們已被限制在Java 1.0上面,所以不能再用較靈活的內(nèi)部類了。按鈕按下以后,采取的第一項(xiàng)行動(dòng)便是檢查線程pl,看看它是否為null(空)。如果不為null,表明有一個(gè)活動(dòng)線程正在運(yùn)行。消息首次發(fā)出時(shí),會(huì)啟動(dòng)一個(gè)新線程,用它監(jiān)視來自服務(wù)器的回應(yīng)。所以假若有個(gè)線程正在運(yùn)行,就意味著這并非用戶第一次發(fā)送消息。pl句柄被設(shè)為null,同時(shí)中止原來的監(jiān)視者(這是最合理的一種做法,因?yàn)閟top()已被Java 1.2“反對(duì)”,這在前一章已解釋過了)。 無(wú)論這是否按鈕被第一次按下,I2中的文字都會(huì)清除。 下一組語(yǔ)句將檢查E-mail名字是否合格。String.indexOf()方法的作用是搜索其中的非法字符。如果找到一個(gè),就把情況報(bào)告給用戶。注意進(jìn)行所有這些工作時(shí),都不必涉及網(wǎng)絡(luò)通信,所以速度非常快,而且不會(huì)影響帶寬和服務(wù)器的性能。 名字校驗(yàn)通過以后,它會(huì)打包到一個(gè)數(shù)據(jù)報(bào)里,然后采用與前面那個(gè)數(shù)據(jù)報(bào)示例一樣的方式發(fā)到主機(jī)地址和端口編號(hào)。第一個(gè)標(biāo)簽會(huì)發(fā)生變化,指出已成功發(fā)送出去。而且按鈕上的文字也會(huì)改變,變成“重發(fā)”(resend)。這時(shí)會(huì)啟動(dòng)線程,第二個(gè)標(biāo)簽則會(huì)告訴我們程序片正在等候來自服務(wù)器的回應(yīng)。 線程的run()方法會(huì)利用NameSender中包含的DatagramSocket來接收數(shù)據(jù)(receive()),除非出現(xiàn)來自服務(wù)器的數(shù)據(jù)報(bào)包,否則receive()會(huì)暫時(shí)處于“堵塞”或者“暫停”狀態(tài)。結(jié)果得到的數(shù)據(jù)包會(huì)放進(jìn)NameSender的DatagramPacketdp中。數(shù)據(jù)會(huì)從包中提取出來,并置入NameSender的第二個(gè)標(biāo)簽。隨后,線程的執(zhí)行將中斷,成為一個(gè)“死”線程。若某段時(shí)間里沒有收到來自服務(wù)器的回應(yīng),用戶可能變得不耐煩,再次按下按鈕。這樣做會(huì)中斷當(dāng)前線程(數(shù)據(jù)發(fā)出以后,會(huì)再建一個(gè)新的)。由于用一個(gè)線程來監(jiān)視回應(yīng)數(shù)據(jù),所以用戶在監(jiān)視期間仍然可以自由使用UI。 1. Web頁(yè) 當(dāng)然,程序片必須放到一個(gè)Web頁(yè)里。下面列出完整的Web頁(yè)源碼;稍微研究一下就可看出,我用它從自己開辦的郵寄列表(Mailling List)里自動(dòng)收集名字。 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)記()的使用非常簡(jiǎn)單,和第13章展示的那一個(gè)并沒有什么區(qū)別。 15.5.3 要注意的問題 前面采取的似乎是一種完美的方法。沒有CGI編程,所以在服務(wù)器啟動(dòng)一個(gè)CGI程序時(shí)不會(huì)出現(xiàn)延遲。數(shù)據(jù)報(bào)方式似乎能產(chǎn)生非常快的響應(yīng)。此外,一旦Java 1.1得到絕大多數(shù)人的采納,服務(wù)器端的那一部分就可完全用Java編寫(盡管利用標(biāo)準(zhǔn)輸入和輸出同一個(gè)非Java程序連接也非常容易)。 但必須注意到一些問題。其中一個(gè)特別容易忽略:由于Java應(yīng)用在服務(wù)器上是連續(xù)運(yùn)行的,而且會(huì)把大多數(shù)時(shí)間花在Datagram.receive()方法的等候上面,這樣便為CPU帶來了額外的開銷。至少,我在自己的服務(wù)器上便發(fā)現(xiàn)了這個(gè)問題。另一方面,那個(gè)服務(wù)器上不會(huì)發(fā)生其他更多的事情。而且假如我們使用一個(gè)任務(wù)更為繁重的服務(wù)器,啟動(dòng)程序用“nice”(一個(gè)Unix程序,用于防止進(jìn)程貪吃CPU資源)或其他等價(jià)程序即可解決問題。在許多情況下,都有必要留意象這樣的一些應(yīng)用——一個(gè)堵塞的receive()完全可能造成CPU的癱瘓。 第二個(gè)問題涉及防火墻。可將防火墻理解成自己的本地網(wǎng)與因特網(wǎng)之間的一道墻(實(shí)際是一個(gè)專用機(jī)器或防火墻軟件)。它監(jiān)視進(jìn)出因特網(wǎng)的所有通信,確保這些通信不違背預(yù)設(shè)的規(guī)則。 防火墻顯得多少有些保守,要求嚴(yán)格遵守所有規(guī)則。假如沒有遵守,它們會(huì)無(wú)情地把它們拒之門外。例如,假設(shè)我們位于防火墻后面的一個(gè)網(wǎng)絡(luò)中,開始用Web瀏覽器同因特網(wǎng)連接,防火墻要求所有傳輸都用可以接受的http端口同服務(wù)器連接,這個(gè)端口是80。現(xiàn)在來了這個(gè)Java程序片NameSender,它試圖將一個(gè)數(shù)據(jù)報(bào)傳到端口8080,這是為了越過“受保護(hù)”的端口范圍0-1024而設(shè)置的。防火墻很自然地把它想象成最壞的情況——有人使用病毒或者非法掃描端口——根本不允許傳輸?shù)睦^續(xù)進(jìn)行。 只要我們的客戶建立的是與因特網(wǎng)的原始連接(比如通過典型的ISP接駁Internet),就不會(huì)出現(xiàn)此類防火墻問題。但也可能有一些重要的客戶隱藏在防火墻后,他們便不能使用我們?cè)O(shè)計(jì)的程序。 在學(xué)過有關(guān)Java的這么多東西以后,這是一件使人相當(dāng)沮喪的事情,因?yàn)榭磥肀仨毞艞壴诜?wù)器上使用Java,改為學(xué)習(xí)如何編寫C或Perl腳本程序。但請(qǐng)大家不要絕望。 一個(gè)出色方案是由Sun公司提出的。如一切按計(jì)劃進(jìn)行,Web服務(wù)器最終都裝備“小服務(wù)程序”或者“服務(wù)程序片”(Servlet)。它們負(fù)責(zé)接收來自客戶的請(qǐng)求(經(jīng)過防火墻允許的80端口)。而且不再是啟動(dòng)一個(gè)CGI程序,它們會(huì)啟動(dòng)小服務(wù)程序。根據(jù)Sun的設(shè)想,這些小服務(wù)程序都是用Java編寫的,而且只能在服務(wù)器上運(yùn)行。運(yùn)行這種小程序的服務(wù)器會(huì)自動(dòng)啟動(dòng)它們,令其對(duì)客戶的請(qǐng)求進(jìn)行處理。這意味著我們的所有程序都可以用Java寫成(100%純咖啡)。這顯然是一種非常吸引人的想法:一旦習(xí)慣了Java,就不必?fù)Q用其他語(yǔ)言在服務(wù)器上處理客戶請(qǐng)求。 由于只能在服務(wù)器上控制請(qǐng)求,所以小服務(wù)程序API沒有提供GUI功能。這對(duì)NameCollector.java來說非常適合,它本來就不需要任何圖形界面。 在本書寫作時(shí),java.sun.com已提供了一個(gè)非常廉價(jià)的小服務(wù)程序?qū)S梅?wù)器。Sun鼓勵(lì)其他Web服務(wù)器開發(fā)者為他們的服務(wù)器軟件產(chǎn)品加入對(duì)小服務(wù)程序的支持。 15.6 Java與CGI的溝通 Java程序可向一個(gè)服務(wù)器發(fā)出一個(gè)CGI請(qǐng)求,這與HTML表單頁(yè)沒什么兩樣。而且和HTML頁(yè)一樣,這個(gè)請(qǐng)求既可以設(shè)為GET(下載),亦可設(shè)為POST(上傳)。除此以外,Java程序還可攔截CGI程序的輸出,所以不必依賴程序來格式化一個(gè)新頁(yè),也不必在出錯(cuò)的時(shí)候強(qiáng)迫用戶從一個(gè)頁(yè)回轉(zhuǎn)到另一個(gè)頁(yè)。事實(shí)上,程序的外觀可以做得跟以前的版本別無(wú)二致。 代碼也要簡(jiǎn)單一些,畢竟用CGI也不是很難就能寫出來(前提是真正地理解它)。所以在這一節(jié)里,我們準(zhǔn)備辦個(gè)CGI編程速成班。為解決常規(guī)問題,將用C++創(chuàng)建一些CGI工具,以便我們編寫一個(gè)能解決所有問題的CGI程序。這樣做的好處是移植能力特別強(qiáng)——即將看到的例子能在支持CGI的任何系統(tǒng)上運(yùn)行,而且不存在防火墻的問題。 這個(gè)例子也闡示了如何在程序片(Applet)和CGI程序之間建立連接,以便將其方便地改編到自己的項(xiàng)目中。 15.6.1 CGI數(shù)據(jù)的編碼 在這個(gè)版本中,我們將收集名字和電子函件地址,并用下述形式將其保存到文件中: First Last ; 這對(duì)任何E-mail程序來說都是一種非常方便的格式。由于只需收集兩個(gè)字段,而且CGI為字段中的編碼采用了一種特殊的格式,所以這里沒有簡(jiǎn)便的方法。如果自己動(dòng)手編制一個(gè)原始的HTML頁(yè),并加入下述代碼行,即可正確地理解這一點(diǎn):

    Name:

    Email Address:

    上述代碼創(chuàng)建了兩個(gè)數(shù)據(jù)輸入字段(區(qū)),名為name和email。另外還有一個(gè)submit(提交)按鈕,用于收集數(shù)據(jù),并將其發(fā)給CGI程序。Listmgr2.exe是駐留在特殊程序目錄中的一個(gè)可執(zhí)行文件。在我們的Web服務(wù)器上,該目錄一般都叫作“cgi-bin”(注釋③)。如果在那個(gè)目錄里找不到該程序,結(jié)果就無(wú)法出現(xiàn)。填好這個(gè)表單,然后按下提交按鈕,即可在瀏覽器的URL地址窗口里看到象下面這樣的內(nèi)容: http://www.myhome.com/cgi-bin/Listmgr2.exe?name=First+Last&email=email@domain.com&submit=Submit ③:在Windows32平臺(tái)下,可利用與Microsoft Office 97或其他產(chǎn)品配套提供的Microsoft Personal Web Server(微軟個(gè)人Web服務(wù)器)進(jìn)行測(cè)試。這是進(jìn)行試驗(yàn)的最好方法,因?yàn)椴槐卣竭B入網(wǎng)絡(luò),可在本地環(huán)境中完成測(cè)試(速度也非常快)。如果使用的是不同的平臺(tái),或者沒有Office 97或者FrontPage 98那樣的產(chǎn)品,可到網(wǎng)上找一個(gè)免費(fèi)的Web服務(wù)器供自己測(cè)試。 當(dāng)然,上述URL實(shí)際顯示時(shí)是不會(huì)拆行的。從中可稍微看出如何對(duì)數(shù)據(jù)編碼并傳給CGI。至少有一件事情能夠肯定——空格是不允許的(因?yàn)樗ǔS糜诜指裘钚袇?shù))。所有必需的空格都用“+”號(hào)替代,每個(gè)字段都包含了字段名(具體由HTML頁(yè)決定),后面跟隨一個(gè)“=”號(hào)以及正式的字段數(shù)據(jù),最后用一個(gè)“&”結(jié)束。 到這時(shí),大家也許會(huì)對(duì)“+”,“=”以及“&”的使用產(chǎn)生疑惑。假如必須在字段里使用這些字符,那么該如何聲明呢?例如,我們可能使用“John & MarshaSmith”這個(gè)名字,其中的“&”代表“And”。事實(shí)上,它會(huì)編碼成下面這個(gè)樣子: John+%26+Marsha+Smith 也就是說,特殊字符會(huì)轉(zhuǎn)換成一個(gè)“%”,并在后面跟上它的十六進(jìn)制ASCII編碼。 幸運(yùn)的是,Java有一個(gè)工具來幫助我們進(jìn)行這種編碼。這是URLEncoder類的一個(gè)靜態(tài)方法,名為encode()。可用下述程序來試驗(yàn)這個(gè)方法: //: EncodeDemo.java // Demonstration of URLEncoder.encode() import java.net.*; public class EncodeDemo { public static void main(String[] args) { String s = ""; for(int i = 0; i < args.length; i++) s += args[i] + " "; s = URLEncoder.encode(s.trim()); System.out.println(s); } } ///:~ 該程序?qū)@取一些命令行參數(shù),把它們合并成一個(gè)由多個(gè)詞構(gòu)成的字串,各詞之間用空格分隔(最后一個(gè)空格用String.trim()剔除了)。隨后對(duì)它們進(jìn)行編碼,并打印出來。 為調(diào)用一個(gè)CGI程序,程序片要做的全部事情就是從自己的字段或其他地方收集數(shù)據(jù),將所有數(shù)據(jù)都編碼成正確的URL樣式,然后匯編到單獨(dú)一個(gè)字串里。每個(gè)字段名后面都加上一個(gè)“=”符號(hào),緊跟正式數(shù)據(jù),再緊跟一個(gè)“&”。為構(gòu)建完整的CGI命令,我們將這個(gè)字串置于CGI程序的URL以及一個(gè)“?”后。這是調(diào)用所有CGI程序的標(biāo)準(zhǔn)方法。大家馬上就會(huì)看到,用一個(gè)程序片能夠很輕松地完成所有這些編碼與合并。 15.6.2 程序片 程序片實(shí)際要比NameSender.java簡(jiǎn)單一些。這部分是由于很容易即可發(fā)出一個(gè)GET請(qǐng)求。此外,也不必等候回復(fù)信息。現(xiàn)在有兩個(gè)字段,而非一個(gè),但大家會(huì)發(fā)現(xiàn)許多程序片都是熟悉的,請(qǐng)比較NameSender.java。 //: NameSender2.java // An applet that sends an email address // via a CGI GET, using Java 1.02. import java.awt.*; import java.applet.*; import java.net.*; import java.io.*; public class NameSender2 extends Applet { final String CGIProgram = "Listmgr2.exe"; Button send = new Button( "Add email address to mailing list"); TextField name = new TextField( "type your name here", 40), email = new TextField( "type your email address here", 40); String str = new String(); Label l = new Label(), l2 = new Label(); int vcount = 0; public void init() { setLayout(new BorderLayout()); Panel p = new Panel(); p.setLayout(new GridLayout(3, 1)); p.add(name); p.add(email); 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); l.setText("Ready to send email address"); } public boolean action (Event evt, Object arg) { if(evt.target.equals(send)) { l2.setText(""); // Check for errors in data: if(name.getText().trim() .indexOf(' ') == -1) { l.setText( "Please give first and last name"); l2.setText(""); return true; } str = email.getText().trim(); if(str.indexOf(' ') != -1) { l.setText( "Spaces not allowed in email name"); l2.setText(""); return true; } if(str.indexOf(',') != -1) { l.setText( "Commas not allowed in email name"); return true; } if(str.indexOf('@') == -1) { l.setText("Email name must include '@'"); l2.setText(""); return true; } if(str.indexOf('@') == 0) { l.setText( "Name must preceed '@' in email name"); 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; } // Build and encode the email data: String emailData = "name=" + URLEncoder.encode( name.getText().trim()) + "&email=" + URLEncoder.encode( email.getText().trim().toLowerCase()) + "&submit=Submit"; // Send the name using CGI's GET process: try { l.setText("Sending..."); URL u = new URL( getDocumentBase(), "cgi-bin/" + CGIProgram + "?" + emailData); l.setText("Sent: " + email.getText()); send.setLabel("Re-send"); l2.setText( "Waiting for reply " + ++vcount); DataInputStream server = new DataInputStream(u.openStream()); String line; while((line = server.readLine()) != null) l2.setText(line); } catch(MalformedURLException e) { l.setText("Bad URl"); } catch(IOException e) { l.setText("IO Exception"); } } else return super.action(evt, arg); return true; } } ///:~ CGI程序(不久即可看到)的名字是Listmgr2.exe。許多Web服務(wù)器都在Unix機(jī)器上運(yùn)行(Linux也越來越受到青睞)。根據(jù)傳統(tǒng),它們一般不為自己的可執(zhí)行程序采用.exe擴(kuò)展名。但在Unix操作系統(tǒng)中,可以把自己的程序稱呼為自己希望的任何東西。若使用的是.exe擴(kuò)展名,程序毋需任何修改即可通過Unix和Win32的運(yùn)行測(cè)試。 和往常一樣,程序片設(shè)置了自己的用戶界面(這次是兩個(gè)輸入字段,不是一個(gè))。唯一顯著的區(qū)別是在action()方法內(nèi)產(chǎn)生的。該方法的作用是對(duì)按鈕按下事件進(jìn)行控制。名字檢查過以后,大家會(huì)發(fā)現(xiàn)下述代碼行: String emailData = "name=" + URLEncoder.encode( name.getText().trim()) + "&email=" + URLEncoder.encode( email.getText().trim().toLowerCase()) + "&submit=Submit"; // Send the name using CGI's GET process: try { l.setText("Sending..."); URL u = new URL( getDocumentBase(), "cgi-bin/" + CGIProgram + "?" + emailData); l.setText("Sent: " + email.getText()); send.setLabel("Re-send"); l2.setText( "Waiting for reply " + ++vcount); DataInputStream server = new DataInputStream(u.openStream()); String line; while((line = server.readLine()) != null) l2.setText(line); // ... name和email數(shù)據(jù)都是它們對(duì)應(yīng)的文字框里提取出來,而且兩端多余的空格都用trim()剔去了。為了進(jìn)入列表,email名字被強(qiáng)制換成小寫形式,以便能夠準(zhǔn)確地對(duì)比(防止基于大小寫形式的錯(cuò)誤判斷)。來自每個(gè)字段的數(shù)據(jù)都編碼為URL形式,隨后采用與HTML頁(yè)中一樣的方式匯編GET字串(這樣一來,我們可將Java程序片與現(xiàn)有的任何CGI程序結(jié)合使用,以滿足常規(guī)的HTML GET請(qǐng)求)。 到這時(shí),一些Java的魔力已經(jīng)開始發(fā)揮作用了:如果想同任何URL連接,只需創(chuàng)建一個(gè)URL對(duì)象,并將地址傳遞給構(gòu)建器即可。構(gòu)建器會(huì)負(fù)責(zé)建立同服務(wù)器的連接(對(duì)Web服務(wù)器來說,所有連接行動(dòng)都是根據(jù)作為URL使用的字串來判斷的)。就目前這種情況來說,URL指向的是當(dāng)前Web站點(diǎn)的cgi-bin目錄(當(dāng)前Web站點(diǎn)的基礎(chǔ)地址是用getDocumentBase()設(shè)定的)。一旦Web服務(wù)器在URL中看到了一個(gè)“cgi-bin”,會(huì)接著希望在它后面跟隨了cgi-bin目錄內(nèi)的某個(gè)程序的名字,那是我們要運(yùn)行的目標(biāo)程序。程序名后面是一個(gè)問號(hào)以及CGI程序會(huì)在QUERY_STRING環(huán)境變量中查找的一個(gè)參數(shù)字串(馬上就要學(xué)到)。 我們發(fā)出任何形式的請(qǐng)求后,一般都會(huì)得到一個(gè)回應(yīng)的HTML頁(yè)。但若使用Java的URL對(duì)象,我們可以攔截自CGI程序傳回的任何東西,只需從URL對(duì)象里取得一個(gè)InputStream(輸入數(shù)據(jù)流)即可。這是用URL對(duì)象的openStream()方法實(shí)現(xiàn),它要封裝到一個(gè)DataInputStream里。隨后就可以讀取數(shù)據(jù)行,若readLine()返回一個(gè)null(空值),就表明CGI程序已結(jié)束了它的輸出。 我們即將看到的CGI程序返回的僅僅是一行,它是用于標(biāo)志成功與否(以及失敗的具體原因)的一個(gè)字串。這一行會(huì)被捕獲并置放第二個(gè)Label字段里,使用戶看到具體發(fā)生了什么事情。 1. 從程序片里顯示一個(gè)Web頁(yè) 程序亦可將CGI程序的結(jié)果作為一個(gè)Web頁(yè)顯示出來,就象它們?cè)谄胀℉TML模式中運(yùn)行那樣。可用下述代碼做到這一點(diǎn): getAppletContext().showDocument(u); 其中,u代表URL對(duì)象。這是將我們重新定向于另一個(gè)Web頁(yè)的一個(gè)簡(jiǎn)單例子。那個(gè)頁(yè)湊巧是一個(gè)CGI程序的輸出,但可以非常方便地進(jìn)入一個(gè)原始的HTML頁(yè),所以可以構(gòu)建這個(gè)程序片,令其產(chǎn)生一個(gè)由密碼保護(hù)的網(wǎng)關(guān),通過它進(jìn)入自己Web站點(diǎn)的特殊部分: //: ShowHTML.java import java.awt.*; import java.applet.*; import java.net.*; import java.io.*; public class ShowHTML extends Applet { static final String CGIProgram = "MyCGIProgram"; Button send = new Button("Go"); Label l = new Label(); public void init() { add(send); add(l); } public boolean action (Event evt, Object arg) { if(evt.target.equals(send)) { try { // This could be an HTML page instead of // a CGI program. Notice that this CGI // program doesn't use arguments, but // you can add them in the usual way. URL u = new URL( getDocumentBase(), "cgi-bin/" + CGIProgram); // Display the output of the URL using // the Web browser, as an ordinary page: getAppletContext().showDocument(u); } catch(Exception e) { l.setText(e.toString()); } } else return super.action(evt, arg); return true; } } ///:~ URL類的最大的特點(diǎn)就是有效地保護(hù)了我們的安全。可以同一個(gè)Web服務(wù)器建立連接,毋需知道幕后的任何東西。 15.6.3 用C++寫的CGI程序 經(jīng)過前面的學(xué)習(xí),大家應(yīng)該能夠根據(jù)例子用ANSI C為自己的服務(wù)器寫出CGI程序。之所以選用ANSI C,是因?yàn)樗鼛缀蹼S處可見,是最流行的C語(yǔ)言標(biāo)準(zhǔn)。當(dāng)然,現(xiàn)在的C++也非常流行了,特別是采用GNU C++編譯器(g++)形式的那一些(注釋④)。可從網(wǎng)上許多地方免費(fèi)下載g++,而且可選用幾乎所有平臺(tái)的版本(通常與Linux那樣的操作系統(tǒng)配套提供,且已預(yù)先安裝好)。正如大家即將看到的那樣,從CGI程序可獲得面向?qū)ο蟪绦蛟O(shè)計(jì)的許多好處。 ④:GNU的全稱是“Gnu's Not Unix”。這最早是由“自由軟件基金會(huì)”(FSF)負(fù)責(zé)開發(fā)的一個(gè)項(xiàng)目,致力于用一個(gè)免費(fèi)的版本取代原有的Unix操作系統(tǒng)。現(xiàn)在的Linux似乎正在做前人沒有做到的事情。但GNU工具在Linux的開發(fā)中扮演了至關(guān)重要的角色。事實(shí)上,Linux的整套軟件包附帶了數(shù)量非常多的GNU組件。 為避免第一次就提出過多的新概念,這個(gè)程序并未打算成為一個(gè)“純”C++程序;有些代碼是用普通C寫成的——盡管還可選用C++的一些替用形式。但這并不是個(gè)突出的問題,因?yàn)樵摮绦蛴肅++制作最大的好處就是能夠創(chuàng)建類。在解析CGI信息的時(shí)候,由于我們最關(guān)心的是字段的“名稱/值”對(duì),所以要用一個(gè)類(Pair)來代表單個(gè)名稱/值對(duì);另一個(gè)類(CGI_vector)則將CGI字串自動(dòng)解析到它會(huì)容納的Pair對(duì)象里(作為一個(gè)vector),這樣即可在有空的時(shí)候把每個(gè)Pair(對(duì))都取出來。 這個(gè)程序同時(shí)也非常有趣,因?yàn)樗菔玖薈++與Java相比的許多優(yōu)缺點(diǎn)。大家會(huì)看到一些相似的東西;比如class關(guān)鍵字。訪問控制使用的是完全相同的關(guān)鍵字public和private,但用法卻有所不同。它們控制的是一個(gè)塊,而非單個(gè)方法或字段(也就是說,如果指定private:,后續(xù)的每個(gè)定義都具有private屬性,直到我們?cè)僦付╬ublic:為止)。另外在創(chuàng)建一個(gè)類的時(shí)候,所有定義都自動(dòng)默認(rèn)為private。 在這兒使用C++的一個(gè)原因是要利用C++“標(biāo)準(zhǔn)模板庫(kù)”(STL)提供的便利。至少,STL包含了一個(gè)vector類。這是一個(gè)C++模板,可在編譯期間進(jìn)行配置,令其只容納一種特定類型的對(duì)象(這里是Pair對(duì)象)。和Java的Vector不同,如果我們?cè)噲D將除Pair對(duì)象之外的任何東西置入vector,C++的vector模板都會(huì)造成一個(gè)編譯期錯(cuò)誤;而Java的Vector能夠照單全收。而且從vector里取出什么東西的時(shí)候,它會(huì)自動(dòng)成為一個(gè)Pair對(duì)象,毋需進(jìn)行造型處理。所以檢查在編譯期進(jìn)行,這使程序顯得更為“健壯”。此外,程序的運(yùn)行速度也可以加快,因?yàn)闆]有必要進(jìn)行運(yùn)行期間的造型。vector也會(huì)過載operator[],所以可以利用非常方便的語(yǔ)法來提取Pair對(duì)象。vector模板將在CGI_vector創(chuàng)建時(shí)使用;在那時(shí),大家就可以體會(huì)到如此簡(jiǎn)短的一個(gè)定義居然蘊(yùn)藏有那么巨大的能量。 若提到缺點(diǎn),就一定不要忘記Pair在下列代碼中定義時(shí)的復(fù)雜程度。與我們?cè)贘ava代碼中看到的相比,Pair的方法定義要多得多。這是由于C++的程序員必須提前知道如何用副本構(gòu)建器控制復(fù)制過程,而且要用過載的operator=完成賦值。正如第12章解釋的那樣,我們有時(shí)也要在Java中考慮同樣的事情。但在C++中,幾乎一刻都不能放松對(duì)這些問題的關(guān)注。 這個(gè)項(xiàng)目首先創(chuàng)建一個(gè)可以重復(fù)使用的部分,由C++頭文件中的Pair和CGI_vector構(gòu)成。從技術(shù)角度看,確實(shí)不應(yīng)把這些東西都塞到一個(gè)頭文件里。但就目前的例子來說,這樣做不會(huì)造成任何方面的損害,而且更具有Java風(fēng)格,所以大家閱讀理解代碼時(shí)要顯得輕松一些: //: CGITools.h // Automatically extracts and decodes data // from CGI GETs and POSTs. Tested with GNU C++ // (available for most server machines). #include #include // STL vector using namespace std; // A class to hold a single name-value pair from // a CGI query. CGI_vector holds Pair objects and // returns them from its operator[]. class Pair { char* nm; char* val; public: Pair() { nm = val = 0; } Pair(char* name, char* value) { // Creates new memory: nm = decodeURLString(name); val = decodeURLString(value); } const char* name() const { return nm; } const char* value() const { return val; } // Test for "emptiness" bool empty() const { return (nm == 0) || (val == 0); } // Automatic type conversion for boolean test: operator bool() const { return (nm != 0) && (val != 0); } // The following constructors & destructor are // necessary for bookkeeping in C++. // Copy-constructor: Pair(const Pair& p) { if(p.nm == 0 || p.val == 0) { nm = val = 0; } else { // Create storage & copy rhs values: nm = new char[strlen(p.nm) + 1]; strcpy(nm, p.nm); val = new char[strlen(p.val) + 1]; strcpy(val, p.val); } } // Assignment operator: Pair& operator=(const Pair& p) { // Clean up old lvalues: delete nm; delete val; if(p.nm == 0 || p.val == 0) { nm = val = 0; } else { // Create storage & copy rhs values: nm = new char[strlen(p.nm) + 1]; strcpy(nm, p.nm); val = new char[strlen(p.val) + 1]; strcpy(val, p.val); } return *this; } ~Pair() { // Destructor delete nm; // 0 value OK delete val; } // If you use this method outide this class, // you're responsible for calling 'delete' on // the pointer that's returned: static char* decodeURLString(const char* URLstr) { int len = strlen(URLstr); char* result = new char[len + 1]; memset(result, len + 1, 0); for(int i = 0, j = 0; i <= len; i++, j++) { if(URLstr[i] == '+') result[j] = ' '; else if(URLstr[i] == '%') { result[j] = translateHex(URLstr[i + 1]) * 16 + translateHex(URLstr[i + 2]); i += 2; // Move past hex code } else // An ordinary character result[j] = URLstr[i]; } return result; } // Translate a single hex character; used by // decodeURLString(): static char translateHex(char hex) { if(hex >= 'A') return (hex & 0xdf) - 'A' + 10; else return hex - '0'; } }; // Parses any CGI query and turns it // into an STL vector of Pair objects: class CGI_vector : public vector { char* qry; const char* start; // Save starting position // Prevent assignment and copy-construction: void operator=(CGI_vector&); CGI_vector(CGI_vector&); public: // const fields must be initialized in the C++ // "Constructor initializer list": CGI_vector(char* query) : start(new char[strlen(query) + 1]) { qry = (char*)start; // Cast to non-const strcpy(qry, query); Pair p; while((p = nextPair()) != 0) push_back(p); } // Destructor: ~CGI_vector() { delete start; } private: // Produces name-value pairs from the query // string. Returns an empty Pair when there's // no more query string left: Pair nextPair() { char* name = qry; if(name == 0 || *name == '\0') return Pair(); // End, return null Pair char* value = strchr(name, '='); if(value == 0) return Pair(); // Error, return null Pair // Null-terminate name, move value to start // of its set of characters: *value = '\0'; value++; // Look for end of value, marked by '&': qry = strchr(value, '&'); if(qry == 0) qry = ""; // Last pair found else { *qry = '\0'; // Terminate value string qry++; // Move to next pair } return Pair(name, value); } }; ///:~ 在#include語(yǔ)句后,可看到有一行是: using namespace std; C++中的“命名空間”(Namespace)解決了由Java的package負(fù)責(zé)的一個(gè)問題:將庫(kù)名隱藏起來。std命名空間引用的是標(biāo)準(zhǔn)C++庫(kù),而vector就在這個(gè)庫(kù)中,所以這一行是必需的。 Pair類表面看異常簡(jiǎn)單,只是容納了兩個(gè)(private)字符指針而已——一個(gè)用于名字,另一個(gè)用于值。默認(rèn)構(gòu)建器將這兩個(gè)指針簡(jiǎn)單地設(shè)為零。這是由于在C++中,對(duì)象的內(nèi)存不會(huì)自動(dòng)置零。第二個(gè)構(gòu)建器調(diào)用方法decodeURLString(),在新分配的堆內(nèi)存中生成一個(gè)解碼過后的字串。這個(gè)內(nèi)存區(qū)域必須由對(duì)象負(fù)責(zé)管理及清除,這與“破壞器”中見到的相同。name()和value()方法為相關(guān)的字段產(chǎn)生只讀指針。利用empty()方法,我們查詢Pair對(duì)象它的某個(gè)字段是否為空;返回的結(jié)果是一個(gè)bool——C++內(nèi)建的基本布爾數(shù)據(jù)類型。operator bool()使用的是C++“運(yùn)算符過載”的一種特殊形式。它允許我們控制自動(dòng)類型轉(zhuǎn)換。如果有一個(gè)名為p的Pair對(duì)象,而且在一個(gè)本來希望是布爾結(jié)果的表達(dá)式中使用,比如if(p){//...,那么編譯器能辨別出它有一個(gè)Pair,而且需要的是個(gè)布爾值,所以自動(dòng)調(diào)用operator bool(),進(jìn)行必要的轉(zhuǎn)換。 接下來的三個(gè)方法屬于常規(guī)編碼,在C++中創(chuàng)建類時(shí)必須用到它們。根據(jù)C++類采用的所謂“經(jīng)典形式”,我們必須定義必要的“原始”構(gòu)建器,以及一個(gè)副本構(gòu)建器和賦值運(yùn)算符——operator=(以及破壞器,用于清除內(nèi)存)。之所以要作這樣的定義,是由于編譯器會(huì)“默默”地調(diào)用它們。在對(duì)象傳入、傳出一個(gè)函數(shù)的時(shí)候,需要調(diào)用副本構(gòu)建器;而在分配對(duì)象時(shí),需要調(diào)用賦值運(yùn)算符。只有真正掌握了副本構(gòu)建器和賦值運(yùn)算符的工作原理,才能在C++里寫出真正“健壯”的類,但這需要需要一個(gè)比較艱苦的過程(注釋⑤)。 ⑤:我的《Thinking in C++》(Prentice-Hall,1995)用了一整章的地方來討論這個(gè)主題。若需更多的幫助,請(qǐng)務(wù)必看看那一章。 只要將一個(gè)對(duì)象按值傳入或傳出函數(shù),就會(huì)自動(dòng)調(diào)用副本構(gòu)建器Pair(const Pair&)。也就是說,對(duì)于準(zhǔn)備為其制作一個(gè)完整副本的那個(gè)對(duì)象,我們不準(zhǔn)備在函數(shù)框架中傳遞它的地址。這并不是Java提供的一個(gè)選項(xiàng),由于我們只能傳遞句柄,所以在Java里沒有所謂的副本構(gòu)建器(如果想制作一個(gè)本地副本,可以“克隆”那個(gè)對(duì)象——使用clone(),參見第12章)。類似地,如果在Java里分配一個(gè)句柄,它會(huì)簡(jiǎn)單地復(fù)制。但C++中的賦值意味著整個(gè)對(duì)象都會(huì)復(fù)制。在副本構(gòu)建器中,我們創(chuàng)建新的存儲(chǔ)空間,并復(fù)制原始數(shù)據(jù)。但對(duì)于賦值運(yùn)算符,我們必須在分配新存儲(chǔ)空間之前釋放老存儲(chǔ)空間。我們要見到的也許是C++類最復(fù)雜的一種情況,但那正是Java的支持者們論證Java比C++簡(jiǎn)單得多的有力證據(jù)。在Java中,我們可以自由傳遞句柄,善后工作則由垃圾收集器負(fù)責(zé),所以可以輕松許多。 但事情并沒有完。Pair類為nm和val使用的是char*,最復(fù)雜的情況主要是圍繞指針展開的。如果用較時(shí)髦的C++ string類來代替char*,事情就要變得簡(jiǎn)單得多(當(dāng)然,并不是所有編譯器都提供了對(duì)string的支持)。那么,Pair的第一部分看起來就象下面這樣: class Pair { string nm; string val; public: Pair() { } Pair(char* name, char* value) { nm = decodeURLString(name); val = decodeURLString(value); } const char* name() const { return nm.c_str(); } const char* value() const { return val.c_str(); } // Test for "emptiness" bool empty() const { return (nm.length() == 0) || (val.length() == 0); } // Automatic type conversion for boolean test: operator bool() const { return (nm.length() != 0) && (val.length() != 0); } (此外,對(duì)這個(gè)類decodeURLString()會(huì)返回一個(gè)string,而不是一個(gè)char*)。我們不必定義副本構(gòu)建器、operator=或者破壞器,因?yàn)榫幾g器已幫我們做了,而且做得非常好。但即使有些事情是自動(dòng)進(jìn)行的,C++程序員也必須了解副本構(gòu)建以及賦值的細(xì)節(jié)。 Pair類剩下的部分由兩個(gè)方法構(gòu)成:decodeURLString()以及一個(gè)“幫助器”方法translateHex()——將由decodeURLString()使用。注意translateHex()并不能防范用戶的惡意輸入,比如“%1H”。分配好足夠的存儲(chǔ)空間后(必須由破壞器釋放),decodeURLString()就會(huì)其中遍歷,將所有“+”都換成一個(gè)空格;將所有十六進(jìn)制代碼(以一個(gè)“%”打頭)換成對(duì)應(yīng)的字符。 CGI_vector用于解析和容納整個(gè)CGI GET命令。它是從STL vector里繼承的,后者例示為容納Pair。C++中的繼承是用一個(gè)冒號(hào)表示,在Java中則要用extends。此外,繼承默認(rèn)為private屬性,所以幾乎肯定需要用到public關(guān)鍵字,就象這樣做的那樣。大家也會(huì)發(fā)現(xiàn)CGI_vector有一個(gè)副本構(gòu)建器以及一個(gè)operator=,但它們都聲明成private。這樣做是為了防止編譯器同步兩個(gè)函數(shù)(如果不自己聲明它們,兩者就會(huì)同步)。但這同時(shí)也禁止了客戶程序員按值或者通過賦值傳遞一個(gè)CGI_vector。 CGI_vector的工作是獲取QUERY_STRING,并把它解析成“名稱/值”對(duì),這需要在Pair的幫助下完成。它首先將字串復(fù)制到本地分配的內(nèi)存,并用常數(shù)指針start跟蹤起始地址(稍后會(huì)在破壞器中用于釋放內(nèi)存)。隨后,它用自己的nextPair()方法將字串解析成原始的“名稱/值”對(duì),各個(gè)對(duì)之間用一個(gè)“=”和“&”符號(hào)分隔。這些對(duì)由nextPair()傳遞給Pair構(gòu)建器,所以nextPair()返回的是一個(gè)Pair對(duì)象。隨后用push_back()將該對(duì)象加入vector。nextPair()遍歷完整個(gè)QUERY_STRING后,會(huì)返回一個(gè)零值。 現(xiàn)在基本工具已定義好,它們可以簡(jiǎn)單地在一個(gè)CGI程序中使用,就象下面這樣: //: Listmgr2.cpp // CGI version of Listmgr.c in C++, which // extracts its input via the GET submission // from the associated applet. Also works as // an ordinary CGI program with HTML forms. #include #include "CGITools.h" const char* dataFile = "list2.txt"; const char* notify = "Bruce@EckelObjects.com"; #undef DEBUG // Similar code as before, except that it looks // for the email name inside of '<>': int inList(FILE* list, const char* emailName) { const int BSIZE = 255; char lbuf[BSIZE]; char emname[BSIZE]; // Put the email name in '<>' so there's no // possibility of a match within another name: sprintf(emname, "<%s>", emailName); // 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(strstr(lbuf, emname) != 0) return 1; } return 0; } void main() { // You MUST print this out, otherwise the // server will not send the response: printf("Content-type: text/plain\n\n"); FILE* list = fopen(dataFile, "a+t"); if(list == 0) { printf("error: could not open database. "); printf("Notify %s", notify); return; } // For a CGI "GET," the server puts the data // in the environment variable QUERY_STRING: CGI_vector query(getenv("QUERY_STRING")); #if defined(DEBUG) // Test: dump all names and values for(int i = 0; i < query.size(); i++) { printf("query[%d].name() = [%s], ", i, query[i].name()); printf("query[%d].value() = [%s]\n", i, query[i].value()); } #endif(DEBUG) Pair name = query[0]; Pair email = query[1]; if(name.empty() || email.empty()) { printf("error: null name or email"); return; } if(inList(list, email.value())) { printf("Already in list: %s", email.value()); return; } // It's not in the list, add it: fseek(list, 0, SEEK_END); fprintf(list, "%s <%s>;\n", name.value(), email.value()); fflush(list); fclose(list); printf("%s <%s> added to list\n", name.value(), email.value()); } ///:~ alreadyInList()函數(shù)與前一個(gè)版本幾乎是完全相同的,只是它假定所有電子函件地址都在一個(gè)“<>”內(nèi)。 在使用GET方法時(shí)(通過在FORM引導(dǎo)命令的METHOD標(biāo)記內(nèi)部設(shè)置,但這在這里由數(shù)據(jù)發(fā)送的方式控制),Web服務(wù)器會(huì)收集位于“?”后面的所有信息,并把它們置入環(huán)境變量QUERY_STRING(查詢字串)里。所以為了讀取那些信息,必須獲得QUERY_STRING的值,這是用標(biāo)準(zhǔn)的C庫(kù)函數(shù)getnv()完成的。在main()中,注意對(duì)QUERY_STRING的解析有多么容易:只需把它傳遞給用于CGI_vector對(duì)象的構(gòu)建器(名為query),剩下的所有工作都會(huì)自動(dòng)進(jìn)行。從這時(shí)開始,我們就可以從query中取出名稱和值,把它們當(dāng)作數(shù)組看待(這是由于operator[]在vector里已經(jīng)過載了)。在調(diào)試代碼中,大家可看到這一切是如何運(yùn)作的;調(diào)試代碼封裝在預(yù)處理器引導(dǎo)命令#if defined(DEBUG)和#endif(DEBUG)之間。 現(xiàn)在,我們迫切需要掌握一些與CGI有關(guān)的東西。CGI程序用兩個(gè)方式之一傳遞它們的輸入:在GET執(zhí)行期間通過QUERY_STRING傳遞(目前用的這種方式),或者在POST期間通過標(biāo)準(zhǔn)輸入。但CGI程序通過標(biāo)準(zhǔn)輸出發(fā)送自己的輸出,這通常是用C程序的printf()命令實(shí)現(xiàn)的。那么這個(gè)輸出到哪里去了呢?它回到了Web服務(wù)器,由服務(wù)器決定該如何處理它。服務(wù)器作出決定的依據(jù)是content-type(內(nèi)容類型)頭數(shù)據(jù)。這意味著假如content-type頭不是它看到的第一件東西,就不知道該如何處理收到的數(shù)據(jù)。因此,我們無(wú)論如何也要使所有CGI程序都從content-type頭開始輸出。 在目前這種情況下,我們希望服務(wù)器將所有信息都直接反饋回客戶程序(亦即我們的程序片,它們正在等候給自己的回復(fù))。信息應(yīng)該原封不動(dòng),所以content-type設(shè)為text/plain(純文本)。一旦服務(wù)器看到這個(gè)頭,就會(huì)將所有字串都直接發(fā)還給客戶。所以每個(gè)字串(三個(gè)用于出錯(cuò)條件,一個(gè)用于成功的加入)都會(huì)返回程序片。 我們用相同的代碼添加電子函件名稱(用戶的姓名)。但在CGI腳本的情況下,并不存在無(wú)限循環(huán)——程序只是簡(jiǎn)單地響應(yīng),然后就中斷。每次有一個(gè)CGI請(qǐng)求抵達(dá)時(shí),程序都會(huì)啟動(dòng),對(duì)那個(gè)請(qǐng)求作出反應(yīng),然后自行關(guān)閉。所以CPU不可能陷入空等待的尷尬境地,只有啟動(dòng)程序和打開文件時(shí)才存在性能上的隱患。Web服務(wù)器對(duì)CGI請(qǐng)求進(jìn)行控制時(shí),它的開銷會(huì)將這種隱患減輕到最低程度。 這種設(shè)計(jì)的另一個(gè)好處是由于Pair和CGI_vector都得到了定義,大多數(shù)工作都幫我們自動(dòng)完成了,所以只需修改main()即可輕松創(chuàng)建自己的CGI程序。盡管小服務(wù)程序(Servlet)最終會(huì)變得越來越流行,但為了創(chuàng)建快速的CGI程序,C++仍然顯得非常方便。 15.6.4 POST的概念 在許多應(yīng)用程序中使用GET都沒有問題。但是,GET要求通過一個(gè)環(huán)境變量將自己的數(shù)據(jù)傳遞給CGI程序。但假如GET字串過長(zhǎng),有些Web服務(wù)器可能用光自己的環(huán)境空間(若字串長(zhǎng)度超過200字符,就應(yīng)開始關(guān)心這方面的問題)。CGI為此提供了一個(gè)解決方案:POST。通過POST,數(shù)據(jù)可以編碼,并按與GET相同的方法連結(jié)起來。但POST利用標(biāo)準(zhǔn)輸入將編碼過后的查詢字串傳遞給CGI程序。我們要做的全部事情就是判斷查詢字串的長(zhǎng)度,而這個(gè)長(zhǎng)度已在環(huán)境變量CONTENT_LENGTH中保存好了。一旦知道了長(zhǎng)度,就可自由分配存儲(chǔ)空間,并從標(biāo)準(zhǔn)輸入中讀入指定數(shù)量的字符。 對(duì)一個(gè)用來控制POST的CGI程序,由CGITools.h提供的Pair和CGI_vector均可不加絲毫改變地使用。下面這段程序揭示了寫這樣的一個(gè)CGI程序有多么簡(jiǎn)單。這個(gè)例子將采用“純”C++,所以studio.h庫(kù)被iostream(IO數(shù)據(jù)流)代替。對(duì)于iostream,我們可以使用兩個(gè)預(yù)先定義好的對(duì)象:cin,用于同標(biāo)準(zhǔn)輸入連接;以及cout,用于同標(biāo)準(zhǔn)輸出連接。有幾個(gè)辦法可從cin中讀入數(shù)據(jù)以及向cout中寫入。但下面這個(gè)程序準(zhǔn)備采用標(biāo)準(zhǔn)方法:用“<<”將信息發(fā)給cout,并用一個(gè)成員函數(shù)(此時(shí)是read())從cin中讀入數(shù)據(jù): //: POSTtest.cpp // CGI_vector works as easily with POST as it // does with GET. Written in "pure" C++. #include #include "CGITools.h" void main() { cout << "Content-type: text/plain\n" << endl; // For a CGI "POST," the server puts the length // of the content string in the environment // variable CONTENT_LENGTH: char* clen = getenv("CONTENT_LENGTH"); if(clen == 0) { cout << "Zero CONTENT_LENGTH" << endl; return; } int len = atoi(clen); char* query_str = new char[len + 1]; cin.read(query_str, len); query_str[len] = '\0'; CGI_vector query(query_str); // Test: dump all names and values for(int i = 0; i < query.size(); i++) cout << "query[" << i << "].name() = [" << query[i].name() << "], " << "query[" << i << "].value() = [" << query[i].value() << "]" << endl; delete query_str; // Release storage } ///:~ getenv()函數(shù)返回指向一個(gè)字串的指針,那個(gè)字串指示著內(nèi)容的長(zhǎng)度。若指針為零,表明CONTENT_LENGTH環(huán)境變量尚未設(shè)置,所以肯定某個(gè)地方出了問題。否則就必須用ANSI C庫(kù)函數(shù)atoi()將字串轉(zhuǎn)換成一個(gè)整數(shù)。這個(gè)長(zhǎng)度將與new一起運(yùn)用,分配足夠的存儲(chǔ)空間,以便容納查詢字串(另加它的空中止符)。隨后為cin()調(diào)用read()。read()函數(shù)需要取得指向目標(biāo)緩沖區(qū)的一個(gè)指針以及要讀入的字節(jié)數(shù)。隨后用空字符(null)中止query_str,指出已經(jīng)抵達(dá)字串的末尾,這就叫作“空中止”。 到這個(gè)時(shí)候,我們得到的查詢字串與GET查詢字串已經(jīng)沒有什么區(qū)別,所以把它傳遞給用于CGI_vector的構(gòu)建器。隨后便和前例一樣,我們可以自由vector內(nèi)不同的字段。 為測(cè)試這個(gè)程序,必須把它編譯到主機(jī)Web服務(wù)器的cgi-bin目錄下。然后就可以寫一個(gè)簡(jiǎn)單的HTML頁(yè)進(jìn)行測(cè)試,就象下面這樣: A test of standard HTML POST Test, uses standard html POST

    Field1:

    Field2:

    Field3:

    Field4:

    Field5:

    Field6:

    填好這個(gè)表單并提交出去以后,會(huì)得到一個(gè)簡(jiǎn)單的文本頁(yè),其中包含了解析出來的結(jié)果。從中可知道CGI程序是否在正常工作。 當(dāng)然,用一個(gè)程序片來提交數(shù)據(jù)顯得更有趣一些。然而,POST數(shù)據(jù)的提交屬于一個(gè)不同的過程。在用常規(guī)方式調(diào)用了CGI程序以后,必須另行建立與服務(wù)器的一個(gè)連接,以便將查詢字串反饋給它。服務(wù)器隨后會(huì)進(jìn)行一番處理,再通過標(biāo)準(zhǔn)輸入將查詢字串反饋回CGI程序。 為建立與服務(wù)器的一個(gè)直接連接,必須取得自己創(chuàng)建的URL,然后調(diào)用openConnection()創(chuàng)建一個(gè)URLConnection。但是,由于URLConnection一般不允許我們把數(shù)據(jù)發(fā)給它,所以必須很可笑地調(diào)用setDoOutput(true)函數(shù),同時(shí)調(diào)用的還包括setDoInput(true)以及setAllowUserInteraction(false)——注釋⑥。最后,可調(diào)用getOutputStream()來創(chuàng)建一個(gè)OutputStream(輸出數(shù)據(jù)流),并把它封裝到一個(gè)DataOutputStream里,以便能按傳統(tǒng)方式同它通信。下面列出的便是一個(gè)用于完成上述工作的程序片,必須在從它的各個(gè)字段里收集了數(shù)據(jù)之后再執(zhí)行它: //: POSTtest.java // An applet that sends its data via a CGI POST import java.awt.*; import java.applet.*; import java.net.*; import java.io.*; public class POSTtest extends Applet { final static int SIZE = 10; Button submit = new Button("Submit"); TextField[] t = new TextField[SIZE]; String query = ""; Label l = new Label(); TextArea ta = new TextArea(15, 60); public void init() { Panel p = new Panel(); p.setLayout(new GridLayout(t.length + 2, 2)); for(int i = 0; i < t.length; i++) { p.add(new Label( "Field " + i + " ", Label.RIGHT)); p.add(t[i] = new TextField(30)); } p.add(l); p.add(submit); add("North", p); add("South", ta); } public boolean action (Event evt, Object arg) { if(evt.target.equals(submit)) { query = ""; ta.setText(""); // Encode the query from the field data: for(int i = 0; i < t.length; i++) query += "Field" + i + "=" + URLEncoder.encode( t[i].getText().trim()) + "&"; query += "submit=Submit"; // Send the name using CGI's POST process: try { URL u = new URL( getDocumentBase(), "cgi-bin/POSTtest"); URLConnection urlc = u.openConnection(); urlc.setDoOutput(true); urlc.setDoInput(true); urlc.setAllowUserInteraction(false); DataOutputStream server = new DataOutputStream( urlc.getOutputStream()); // Send the data server.writeBytes(query); server.close(); // Read and display the response. You // cannot use // getAppletContext().showDocument(u); // to display the results as a Web page! DataInputStream in = new DataInputStream( urlc.getInputStream()); String s; while((s = in.readLine()) != null) { ta.appendText(s + "\n"); } in.close(); } catch (Exception e) { l.setText(e.toString()); } } else return super.action(evt, arg); return true; } } ///:~ ⑥:我不得不說自己并沒有真正理解這兒都發(fā)生了什么事情,這些概念都是從Elliotte Rusty Harold編著的《Java Network Programming》里得來的,該書由O'Reilly于1997年出版。他在書中提到了Java連網(wǎng)函數(shù)庫(kù)中出現(xiàn)的許多令人迷惑的Bug。所以一旦涉足這些領(lǐng)域,事情就不是編寫代碼,然后讓它自己運(yùn)行那么簡(jiǎn)單。一定要警惕潛在的陷阱! 信息發(fā)送到服務(wù)器后,我們調(diào)用getInputStream(),并把返回值封裝到一個(gè)DataInputStream里,以便自己能讀取結(jié)果。要注意的一件事情是結(jié)果以文本行的形式顯示在一個(gè)TextArea(文本區(qū)域)中。為什么不簡(jiǎn)單地使用getAppletContext().showDocument(u)呢?事實(shí)上,這正是那些陷阱中的一個(gè)。上述代碼可以很好地工作,但假如試圖換用showDocument(),幾乎一切都會(huì)停止運(yùn)行。也就是說,showDocument()確實(shí)可以運(yùn)行,但從POSTtest得到的返回結(jié)果是“Zero CONTENT_LENGTH”(內(nèi)容長(zhǎng)度為零)。所以不知道為什么原因,showDocument()阻止了POST查詢向CGI程序的傳遞。我很難判斷這到底是一個(gè)在以后版本里會(huì)修復(fù)的Bug,還是由于我的理解不夠(我看過的書對(duì)此講得都很模糊)。但無(wú)論在哪種情況下,只要能堅(jiān)持在文本區(qū)域里觀看自CGI程序返回的內(nèi)容,上述程序片運(yùn)行時(shí)就沒有問題。 15.7 用JDBC連接數(shù)據(jù)庫(kù) 據(jù)估算,將近一半的軟件開發(fā)都要涉及客戶(機(jī))/服務(wù)器方面的操作。Java為自己保證的一項(xiàng)出色能力就是構(gòu)建與平臺(tái)無(wú)關(guān)的客戶機(jī)/服務(wù)器數(shù)據(jù)庫(kù)應(yīng)用。在Java 1.1中,這一保證通過Java數(shù)據(jù)庫(kù)連接(JDBC)實(shí)現(xiàn)了。 數(shù)據(jù)庫(kù)最主要的一個(gè)問題就是各家公司之間的規(guī)格大戰(zhàn)。確實(shí)存在一種“標(biāo)準(zhǔn)”數(shù)據(jù)庫(kù)語(yǔ)言,即“結(jié)構(gòu)查詢語(yǔ)言”(SQL-92),但通常都必須確切知道自己要和哪家數(shù)據(jù)庫(kù)公司打交道,否則極易出問題,盡管存在所謂的“標(biāo)準(zhǔn)”。JDBC是面向“與平臺(tái)無(wú)關(guān)”設(shè)計(jì)的,所以在編程的時(shí)候不必關(guān)心自己要使用的是什么數(shù)據(jù)庫(kù)產(chǎn)品。然而,從JDBC里仍有可能發(fā)出對(duì)某些數(shù)據(jù)庫(kù)公司專用功能的調(diào)用,所以仍然不可任性妄為。 和Java中的許多API一樣,JDBC也做到了盡量的簡(jiǎn)化。我們發(fā)出的方法調(diào)用對(duì)應(yīng)于從數(shù)據(jù)庫(kù)收集數(shù)據(jù)時(shí)想當(dāng)然的做法:同數(shù)據(jù)庫(kù)連接,創(chuàng)建一個(gè)語(yǔ)句并執(zhí)行查詢,然后處理結(jié)果集。 為實(shí)現(xiàn)這一“與平臺(tái)無(wú)關(guān)”的特點(diǎn),JDBC為我們提供了一個(gè)“驅(qū)動(dòng)程序管理器”,它能動(dòng)態(tài)維護(hù)數(shù)據(jù)庫(kù)查詢所需的所有驅(qū)動(dòng)程序?qū)ο蟆K约偃缫B接由三家公司開發(fā)的不同種類的數(shù)據(jù)庫(kù),就需要三個(gè)單獨(dú)的驅(qū)動(dòng)程序?qū)ο蟆r?qū)動(dòng)程序?qū)ο髸?huì)在裝載時(shí)由“驅(qū)動(dòng)程序管理器”自動(dòng)注冊(cè),并可用Class.forName()強(qiáng)行裝載。 為打開一個(gè)數(shù)據(jù)庫(kù),必須創(chuàng)建一個(gè)“數(shù)據(jù)庫(kù)URL”,它要指定下述三方面的內(nèi)容: (1) 用“jdbc”指出要使用JDBC。 (2) “子協(xié)議”:驅(qū)動(dòng)程序的名字或者一種數(shù)據(jù)庫(kù)連接機(jī)制的名稱。由于JDBC的設(shè)計(jì)從ODBC吸收了許多靈感,所以可以選用的第一種子協(xié)議就是“jdbc-odbc橋”,它用“odbc”關(guān)鍵字即可指定。 (3) 數(shù)據(jù)庫(kù)標(biāo)識(shí)符:隨使用的數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序的不同而變化,但一般都提供了一個(gè)比較符合邏輯的名稱,由數(shù)據(jù)庫(kù)管理軟件映射(對(duì)應(yīng))到保存了數(shù)據(jù)表的一個(gè)物理目錄。為使自己的數(shù)據(jù)庫(kù)標(biāo)識(shí)符具有任何含義,必須用自己的數(shù)據(jù)庫(kù)管理軟件為自己喜歡的名字注冊(cè)(注冊(cè)的具體過程又隨運(yùn)行平臺(tái)的不同而變化)。 所有這些信息都統(tǒng)一編譯到一個(gè)字串里,即“數(shù)據(jù)庫(kù)URL”。舉個(gè)例子來說,若想通過ODBC子協(xié)議同一個(gè)標(biāo)識(shí)為“people”的數(shù)據(jù)庫(kù)連接,相應(yīng)的數(shù)據(jù)庫(kù)URL可設(shè)為: String dbUrl = "jdbc:odbc:people" 如果通過一個(gè)網(wǎng)絡(luò)連接,數(shù)據(jù)庫(kù)URL也需要包含對(duì)遠(yuǎn)程機(jī)器進(jìn)行標(biāo)識(shí)的信息。 準(zhǔn)備好同數(shù)據(jù)庫(kù)連接后,可調(diào)用靜態(tài)方法DriverManager.getConnection(),將數(shù)據(jù)庫(kù)的URL以及進(jìn)入那個(gè)數(shù)據(jù)庫(kù)所需的用戶名密碼傳遞給它。得到的返回結(jié)果是一個(gè)Connection對(duì)象,利用它即可查詢和操縱數(shù)據(jù)庫(kù)。 下面這個(gè)例子將打開一個(gè)聯(lián)絡(luò)信息數(shù)據(jù)庫(kù),并根據(jù)命令行提供的參數(shù)查詢一個(gè)人的姓(Last Name)。它只選擇那些有E-mail地址的人的名字,然后列印出符合查詢條件的所有人: //: Lookup.java // Looks up email addresses in a // local database using JDBC import java.sql.*; public class Lookup { public static void main(String[] args) { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; try { // Load the driver (registers itself) Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); Statement s = c.createStatement(); // SQL code: ResultSet r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); while(r.next()) { // Capitalization doesn't matter: System.out.println( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") ); } s.close(); // Also closes ResultSet } catch(Exception e) { e.printStackTrace(); } } } ///:~ 可以看到,數(shù)據(jù)庫(kù)URL的創(chuàng)建過程與我們前面講述的完全一樣。在該例中,數(shù)據(jù)庫(kù)未設(shè)密碼保護(hù),所以用戶名和密碼都是空串。 用DriverManager.getConnection()建好連接后,接下來可根據(jù)結(jié)果Connection對(duì)象創(chuàng)建一個(gè)Statement(語(yǔ)句)對(duì)象,這是用createStatement()方法實(shí)現(xiàn)的。根據(jù)結(jié)果Statement,我們可調(diào)用executeQuery(),向其傳遞包含了SQL-92標(biāo)準(zhǔn)SQL語(yǔ)句的一個(gè)字串(不久就會(huì)看到如何自動(dòng)創(chuàng)建這類語(yǔ)句,所以沒必要在這里知道關(guān)于SQL更多的東西)。 executeQuery()方法會(huì)返回一個(gè)ResultSet(結(jié)果集)對(duì)象,它與繼承器非常相似:next()方法將繼承器移至語(yǔ)句中的下一條記錄;如果已抵達(dá)結(jié)果集的末尾,則返回null。我們肯定能從executeQuery()返回一個(gè)ResultSet對(duì)象,即使查詢結(jié)果是個(gè)空集(也就是說,不會(huì)產(chǎn)生一個(gè)違例)。注意在試圖讀取任何記錄數(shù)據(jù)之前,都必須調(diào)用一次next()。若結(jié)果集為空,那么對(duì)next()的這個(gè)首次調(diào)用就會(huì)返回false。對(duì)于結(jié)果集中的每條記錄,都可將字段名作為字串使用(當(dāng)然還有其他方法),從而選擇不同的字段。另外要注意的是字段名的大小寫是無(wú)關(guān)緊要的——SQL數(shù)據(jù)庫(kù)不在乎這個(gè)問題。為決定返回的類型,可調(diào)用getString(),getFloat()等等。到這個(gè)時(shí)候,我們已經(jīng)用Java的原始格式得到了自己的數(shù)據(jù)庫(kù)數(shù)據(jù),接下去可用Java代碼做自己想做的任何事情了。 15.7.1 讓示例運(yùn)行起來 就JDBC來說,代碼本身是很容易理解的。最令人迷惑的部分是如何使它在自己特定的系統(tǒng)上運(yùn)行起來。之所以會(huì)感到迷惑,是由于它要求我們掌握如何才能使JDBC驅(qū)動(dòng)程序正確裝載,以及如何用我們的數(shù)據(jù)庫(kù)管理軟件來設(shè)置一個(gè)數(shù)據(jù)庫(kù)。 當(dāng)然,具體的操作過程在不同的機(jī)器上也會(huì)有所區(qū)別。但這兒提供的在32位Windows環(huán)境下操作過程可有效幫助大家理解在其他平臺(tái)上的操作。 1. 步驟1:尋找JDBC驅(qū)動(dòng)程序 上述程序包含了下面這條語(yǔ)句: Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); 這似乎暗示著一個(gè)目錄結(jié)構(gòu),但大家不要被它蒙騙了。在我手上這個(gè)JDK 1.1安裝版本中,根本不存在叫作JdbcOdbcDriver.class的一個(gè)文件。所以假如在看了這個(gè)例子后去尋找它,那么必然會(huì)徒勞而返。另一些人提供的例子使用的是一個(gè)假名字,如“myDriver.ClassName”,但人們從字面上得不到任何幫助。事實(shí)上,上述用于裝載jdbc-odbc驅(qū)動(dòng)程序(實(shí)際是與JDK 1.1配套提供的唯一驅(qū)動(dòng))的語(yǔ)句在聯(lián)機(jī)文檔的多處地方均有出現(xiàn)(特別是在一個(gè)標(biāo)記為“JDBC-ODBC Bridge Driver”的頁(yè)內(nèi))。若上面的裝載語(yǔ)句不能工作,那么它的名字可能已隨著Java新版本的發(fā)布而改變了;此時(shí)應(yīng)到聯(lián)機(jī)文檔里尋找新的表述方式。 若裝載語(yǔ)句出錯(cuò),會(huì)在這個(gè)時(shí)候得到一個(gè)違例。為了檢驗(yàn)驅(qū)動(dòng)程序裝載語(yǔ)句是不是能正常工作,請(qǐng)將該語(yǔ)句后面直到catch從句之間的代碼暫時(shí)設(shè)為注釋。如果程序運(yùn)行時(shí)未出現(xiàn)違例,表明驅(qū)動(dòng)程序的裝載是正確的。 2. 步驟2:配置數(shù)據(jù)庫(kù) 同樣地,我們只限于在32位Windows環(huán)境中工作;您可能需要研究一下自己的操作系統(tǒng),找出適合自己平臺(tái)的配置方法。 首先打開控制面板。其中可能有兩個(gè)圖標(biāo)都含有“ODBC”字樣,必須選擇那個(gè)“32位ODBC”,因?yàn)榱硪粋€(gè)是為了保持與16位軟件的向后兼容而設(shè)置的,和JDBC混用沒有任何結(jié)果。雙擊“32位ODBC”圖標(biāo)后,看到的應(yīng)該是一個(gè)卡片式對(duì)話框,上面一排有多個(gè)卡片標(biāo)簽,其中包括“用戶DSN”、“系統(tǒng)DSN”、“文件DSN”等等。其中,“DSN”代表“數(shù)據(jù)源名稱”(Data Source Name)。它們都與JDBC-ODBC橋有關(guān),但設(shè)置數(shù)據(jù)庫(kù)時(shí)唯一重要的地方“系統(tǒng)DSN”。盡管如此,由于需要測(cè)試自己的配置以及創(chuàng)建查詢,所以也需要在“文件DSN”中設(shè)置自己的數(shù)據(jù)庫(kù)。這樣便可讓Microsoft Query工具(與Microsoft Office配套提供)正確地找到數(shù)據(jù)庫(kù)。注意一些軟件公司也設(shè)計(jì)了自己的查詢工具。 最有趣的數(shù)據(jù)庫(kù)是我們已經(jīng)使用過的一個(gè)。標(biāo)準(zhǔn)ODBC支持多種文件格式,其中包括由不同公司專用的一些格式,如dBASE。然而,它也包括了簡(jiǎn)單的“逗號(hào)分隔ASCII”格式,它幾乎是每種數(shù)據(jù)工具都能夠生成的。就目前的例子來說,我只選擇自己的“people”數(shù)據(jù)庫(kù)。這是我多年來一直在維護(hù)的一個(gè)數(shù)據(jù)庫(kù),中間使用了各種聯(lián)絡(luò)管理工具。我把它導(dǎo)出成為一個(gè)逗號(hào)分隔的ASCII文件(一般有個(gè).csv擴(kuò)展名,用Outlook Express導(dǎo)出通信簿時(shí)亦可選用同樣的文件格式)。在“文件DSN”區(qū)域,我按下“添加”按鈕,選擇用于控制逗號(hào)分隔ASCII文件的文本驅(qū)動(dòng)程序(Microsoft Text Driver),然后撤消對(duì)“使用當(dāng)前目錄”的選擇,以便導(dǎo)出數(shù)據(jù)文件時(shí)可以自行指定目錄。 大家會(huì)注意到在進(jìn)行這些工作的時(shí)候,并沒有實(shí)際指定一個(gè)文件,只是一個(gè)目錄。那是因?yàn)閿?shù)據(jù)庫(kù)通常是由某個(gè)目錄下的一系列文件構(gòu)成的(盡管也可能采用其他形式)。每個(gè)文件一般都包含了單個(gè)“數(shù)據(jù)表”,而且SQL語(yǔ)句可以產(chǎn)生從數(shù)據(jù)庫(kù)中多個(gè)表摘取出來的結(jié)果(這叫作“聯(lián)合”,或者join)只包含了單張表的數(shù)據(jù)庫(kù)(就象目前這個(gè))通常叫作“平面文件數(shù)據(jù)庫(kù)”。對(duì)于大多數(shù)問題,如果已經(jīng)超過了簡(jiǎn)單的數(shù)據(jù)存儲(chǔ)與獲取力所能及的范圍,那么必須使用多個(gè)數(shù)據(jù)表。通過“聯(lián)合”,從而獲得希望的結(jié)果。我們把這些叫作“關(guān)系型”數(shù)據(jù)庫(kù)。 3. 步驟3:測(cè)試配置 為了對(duì)配置進(jìn)行測(cè)試,需用一種方式核實(shí)數(shù)據(jù)庫(kù)是否可由查詢它的一個(gè)程序“見到”。當(dāng)然,可以簡(jiǎn)單地運(yùn)行上述的JDBC示范程序,并加入下述語(yǔ)句: Connection c = DriverManager.getConnection( dbUrl, user, password); 若擲出一個(gè)違例,表明你的配置有誤。 然而,此時(shí)很有必要使用一個(gè)自動(dòng)化的查詢生成工具。我使用的是與Microsoft Office配套提供的Microsoft Query,但你完全可以自行選擇一個(gè)。查詢工具必須知道數(shù)據(jù)庫(kù)在什么地方,而Microsoft Query要求我進(jìn)入ODBC Administrator的“文件DSN”卡片,并在那里新添一個(gè)條目。同樣指定文本驅(qū)動(dòng)程序以及保存數(shù)據(jù)庫(kù)的目錄。雖然可將這個(gè)條目命名為自己喜歡的任何東西,但最好還是使用與“系統(tǒng)DSN”中相同的名字。 做完這些工作后,再用查詢工具創(chuàng)建一個(gè)新查詢時(shí),便會(huì)發(fā)現(xiàn)自己的數(shù)據(jù)庫(kù)可以使用了。 4. 步驟4:建立自己的SQL查詢 我用Microsoft Query創(chuàng)建的查詢不僅指出目標(biāo)數(shù)據(jù)庫(kù)存在且次序良好,也會(huì)自動(dòng)生成SQL代碼,以便將其插入我自己的Java程序。我希望這個(gè)查詢能夠檢查記錄中是否存在與啟動(dòng)Java程序時(shí)在命令行鍵入的相同的“姓”(Last Name)。所以作為一個(gè)起點(diǎn),我搜索自己的姓“Eckel”。另外,我希望只顯示出有對(duì)應(yīng)E-mail地址的那些名字。創(chuàng)建這個(gè)查詢的步驟如下: (1) 啟動(dòng)一個(gè)新查詢,并使用查詢向?qū)В≦uery Wizard)。選擇“people”數(shù)據(jù)庫(kù)(等價(jià)于用適應(yīng)的數(shù)據(jù)庫(kù)URL打開數(shù)據(jù)庫(kù)連接)。 (2) 選擇數(shù)據(jù)庫(kù)中的“people”表。從這張數(shù)據(jù)表中,選擇FIRST,LAST和EMAIL列。 (3) 在“Filter Data”(過濾器數(shù)據(jù)庫(kù))下,選擇LAST,并選擇“equals”(等于),加上參數(shù)Eckel。點(diǎn)選“And”單選鈕。 (4) 選擇EMAIL,并選中“Is not Null”(不為空)。 (5) 在“Sort By”下,選擇FIRST。 查詢結(jié)果會(huì)向我們展示出是否能得到自己希望的東西。 現(xiàn)在可以按下SQL按鈕。不需要我們?nèi)魏畏矫娴慕槿耄_的SQL代碼會(huì)立即彈現(xiàn)出來,以便我們粘貼和復(fù)制。對(duì)于這個(gè)查詢,相應(yīng)的SQL代碼如下: SELECT people.FIRST, people.LAST, people.EMAIL FROM people.csv people WHERE (people.LAST='Eckel') AND (people.EMAIL Is Not Null) ORDER BY people.FIRST 若查詢比較復(fù)雜,手工編碼極易出錯(cuò)。但利用一個(gè)查詢工具,就可以交互式地測(cè)試自己的查詢,并自動(dòng)獲得正確的代碼。事實(shí)上,親手為這些事情編碼是難以讓人接受的。 5. 步驟5:在自己的查詢中修改和粘貼 我們注意到上述代碼與程序中使用的代碼是有所區(qū)別的。那是由于查詢工具對(duì)所有名字都進(jìn)行了限定,即便涉及的僅有一個(gè)數(shù)據(jù)表(若真的涉及多個(gè)數(shù)據(jù)表,這種限定可避免來自不同表的同名數(shù)據(jù)列發(fā)生沖突)。由于這個(gè)查詢只需要用到一個(gè)數(shù)據(jù)表,所以可考慮從大多數(shù)名字中刪除“people”限定符,就象下面這樣: SELECT FIRST, LAST, EMAIL FROM people.csv people WHERE (LAST='Eckel') AND (EMAIL Is Not Null) ORDER BY FIRST 此外,我們不希望“硬編碼”這個(gè)程序,從而只能查找一個(gè)特定的名字。相反,它應(yīng)該能查找我們?cè)诿钚袆?dòng)態(tài)提供的一個(gè)名字。所以還要進(jìn)行必要的修改,并將SQL語(yǔ)句轉(zhuǎn)換成一個(gè)動(dòng)態(tài)生成的字串。如下所示: "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + "(LAST='" + args[0] + "') " + " AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); SQL還有一種方式可將名字插入一個(gè)查詢,名為“程序”(Procedures),它的速度非常快。但對(duì)于我們的大多數(shù)實(shí)驗(yàn)性數(shù)據(jù)庫(kù)操作,以及一些初級(jí)應(yīng)用,用Java構(gòu)建查詢字串已經(jīng)很不錯(cuò)了。 從這個(gè)例子可以看出,利用目前找得到的工具——特別是查詢構(gòu)建工具——涉及SQL及JDBC的數(shù)據(jù)庫(kù)編程是非常簡(jiǎn)單和直觀的。 15.7.2 查找程序的GUI版本 最好的方法是讓查找程序一直保持運(yùn)行,要查找什么東西時(shí)只需簡(jiǎn)單地切換到它,并鍵入要查找的名字即可。下面這個(gè)程序?qū)⒉檎页绦蜃鳛橐粋€(gè)“application/applet”創(chuàng)建,且添加了名字自動(dòng)填寫功能,所以不必鍵入完整的姓,即可看到數(shù)據(jù): //: VLookup.java // GUI version of Lookup.java import java.awt.*; import java.awt.event.*; import java.applet.*; import java.sql.*; public class VLookup extends Applet { String dbUrl = "jdbc:odbc:people"; String user = ""; String password = ""; Statement s; TextField searchFor = new TextField(20); Label completion = new Label(" "); TextArea results = new TextArea(40, 20); public void init() { searchFor.addTextListener(new SearchForL()); Panel p = new Panel(); p.add(new Label("Last name to search for:")); p.add(searchFor); p.add(completion); setLayout(new BorderLayout()); add(p, BorderLayout.NORTH); add(results, BorderLayout.CENTER); try { // Load the driver (registers itself) Class.forName( "sun.jdbc.odbc.JdbcOdbcDriver"); Connection c = DriverManager.getConnection( dbUrl, user, password); s = c.createStatement(); } catch(Exception e) { results.setText(e.getMessage()); } } class SearchForL implements TextListener { public void textValueChanged(TextEvent te) { ResultSet r; if(searchFor.getText().length() == 0) { completion.setText(""); results.setText(""); return; } try { // Name completion: r = s.executeQuery( "SELECT LAST FROM people.csv people " + "WHERE (LAST Like '" + searchFor.getText() + "%') ORDER BY LAST"); if(r.next()) completion.setText( r.getString("last")); r = s.executeQuery( "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE (LAST='" + completion.getText() + "') AND (EMAIL Is Not Null) " + "ORDER BY FIRST"); } catch(Exception e) { results.setText( searchFor.getText() + "\n"); results.append(e.getMessage()); return; } results.setText(""); try { while(r.next()) { results.append( r.getString("Last") + ", " + r.getString("fIRST") + ": " + r.getString("EMAIL") + "\n"); } } catch(Exception e) { results.setText(e.getMessage()); } } } public static void main(String[] args) { VLookup applet = new VLookup(); Frame aFrame = new Frame("Email lookup"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(500,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~ 數(shù)據(jù)庫(kù)的許多邏輯都是相同的,但大家可看到這里添加了一個(gè)TextListener,用于監(jiān)視在TextField(文本字段)的輸入。所以只要鍵入一個(gè)新字符,它首先就會(huì)試著查找數(shù)據(jù)庫(kù)中的“姓”,并顯示出與當(dāng)前輸入相符的第一條記錄(將其置入completion Label,并用它作為要查找的文本)。因此,只要我們鍵入了足夠的字符,使程序能找到與之相符的唯一一條記錄,就可以停手了。 15.7.3 JDBC API為何如何復(fù)雜 閱覽JDBC的聯(lián)機(jī)幫助文檔時(shí),我們往往會(huì)產(chǎn)生畏難情緒。特別是DatabaseMetaData接口——與Java中看到的大多數(shù)接口相反,它的體積顯得非常龐大——存在著數(shù)量眾多的方法,比如dataDefinitionCausesTransactionCommit(),getMaxColumnNameLength(),getMaxStatementLength(),storesMixedCaseQuotedIdentifiers(),supportsANSI92IntermediateSQL(),supportsLimitedOuterJoins()等等。它們有這兒有什么意義嗎? 正如早先指出的那樣,數(shù)據(jù)庫(kù)起初一直處于一種混亂狀態(tài)。這主要是由于各種數(shù)據(jù)庫(kù)應(yīng)用提出的要求造成的,所以數(shù)據(jù)庫(kù)工具顯得非常“強(qiáng)大”——換言之,“龐大”。只是近幾年才涌現(xiàn)出了SQL的通用語(yǔ)言(常用的還有其他許多數(shù)據(jù)庫(kù)語(yǔ)言)。但即便象SQL這樣的“標(biāo)準(zhǔn)”,也存在無(wú)數(shù)的變種,所以JDBC必須提供一個(gè)巨大的DatabaseMetaData接口,使我們的代碼能真正利用當(dāng)前要連接的一種“標(biāo)準(zhǔn)”SQL數(shù)據(jù)庫(kù)的能力。簡(jiǎn)言之,我們可編寫出簡(jiǎn)單的、能移植的SQL。但如果想優(yōu)化代碼的執(zhí)行速度,那么為了適應(yīng)不同數(shù)據(jù)庫(kù)類型的特點(diǎn),我們的編寫代碼的麻煩就大了。 當(dāng)然,這并不是Java的缺陷。數(shù)據(jù)庫(kù)產(chǎn)品之間的差異是我們和JDBC都要面對(duì)的一個(gè)現(xiàn)實(shí)。但是,如果能編寫通用的查詢,而不必太關(guān)心性能,那么事情就要簡(jiǎn)單得多。即使必須對(duì)性能作一番調(diào)整,只要知道最終面向的平臺(tái),也不必針對(duì)每一種情況都編寫不同的優(yōu)化代碼。 在Sun發(fā)布的Java 1.1產(chǎn)品中,配套提供了一系列電子文檔,其中有對(duì)JDBC更全面的介紹。此外,在由Hamilton Cattel和Fisher編著、Addison-Wesley于1997年出版的《JDBC Database Access with Java》中,也提供了有關(guān)這一主題的許多有用資料。同時(shí),書店里也經(jīng)常出現(xiàn)一些有關(guān)JDBC的新書。 15.8 遠(yuǎn)程方法 為通過網(wǎng)絡(luò)執(zhí)行其他機(jī)器上的代碼,傳統(tǒng)的方法不僅難以學(xué)習(xí)和掌握,也極易出錯(cuò)。思考這個(gè)問題最佳的方式是:某些對(duì)象正好位于另一臺(tái)機(jī)器,我們可向它們發(fā)送一條消息,并獲得返回結(jié)果,就象那些對(duì)象位于自己的本地機(jī)器一樣。Java 1.1的“遠(yuǎn)程方法調(diào)用”(RMI)采用的正是這種抽象。本節(jié)將引導(dǎo)大家經(jīng)歷一些必要的步驟,創(chuàng)建自己的RMI對(duì)象。 15.8.1 遠(yuǎn)程接口概念 RMI對(duì)接口有著強(qiáng)烈的依賴。在需要?jiǎng)?chuàng)建一個(gè)遠(yuǎn)程對(duì)象的時(shí)候,我們通過傳遞一個(gè)接口來隱藏基層的實(shí)施細(xì)節(jié)。所以客戶得到遠(yuǎn)程對(duì)象的一個(gè)句柄時(shí),它們真正得到的是接口句柄。這個(gè)句柄正好同一些本地的根代碼連接,由后者負(fù)責(zé)通過網(wǎng)絡(luò)通信。但我們并不關(guān)心這些事情,只需通過自己的接口句柄發(fā)送消息即可。 創(chuàng)建一個(gè)遠(yuǎn)程接口時(shí),必須遵守下列規(guī)則: (1) 遠(yuǎn)程接口必須為public屬性(不能有“包訪問”;也就是說,它不能是“友好的”)。否則,一旦客戶試圖裝載一個(gè)實(shí)現(xiàn)了遠(yuǎn)程接口的遠(yuǎn)程對(duì)象,就會(huì)得到一個(gè)錯(cuò)誤。 (2) 遠(yuǎn)程接口必須擴(kuò)展接口java.rmi.Remote。 (3) 除與應(yīng)用程序本身有關(guān)的違例之外,遠(yuǎn)程接口中的每個(gè)方法都必須在自己的throws從句中聲明java.rmi.RemoteException。 (4) 作為參數(shù)或返回值傳遞的一個(gè)遠(yuǎn)程對(duì)象(不管是直接的,還是在本地對(duì)象中嵌入)必須聲明為遠(yuǎn)程接口,不可聲明為實(shí)施類。 下面是一個(gè)簡(jiǎn)單的遠(yuǎn)程接口示例,它代表的是一個(gè)精確計(jì)時(shí)服務(wù): //: PerfectTimeI.java // The PerfectTime remote interface package c15.ptime; import java.rmi.*; interface PerfectTimeI extends Remote { long getPerfectTime() throws RemoteException; } ///:~ 它表面上與其他接口是類似的,只是對(duì)Remote進(jìn)行了擴(kuò)展,而且它的所有方法都會(huì)“擲”出RemoteException(遠(yuǎn)程違例)。記住接口和它所有的方法都是public的。 15.8.2 遠(yuǎn)程接口的實(shí)施 服務(wù)器必須包含一個(gè)擴(kuò)展了UnicastRemoteObject的類,并實(shí)現(xiàn)遠(yuǎn)程接口。這個(gè)類也可以含有附加的方法,但客戶只能使用遠(yuǎn)程接口中的方法。這是顯然的,因?yàn)榭蛻舻玫降闹皇侵赶蚪涌诘囊粋€(gè)句柄,而非實(shí)現(xiàn)它的那個(gè)類。 必須為遠(yuǎn)程對(duì)象明確定義構(gòu)建器,即使只準(zhǔn)備定義一個(gè)默認(rèn)構(gòu)建器,用它調(diào)用基礎(chǔ)類構(gòu)建器。必須把它明確地編寫出來,因?yàn)樗仨殹皵S”出RemoteException違例。 下面列出遠(yuǎn)程接口PerfectTime的實(shí)施過程: //: PerfectTime.java // The implementation of the PerfectTime // remote object package c15.ptime; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.net.*; public class PerfectTime extends UnicastRemoteObject implements PerfectTimeI { // Implementation of the interface: public long getPerfectTime() throws RemoteException { return System.currentTimeMillis(); } // Must implement constructor to throw // RemoteException: public PerfectTime() throws RemoteException { // super(); // Called automatically } // Registration for RMI serving: public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTime pt = new PerfectTime(); Naming.bind( "http://colossus:2005/PerfectTime", pt); System.out.println("Ready to do time"); } catch(Exception e) { e.printStackTrace(); } } } ///:~ 在這里,main()控制著設(shè)置服務(wù)器的全部細(xì)節(jié)。保存RMI對(duì)象時(shí),必須在程序的某個(gè)地方采取下述操作: (1) 創(chuàng)建和安裝一個(gè)安全管理器,令其支持RMI。作為Java發(fā)行包的一部分,適用于RMI唯一一個(gè)是RMISecurityManager。 (2) 創(chuàng)建遠(yuǎn)程對(duì)象的一個(gè)或多個(gè)實(shí)例。在這里,大家可看到創(chuàng)建的是PerfectTime對(duì)象。 (3) 向RMI遠(yuǎn)程對(duì)象注冊(cè)表注冊(cè)至少一個(gè)遠(yuǎn)程對(duì)象。一個(gè)遠(yuǎn)程對(duì)象擁有的方法可生成指向其他遠(yuǎn)程對(duì)象的句柄。這樣一來,客戶只需到注冊(cè)表里訪問一次,得到第一個(gè)遠(yuǎn)程對(duì)象即可。 1. 設(shè)置注冊(cè)表 在這兒,大家可看到對(duì)靜態(tài)方法Naming.bind()的一個(gè)調(diào)用。然而,這個(gè)調(diào)用要求注冊(cè)表作為計(jì)算機(jī)上的一個(gè)獨(dú)立進(jìn)程運(yùn)行。注冊(cè)表服務(wù)器的名字是rmiregistry。在32位Windows環(huán)境中,可使用: start rmiregistry 令其在后臺(tái)運(yùn)行。在Unix中,使用: rmiregistry & 和許多網(wǎng)絡(luò)程序一樣,rmiregistry位于機(jī)器啟動(dòng)它所在的某個(gè)IP地址處,但它也必須監(jiān)視一個(gè)端口。如果象上面那樣調(diào)用rmiregistry,不使用參數(shù),注冊(cè)表的端口就會(huì)默認(rèn)為1099。若希望它位于其他某個(gè)端口,只需在命令行添加一個(gè)參數(shù),指定那個(gè)端口編號(hào)即可。對(duì)這個(gè)例子來說,端口將位于2005,所以rmiregistry應(yīng)該象下面這樣啟動(dòng)(對(duì)于32位Windows): start rmiregistry 2005 對(duì)于Unix,則使用下述命令: rmiregistry 2005 & 與端口有關(guān)的信息必須傳送給bind()命令,同時(shí)傳送的還有注冊(cè)表所在的那臺(tái)機(jī)器的IP地址。但假若我們想在本地測(cè)試RMI程序,就象本章的網(wǎng)絡(luò)程序一直測(cè)試的那樣,這樣做就會(huì)帶來問題。在JDK 1.1.1版本中,存在著下述兩方面的問題(注釋⑦): (1) localhost不能隨RMI工作。所以為了在單獨(dú)一臺(tái)機(jī)器上完成對(duì)RMI的測(cè)試,必須提供機(jī)器的名字。為了在32位Windows環(huán)境中調(diào)查自己機(jī)器的名字,可進(jìn)入控制面板,選擇“網(wǎng)絡(luò)”,選擇“標(biāo)識(shí)”卡片,其中列出了計(jì)算機(jī)的名字。就我自己的情況來說,我的機(jī)器叫作“Colossus”(因?yàn)槲矣脦讉€(gè)大容量的硬盤保存各種不同的開發(fā)系統(tǒng)——Clossus是“巨人”的意思)。似乎大寫形式會(huì)被忽略。 (2) 除非計(jì)算機(jī)有一個(gè)活動(dòng)的TCP/IP連接,否則RMI不能工作,即使所有組件都只需要在本地機(jī)器里互相通信。這意味著在試圖運(yùn)行程序之前,必須連接到自己的ISP(因特網(wǎng)服務(wù)提供者),否則會(huì)得到一些含義模糊的違例消息。 ⑦:為找出這些信息,我不知損傷了多少個(gè)腦細(xì)胞。 考慮到這些因素,bind()命令變成了下面這個(gè)樣子: Naming.bind("http://colossus:2005/PerfectTime", pt); 若使用默認(rèn)端口1099,就沒有必要指定一個(gè)端口,所以可以使用: Naming.bind("http://colossus/PerfectTime", pt); 在JDK未來的版本中(1.1之后),一旦改正了localhost的問題,就能正常地進(jìn)行本地測(cè)試,去掉IP地址,只使用標(biāo)識(shí)符: Naming.bind("PerfectTime", pt); 服務(wù)名是任意的;它在這里正好為PerfectTime,和類名一樣,但你可以根據(jù)情況任意修改。最重要的是確保它在注冊(cè)表里是個(gè)獨(dú)一無(wú)二的名字,以便客戶正常地獲取遠(yuǎn)程對(duì)象。若這個(gè)名字已在注冊(cè)表里了,就會(huì)得到一個(gè)AlreadyBoundException違例。為防止這個(gè)問題,可考慮堅(jiān)持使用rebind(),放棄bind()。這是由于rebind()要么會(huì)添加一個(gè)新條目,要么將同名的條目替換掉。 盡管main()退出,我們的對(duì)象已經(jīng)創(chuàng)建并注冊(cè),所以會(huì)由注冊(cè)表一直保持活動(dòng)狀態(tài),等候客戶到達(dá)并發(fā)出對(duì)它的請(qǐng)求。只要rmiregistry處于運(yùn)行狀態(tài),而且我們沒有為名字調(diào)用Naming.unbind()方法,對(duì)象就肯定位于那個(gè)地方。考慮到這個(gè)原因,在我們?cè)O(shè)計(jì)自己的代碼時(shí),需要先關(guān)閉rmiregistry,并在編譯遠(yuǎn)程對(duì)象的一個(gè)新版本時(shí)重新啟動(dòng)它。 并不一定要將rmiregistry作為一個(gè)外部進(jìn)程啟動(dòng)。若事前知道自己的是要求用以注冊(cè)表的唯一一個(gè)應(yīng)用,就可在程序內(nèi)部啟動(dòng)它,使用下述代碼: LocateRegistry.createRegistry(2005); 和前面一樣,2005代表我們?cè)谶@個(gè)例子里選用的端口號(hào)。這等價(jià)于在命令行執(zhí)行rmiregistry 2005。但在設(shè)計(jì)RMI代碼時(shí),這種做法往往顯得更加方便,因?yàn)樗∠藛?dòng)和中止注冊(cè)表所需的額外步驟。一旦執(zhí)行完這個(gè)代碼,就可象以前一樣使用Naming進(jìn)行“綁定”——bind()。 15.8.3 創(chuàng)建根與干 若編譯和運(yùn)行PerfectTime.java,即使rmiregistry正確運(yùn)行,它也無(wú)法工作。這是由于RMI的框架尚未就位。首先必須創(chuàng)建根和干,以便提供網(wǎng)絡(luò)連接操作,并使我們將遠(yuǎn)程對(duì)象偽裝成自己機(jī)器內(nèi)的某個(gè)本地對(duì)象。 所有這些幕后的工作都是相當(dāng)復(fù)雜的。我們從遠(yuǎn)程對(duì)象傳入、傳出的任何對(duì)象都必須“implement Serializable”(如果想傳遞遠(yuǎn)程引用,而非整個(gè)對(duì)象,對(duì)象的參數(shù)就可以“implement Remote”)。因此可以想象,當(dāng)根和干通過網(wǎng)絡(luò)“匯集”所有參數(shù)并返回結(jié)果的時(shí)候,會(huì)自動(dòng)進(jìn)行序列化以及數(shù)據(jù)的重新裝配。幸運(yùn)的是,我們根本沒必要了解這些方面的任何細(xì)節(jié),但根和干卻是必須創(chuàng)建的。一個(gè)簡(jiǎn)單的過程如下:在編譯好的代碼中調(diào)用rmic,它會(huì)創(chuàng)建必需的一些文件。所以唯一要做的事情就是為編譯過程新添一個(gè)步驟。 然而,rmic工具與特定的包和類路徑有很大的關(guān)聯(lián)。PerfectTime.java位于包c(diǎn)15.Ptime中,即使我們調(diào)用與PerfectTime.class同一目錄內(nèi)的rmic,rmic都無(wú)法找到文件。這是由于它搜索的是類路徑。因此,我們必須同時(shí)指定類路徑,就象下面這樣: rmic c15.PTime.PerfectTime 執(zhí)行這個(gè)命令時(shí),并不一定非要在包含了PerfectTime.class的目錄中,但結(jié)果會(huì)置于當(dāng)前目錄。 若rmic成功運(yùn)行,目錄里就會(huì)多出兩個(gè)新類: PerfectTime_Stub.class PerfectTime_Skel.class 它們分別對(duì)應(yīng)根(Stub)和干(Skeleton)。現(xiàn)在,我們已準(zhǔn)備好讓服務(wù)器與客戶互相溝通了。 15.8.4 使用遠(yuǎn)程對(duì)象 RMI全部的宗旨就是盡可能簡(jiǎn)化遠(yuǎn)程對(duì)象的使用。我們?cè)诳蛻舫绦蛑幸龅奈ㄒ灰患~外的事情就是查找并從服務(wù)器取回遠(yuǎn)程接口。自此以后,剩下的事情就是普通的Java編程:將消息發(fā)給對(duì)象。下面是使用PerfectTime的程序: //: DisplayPerfectTime.java // Uses remote object PerfectTime package c15.ptime; import java.rmi.*; import java.rmi.registry.*; public class DisplayPerfectTime { public static void main(String[] args) { System.setSecurityManager( new RMISecurityManager()); try { PerfectTimeI t = (PerfectTimeI)Naming.lookup( "http://colossus:2005/PerfectTime"); for(int i = 0; i < 10; i++) System.out.println("Perfect time = " + t.getPerfectTime()); } catch(Exception e) { e.printStackTrace(); } } } ///:~ ID字串與那個(gè)用Naming注冊(cè)對(duì)象的那個(gè)字串是相同的,第一部分指出了URL和端口號(hào)。由于我們準(zhǔn)備使用一個(gè)URL,所以也可以指定因特網(wǎng)上的一臺(tái)機(jī)器。 從Naming.lookup()返回的必須造型到遠(yuǎn)程接口,而不是到類。若換用類,會(huì)得到一個(gè)違例提示。 在下述方法調(diào)用中: t.getPerfectTime( ) 我們可看到一旦獲得遠(yuǎn)程對(duì)象的句柄,用它進(jìn)行的編程與用本地對(duì)象的編程是非常相似(僅有一個(gè)區(qū)別:遠(yuǎn)程方法會(huì)“擲”出一個(gè)RemoteException違例)。 15.8.5 RMI的替選方案 RMI只是一種創(chuàng)建特殊對(duì)象的方式,它創(chuàng)建的對(duì)象可通過網(wǎng)絡(luò)發(fā)布。它最大的優(yōu)點(diǎn)就是提供了一種“純Java”方案,但假如已經(jīng)有許多用其他語(yǔ)言編寫的代碼,則RMI可能無(wú)法滿足我們的要求。目前,兩種最具競(jìng)爭(zhēng)力的替選方案是微軟的DCOM(根據(jù)微軟的計(jì)劃,它最終會(huì)移植到除Windows以外的其他平臺(tái))以及CORBA。CORBA自Java 1.1便開始支持,是一種全新設(shè)計(jì)的概念,面向跨平臺(tái)應(yīng)用。在由Orfali和Harkey編著的《Client/Server Programming with Java and CORBA》一書中(John Wiley&Sons 1997年出版),大家可獲得對(duì)Java中的分布式對(duì)象的全面介紹(該書似乎對(duì)CORBA似乎有些偏見)。為CORBA賦予一個(gè)較公正的對(duì)待的一本書是由Andreas Vogel和Keith Duddy編寫的《Java Programming with CORBA》,John Wiley&Sons于1997年出版。 15.9 總結(jié) 由于篇幅所限,還有其他許多涉及連網(wǎng)的概念沒有介紹給大家。Java也為URL提供了相當(dāng)全面的支持,包括為因特網(wǎng)上不同類型的客戶提供協(xié)議控制器等等。 除此以外,一種正在逐步流行的技術(shù)叫作Servlet Server。它是一種因特網(wǎng)服務(wù)器應(yīng)用,通過Java控制客戶請(qǐng)求,而非使用以前那種速度很慢、且相當(dāng)麻煩的CGI(通用網(wǎng)關(guān)接口)協(xié)議。這意味著為了在服務(wù)器那一端提供服務(wù),我們可以用Java編程,不必使用自己不熟悉的其他語(yǔ)言。由于Java具有優(yōu)秀的移植能力,所以不必關(guān)心具體容納這個(gè)服務(wù)器是什么平臺(tái)。 所有這些以及其他特性都在《Java Network Programming》一書中得到了詳細(xì)講述。該書由Elliotte Rusty Harold編著,O'Reilly于1997年出版。 15.10 練習(xí) (1) 編譯和運(yùn)行本章中的JabberServer和JabberClient程序。接著編輯一下程序,刪去為輸入和輸出設(shè)計(jì)的所有緩沖機(jī)制,然后再次編譯和運(yùn)行,觀察一下結(jié)果。 (2) 創(chuàng)建一個(gè)服務(wù)器,用它請(qǐng)求用戶輸入密碼,然后打開一個(gè)文件,并將文件通過網(wǎng)絡(luò)連接傳送出去。創(chuàng)建一個(gè)同該服務(wù)器連接的客戶,為其分配適當(dāng)?shù)拿艽a,然后捕獲和保存文件。在自己的機(jī)器上用localhost(通過調(diào)用InetAddress.getByName(null)生成本地IP地址127.0.0.1)測(cè)試這兩個(gè)程序。 (3) 修改練習(xí)2中的程序,令其用多線程機(jī)制對(duì)多個(gè)客戶進(jìn)行控制。 (4) 修改JabberClient,禁止輸出刷新,并觀察結(jié)果。 (5) 以ShowHTML.java為基礎(chǔ),創(chuàng)建一個(gè)程序片,令其成為對(duì)自己Web站點(diǎn)的特定部分進(jìn)行密碼保護(hù)的大門。 (6) (可能有些難度)創(chuàng)建一對(duì)客戶/服務(wù)器程序,利用數(shù)據(jù)報(bào)(Datagram)將一個(gè)文件從一臺(tái)機(jī)器傳到另一臺(tái)(參見本章數(shù)據(jù)報(bào)小節(jié)末尾的敘述)。 (7) (可能有些難度)對(duì)VLookup.java程序作一番修改,使我們能點(diǎn)擊得到的結(jié)果名字,然后程序會(huì)自動(dòng)取得那個(gè)名字,并把它復(fù)制到剪貼板(以便我們方便地粘貼到自己的E-mail)。可能要回過頭去研究一下IO數(shù)據(jù)流的那一章,回憶該如何使用Java 1.1剪貼板。

    posted on 2006-04-19 12:03 空空也 閱讀(535) 評(píng)論(0)  編輯  收藏


    只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


    網(wǎng)站導(dǎo)航:
     
    主站蜘蛛池模板: 亚洲AV日韩AV永久无码色欲| 久久精品国产亚洲av天美18| 人人狠狠综合久久亚洲| 中文在线免费视频| 成年人性生活免费视频| 亚洲日本va中文字幕久久| 亚洲AV无码一区二区三区人 | 亚洲免费综合色在线视频| 九九热久久免费视频| 久久久久国色AV免费观看性色| 国产亚洲精品激情都市| 国产精品亚洲精品青青青| 丝袜捆绑调教视频免费区| 午夜时刻免费入口| 亚洲欧洲日韩不卡| 日本精品久久久久久久久免费 | 亚洲国产电影在线观看| jizz免费观看视频| 最近中文字幕mv免费高清电影 | 午夜理伦剧场免费| 亚洲福利精品电影在线观看| 亚洲国产日韩在线人成下载| 二区久久国产乱子伦免费精品| 免费精品一区二区三区在线观看| 亚洲av女电影网| 成人午夜影视全部免费看| 最近最新中文字幕完整版免费高清 | 青青草国产免费久久久91| 91久久亚洲国产成人精品性色| 一级毛片人与动免费观看| 女人18毛片水最多免费观看| 91天堂素人精品系列全集亚洲| 国产精品偷伦视频免费观看了 | 亚洲香蕉久久一区二区| 久久成人无码国产免费播放| 亚洲AV蜜桃永久无码精品| 亚洲人片在线观看天堂无码| 中国人xxxxx69免费视频| 久久久久久久尹人综合网亚洲| 色吊丝性永久免费看码 | 日本高清免费中文在线看|