?? chapter15.htm
字號(hào):
1.0編寫(xiě)。基于這一前提,我們不能用JAR文件來(lái)合并(壓縮)程序片中的.class文件。所以,我們應(yīng)盡可能減少.class文件的使用數(shù)量,以縮短下載時(shí)間。<br>
好了,再來(lái)說(shuō)說(shuō)我用的Web服務(wù)器(寫(xiě)這個(gè)示范程序時(shí)用的就是它)。它確實(shí)支持Java,但僅限于Java
1.0!所以服務(wù)器應(yīng)用也必須用Java 1.0編寫(xiě)。<br>
<br>
15.5.1 服務(wù)器應(yīng)用<br>
現(xiàn)在討論一下服務(wù)器應(yīng)用(程序)的問(wèn)題,我把它叫作NameCollecor(名字收集器)。假如多名用戶同時(shí)嘗試提交他們的E-mail地址,那么會(huì)發(fā)生什么情況呢?若NameCollector使用TCP/IP套接字,那么必須運(yùn)用早先介紹的多線程機(jī)制來(lái)實(shí)現(xiàn)對(duì)多個(gè)客戶的并發(fā)控制。但所有這些線程都試圖把數(shù)據(jù)寫(xiě)到同一個(gè)文件里,其中保存了所有E-mail地址。這便要求我們?cè)O(shè)立一種鎖定機(jī)制,保證多個(gè)線程不會(huì)同時(shí)訪問(wèn)那個(gè)文件。一個(gè)“信號(hào)機(jī)”可在這里幫助我們達(dá)到目的,但或許還有一種更簡(jiǎn)單的方式。<br>
如果我們換用數(shù)據(jù)報(bào),就不必使用多線程了。用單個(gè)數(shù)據(jù)報(bào)即可“偵聽(tīng)”進(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)求。<br>
服務(wù)器應(yīng)用收到一個(gè)數(shù)據(jù)報(bào),并對(duì)它進(jìn)行解讀的時(shí)候,必須提取出其中的電子函件地址,并檢查本機(jī)保存的數(shù)據(jù)文件,看看里面是否已經(jīng)包含了那個(gè)地址(如果沒(méi)有,則添加之)。所以我們現(xiàn)在遇到了一個(gè)新的問(wèn)題。Java
1.0似乎沒(méi)有足夠的能力來(lái)方便地處理包含了電子函件地址的文件(Java
1.1則不然)。但是,用C輕易就可以解決這個(gè)問(wèn)題。因此,我們?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ǔ)言寫(xiě)一個(gè)程序,只要它能從標(biāo)準(zhǔn)輸入中取得自己的輸入數(shù)據(jù),并將輸出結(jié)果寫(xiě)入標(biāo)準(zhǔn)輸出即可。如果有些問(wèn)題不能用Java簡(jiǎn)便與快速地解決(或者想利用原有代碼,不想改寫(xiě)),就可以考慮采用這種方法。亦可使用Java的“固有方法”(Native
Method),但那要求更多的技巧,大家可以參考一下附錄A。<br>
<br>
1. C程序<br>
這個(gè)非Java應(yīng)用是用C寫(xiě)成,因?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ǔ)言寫(xiě)一個(gè)程序,并從Java中調(diào)用它。在這里具體采用何種語(yǔ)言并不重要,只要能夠從標(biāo)準(zhǔn)輸入中讀取數(shù)據(jù),并能寫(xiě)入標(biāo)準(zhǔn)輸出即可。<br>
<br>
852-853頁(yè)程序<br>
<br>
該程序假設(shè)C編譯器能接受'//'樣式注釋?zhuān)ㄔS多編譯器都能,亦可換用一個(gè)C++編譯器來(lái)編譯這個(gè)程序)。如果你的編譯器不能接受,則簡(jiǎn)單地將那些注釋刪掉即可。<br>
文件中的第一個(gè)函數(shù)檢查我們作為第二個(gè)參數(shù)(指向一個(gè)char的指針)傳遞給它的名字是否已在文件中。在這兒,我們將文件作為一個(gè)FILE指針傳遞,它指向一個(gè)已打開(kāi)的文件(文件是在main()中打開(kāi)的)。函數(shù)fseek()在文件中遍歷;我們?cè)谶@兒用它移至文件開(kāi)頭。fgets()從文件list中讀入一行內(nèi)容,并將其置入緩沖區(qū)lbuf——不會(huì)超過(guò)規(guī)定的緩沖區(qū)長(zhǎng)度BSIZE。所有這些工作都在一個(gè)while循環(huán)中進(jìn)行,所以文件中的每一行都會(huì)讀入。接下來(lái),用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)容的上面)。如果找遍列表都沒(méi)有發(fā)現(xiàn)相符的內(nèi)容,則函數(shù)返回0。<br>
在main()中,我們用fopen()打開(kāi)文件。第一個(gè)參數(shù)是文件名,第二個(gè)是打開(kāi)文件的方式;a+表示“追加”,以及“打開(kāi)”(或“創(chuàng)建”,假若文件尚不存在),以便到文件的末尾進(jìn)行更新。fopen()函數(shù)返回的是一個(gè)FILE指針;若為0,表示打開(kāi)操作失敗。此時(shí)需要用perror()打印一條出錯(cuò)提示消息,并用exit()中止程序運(yùn)行。<br>
如果文件成功打開(kāi),程序就會(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)行刷新。<br>
如果名字不在列表中,就用fseek()移到列表末尾,并用fprintf()將名字“打印”到列表末尾。隨后,用printf()指出名字已成功加入列表(同樣需要刷新標(biāo)準(zhǔn)輸出),無(wú)限循環(huán)返回,繼續(xù)等候一個(gè)新名字的進(jìn)入。<br>
記住一般不能先在自己的計(jì)算機(jī)上編譯此程序,再把編譯好的內(nèi)容上載到Web服務(wù)器,因?yàn)槟桥_(tái)機(jī)器使用的可能是不同類(lèi)的處理器和操作系統(tǒng)。例如,我的Web服務(wù)器安裝的是Intel的CPU,但操作系統(tǒng)是Linux,所以必須先下載源碼,再用遠(yuǎn)程命令(通過(guò)telnet)指揮Linux自帶的C編譯器,令其在服務(wù)器端編譯好程序。<br>
<br>
2. Java程序<br>
這個(gè)程序先啟動(dòng)上述的C程序,再建立必要的連接,以便同它“交談”。隨后,它創(chuàng)建一個(gè)數(shù)據(jù)報(bào)套接字,用它“監(jiān)視”或者“偵聽(tīng)”來(lái)自程序片的數(shù)據(jù)報(bào)包。<br>
<br>
854-956頁(yè)程序<br>
<br>
NameCollector中的第一個(gè)定義應(yīng)該是大家所熟悉的:選定端口,創(chuàng)建一個(gè)數(shù)據(jù)報(bào)包,然后創(chuàng)建指向一個(gè)DatagramSocket的句柄。接下來(lái)的三個(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)然地需要“封裝”起來(lái),所以我們最后得到的是一個(gè)PrintStream和DataInputStream。<br>
這個(gè)程序的所有工作都是在構(gòu)建器內(nèi)進(jìn)行的。為啟動(dòng)C程序,需要取得當(dāng)前的Runtime對(duì)象。我們用它調(diào)用exec(),再由后者返回Process對(duì)象。在Process對(duì)象中,大家可看到通過(guò)一簡(jiǎn)單的調(diào)用即可生成數(shù)據(jù)流:getOutputStream()和getInputStream()。從這個(gè)時(shí)候開(kāi)始,我們需要考慮的全部事情就是將數(shù)據(jù)傳給數(shù)據(jù)流nameList,并從addResult中取得結(jié)果。<br>
和往常一樣,我們將DatagramSocket同一個(gè)端口連接到一起。在無(wú)限while循環(huán)中,程序會(huì)調(diào)用receive()——除非一個(gè)數(shù)據(jù)報(bào)到來(lái),否則receive()會(huì)一起處于“堵塞”狀態(tài)。數(shù)據(jù)報(bào)出現(xiàn)以后,它的內(nèi)容會(huì)提取到String
rcvd里。我們首先將該字串兩頭的空格剔除(trim),再將其發(fā)給C程序。如下所示:<br>
nameList.println(rcvd.trim());<br>
之所以能這樣編碼,是因?yàn)镴ava的exec()允許我們?cè)L問(wèn)任何可執(zhí)行模塊,只要它能從標(biāo)準(zhǔn)輸入中讀,并能向標(biāo)準(zhǔn)輸出中寫(xiě)。還有另一些方式可與非Java代碼“交談”,這將在附錄A中討論。<br>
從C程序中捕獲結(jié)果就顯得稍微麻煩一些。我們必須調(diào)用read(),并提供一個(gè)緩沖區(qū),以便保存結(jié)果。read()的返回值是來(lái)自C程序的字節(jié)數(shù)。若這個(gè)值為-1,意味著某個(gè)地方出現(xiàn)了問(wèn)題。否則,我們就將resultBuf(結(jié)果緩沖區(qū))轉(zhuǎn)換成一個(gè)字串,然后同樣清除多余的空格。隨后,這個(gè)字串會(huì)象往常一樣進(jìn)入一個(gè)DatagramPacket,并傳回當(dāng)初發(fā)出請(qǐng)求的那個(gè)同樣的地址。注意發(fā)送方的地址也是我們接收到的DatagramPacket的一部分。<br>
記住盡管C程序必須在Web服務(wù)器上編譯,但Java程序的編譯場(chǎng)所可以是任意的。這是由于不管使用的是什么硬件平臺(tái)和操作系統(tǒng),編譯得到的字節(jié)碼都是一樣的。就就是Java的“跨平臺(tái)”兼容能力。<br>
<br>
15.5.2 NameSender程序片<br>
正如早先指出的那樣,程序片必須用Java 1.0編寫(xiě),使其能與絕大多數(shù)的瀏覽器適應(yīng)。也正是由于這個(gè)原因,我們產(chǎn)生的類(lèi)數(shù)量應(yīng)盡可能地少。所以我們?cè)谶@兒不考慮使用前面設(shè)計(jì)好的Dgram類(lèi),而將數(shù)據(jù)報(bào)的所有維護(hù)工作都轉(zhuǎn)到代碼行中進(jìn)行。此外,程序片要用一個(gè)線程監(jiān)視由服務(wù)器傳回的響應(yīng)信息,而非實(shí)現(xiàn)Runnable接口,用集成到程序片的一個(gè)獨(dú)立線程來(lái)做這件事情。當(dāng)然,這樣做對(duì)代碼的可讀性不利,但卻能產(chǎn)生一個(gè)單類(lèi)(以及單個(gè)服務(wù)器請(qǐng)求)程序片:<br>
<br>
858-860頁(yè)程序<br>
<br>
程序片的UI(用戶界面)非常簡(jiǎn)單。它包含了一個(gè)TestField(文本字段),以便我們鍵入一個(gè)電子函件地址;以及一個(gè)Button(按鈕),用于將地址發(fā)給服務(wù)器。兩個(gè)Label(標(biāo)簽)用于向用戶報(bào)告狀態(tài)信息。<br>
到現(xiàn)在為止,大家已能判斷出DatagramSocket、InetAddress、緩沖區(qū)以及DatagramPacket都屬于網(wǎng)絡(luò)連接中比較麻煩的部分。最后,大家可看到run()方法實(shí)現(xiàn)了線程部分,使程序片能夠“偵聽(tīng)”由服務(wù)器傳回的響應(yīng)信息。<br>
init()方法用大家熟悉的布局工具設(shè)置GUI,然后創(chuàng)建DatagramSocket,它將同時(shí)用于數(shù)據(jù)報(bào)的收發(fā)。<br>
action()方法只負(fù)責(zé)監(jiān)視我們是否按下了“發(fā)送”(send)按鈕。記住,我們已被限制在Java
1.0上面,所以不能再用較靈活的內(nèi)部類(lèi)了。按鈕按下以后,采取的第一項(xiàng)行動(dòng)便是檢查線程pl,看看它是否為null(空)。如果不為null,表明有一個(gè)活動(dòng)線程正在運(yùn)行。消息首次發(fā)出時(shí),會(huì)啟動(dòng)一個(gè)新線程,用它監(jiān)視來(lái)自服務(wù)器的回應(yīng)。所以假若有個(gè)線程正在運(yùn)行,就意味著這并非用戶第一次發(fā)送消息。pl句柄被設(shè)為null,同時(shí)中止原來(lái)的監(jiān)視者(這是最合理的一種做法,因?yàn)閟top()已被Java
1.2“反對(duì)”,這在前一章已解釋過(guò)了)。<br>
無(wú)論這是否按鈕被第一次按下,I2中的文字都會(huì)清除。<br>
下一組語(yǔ)句將檢查E-mail名字是否合格。String.indexOf()方法的作用是搜索其中的非法字符。如果找到一個(gè),就把情況報(bào)告給用戶。注意進(jìn)行所有這些工作時(shí),都不必涉及網(wǎng)絡(luò)通信,所以速度非常快,而且不會(huì)影響帶寬和服務(wù)器的性能。<br>
名字校驗(yàn)通過(guò)以后,它會(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ì)告訴我們程序片正在等候來(lái)自服務(wù)器的回應(yīng)。<br>
線程的run()方法會(huì)利用NameSender中包含的DatagramSocket來(lái)接收數(shù)據(jù)(receive()),除非出現(xiàn)來(lái)自服務(wù)器的數(shù)據(jù)報(bào)包,否則receive()會(huì)暫時(shí)處于“堵塞”或者“暫停”狀態(tài)。結(jié)果得到的數(shù)據(jù)包會(huì)放進(jìn)NameSender的DatagramPacketdp中。數(shù)據(jù)會(huì)從包中提取出來(lái),并置入NameSender的第二個(gè)標(biāo)簽。隨后,線程的執(zhí)行將中斷,成為一個(gè)“死”線程。若某段時(shí)間里沒(méi)有收到來(lái)自服務(wù)器的回應(yīng),用戶可能變得不耐煩,再次按下按鈕。這樣做會(huì)中斷當(dāng)前線程(數(shù)據(jù)發(fā)出以后,會(huì)再建一個(gè)新的)。由于用一個(gè)線程來(lái)監(jiān)視回應(yīng)數(shù)據(jù),所以用戶在監(jiān)視期間仍然可以自由使用UI。<br>
<br>
1. Web頁(yè)<br>
當(dāng)然,程序片必須放到一個(gè)Web頁(yè)里。下面列出完整的Web頁(yè)源碼;稍微研究一下就可看出,我用它從自己開(kāi)辦的郵寄列表(Mailling
List)里自動(dòng)收集名字。<br>
程序片標(biāo)記(<applet>)的使用非常簡(jiǎn)單,和第13章展示的那一個(gè)并沒(méi)有什么區(qū)別。<br>
?? 快捷鍵說(shuō)明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號(hào)
Ctrl + =
減小字號(hào)
Ctrl + -