?? chapter15.htm
字號:
如果ServerSocket創(chuàng)建失敗,則再一次通過main()擲出違例。如果成功,則位于外層的try-finally代碼塊可以擔(dān)保正確的清除。位于內(nèi)層的try-catch塊只負(fù)責(zé)防范ServeOneJabber構(gòu)建器的失敗;若構(gòu)建器成功,則ServeOneJabber線程會將對應(yīng)的套接字關(guān)掉。<br>
為了證實(shí)服務(wù)器代碼確實(shí)能為多名客戶提供服務(wù),下面這個(gè)程序?qū)?chuàng)建許多客戶(使用線程),并同相同的服務(wù)器建立連接。每個(gè)線程的“存在時(shí)間”都是有限的。一旦到期,就留出空間以便創(chuàng)建一個(gè)新線程。允許創(chuàng)建的線程的最大數(shù)量是由final
int maxthreads決定的。大家會注意到這個(gè)值非常關(guān)鍵,因?yàn)榧偃绨阉O(shè)得很大,線程便有可能耗盡資源,并產(chǎn)生不可預(yù)知的程序錯(cuò)誤。<br>
<br>
840-842頁程序<br>
<br>
JabberClientThread構(gòu)建器獲取一個(gè)InetAddress,并用它打開一個(gè)套接字。大家可能已看出了這樣的一個(gè)套路:Socket肯定用于創(chuàng)建某種Reader以及/或者Writer(或者InputStream和/或OutputStream)對象,這是運(yùn)用Socket的唯一方式(當(dāng)然,我們可考慮編寫一、兩個(gè)類,令其自動完成這些操作,避免大量重復(fù)的代碼編寫工作)。同樣地,start()執(zhí)行線程的初始化,并調(diào)用run()。在這里,消息發(fā)送給服務(wù)器,而來自服務(wù)器的信息則在屏幕上回顯出來。然而,線程的“存在時(shí)間”是有限的,最終都會結(jié)束。注意在套接字創(chuàng)建好以后,但在構(gòu)建器完成之前,假若構(gòu)建器失敗,套接字會被清除。否則,為套接字調(diào)用close()的責(zé)任便落到了run()方法的頭上。<br>
threadcount跟蹤計(jì)算目前存在的JabberClientThread對象的數(shù)量。它將作為構(gòu)建器的一部分增值,并在run()退出時(shí)減值(run()退出意味著線程中止)。在MultiJabberClient.main()中,大家可以看到線程的數(shù)量會得到檢查。若數(shù)量太多,則多余的暫時(shí)不創(chuàng)建。方法隨后進(jìn)入“休眠”狀態(tài)。這樣一來,一旦部分線程最后被中止,多作的那些線程就可以創(chuàng)建了。大家可試驗(yàn)一下逐漸增大MAX_THREADS,看看對于你使用的系統(tǒng)來說,建立多少線程(連接)才會使您的系統(tǒng)資源降低到危險(xiǎn)程度。<br>
<br>
15.4 數(shù)據(jù)報(bào)<br>
大家迄今看到的例子使用的都是“傳輸控制協(xié)議”(TCP),亦稱作“基于數(shù)據(jù)流的套接字”。根據(jù)該協(xié)議的設(shè)計(jì)宗旨,它具有高度的可靠性,而且能保證數(shù)據(jù)順利抵達(dá)目的地。換言之,它允許重傳那些由于各種原因半路“走失”的數(shù)據(jù)。而且收到字節(jié)的順序與它們發(fā)出來時(shí)是一樣的。當(dāng)然,這種控制與可靠性需要我們付出一些代價(jià):TCP具有非常高的開銷。<br>
還有另一種協(xié)議,名為“用戶數(shù)據(jù)報(bào)協(xié)議”(UDP),它并不刻意追求數(shù)據(jù)包會完全發(fā)送出去,也不能擔(dān)保它們抵達(dá)的順序與它們發(fā)出時(shí)一樣。我們認(rèn)為這是一種“不可靠協(xié)議”(TCP當(dāng)然是“可靠協(xié)議”)。聽起來似乎很糟,但由于它的速度快得多,所以經(jīng)常還是有用武之地的。對某些應(yīng)用來說,比如聲音信號的傳輸,如果少量數(shù)據(jù)包在半路上丟失了,那么用不著太在意,因?yàn)閭鬏數(shù)乃俣蕊@得更重要一些。大多數(shù)互聯(lián)網(wǎng)游戲,如Diablo,采用的也是UDP協(xié)議通信,因?yàn)榫W(wǎng)絡(luò)通信的快慢是游戲是否流暢的決定性因素。也可以想想一臺報(bào)時(shí)服務(wù)器,如果某條消息丟失了,那么也真的不必過份緊張。另外,有些應(yīng)用也許能向服務(wù)器傳回一條UDP消息,以便以后能夠恢復(fù)。如果在適當(dāng)?shù)臅r(shí)間里沒有響應(yīng),消息就會丟失。<br>
Java對數(shù)據(jù)報(bào)的支持與它對TCP套接字的支持大致相同,但也存在一個(gè)明顯的區(qū)別。對數(shù)據(jù)報(bào)來說,我們在客戶和服務(wù)器程序都可以放置一個(gè)DatagramSocket(數(shù)據(jù)報(bào)套接字),但與ServerSocket不同,前者不會干巴巴地等待建立一個(gè)連接的請求。這是由于不再存在“連接”,取而代之的是一個(gè)數(shù)據(jù)報(bào)陳列出來。另一項(xiàng)本質(zhì)的區(qū)別的是對TCP套接字來說,一旦我們建好了連接,便不再需要關(guān)心誰向誰“說話”——只需通過會話流來回傳送數(shù)據(jù)即可。但對數(shù)據(jù)報(bào)來說,它的數(shù)據(jù)包必須知道自己來自何處,以及打算去哪里。這意味著我們必須知道每個(gè)數(shù)據(jù)報(bào)包的這些信息,否則信息就不能正常地傳遞。<br>
DatagramSocket用于收發(fā)數(shù)據(jù)包,而DatagramPacket包含了具體的信息。準(zhǔn)備接收一個(gè)數(shù)據(jù)報(bào)時(shí),只需提供一個(gè)緩沖區(qū),以便安置接收到的數(shù)據(jù)。數(shù)據(jù)包抵達(dá)時(shí),通過DatagramSocket,作為信息起源地的因特網(wǎng)地址以及端口編號會自動得到初化。所以一個(gè)用于接收數(shù)據(jù)報(bào)的DatagramPacket構(gòu)建器是:<br>
DatagramPacket(buf, buf.length)<br>
其中,buf是一個(gè)字節(jié)數(shù)組。既然buf是個(gè)數(shù)組,大家可能會奇怪為什么構(gòu)建器自己不能調(diào)查出數(shù)組的長度呢?實(shí)際上我也有同感,唯一能猜到的原因就是C風(fēng)格的編程使然,那里的數(shù)組不能自己告訴我們它有多大。<br>
可以重復(fù)使用數(shù)據(jù)報(bào)的接收代碼,不必每次都建一個(gè)新的。每次用它的時(shí)候(再生),緩沖區(qū)內(nèi)的數(shù)據(jù)都會被覆蓋。<br>
緩沖區(qū)的最大容量僅受限于允許的數(shù)據(jù)報(bào)包大小,這個(gè)限制位于比64KB稍小的地方。但在許多應(yīng)用程序中,我們都寧愿它變得還要小一些,特別是在發(fā)送數(shù)據(jù)的時(shí)候。具體選擇的數(shù)據(jù)包大小取決于應(yīng)用程序的特定要求。<br>
發(fā)出一個(gè)數(shù)據(jù)報(bào)時(shí),DatagramPacket不僅需要包含正式的數(shù)據(jù),也要包含因特網(wǎng)地址以及端口號,以決定它的目的地。所以用于輸出DatagramPacket的構(gòu)建器是:<br>
DatagramPacket(buf, length, inetAddress, port)<br>
這一次,buf(一個(gè)字節(jié)數(shù)組)已經(jīng)包含了我們想發(fā)出的數(shù)據(jù)。length可以是buf的長度,但也可以更短一些,意味著我們只想發(fā)出那么多的字節(jié)。另兩個(gè)參數(shù)分別代表數(shù)據(jù)包要到達(dá)的因特網(wǎng)地址以及目標(biāo)機(jī)器的一個(gè)目標(biāo)端口(注釋②)。<br>
<br>
②:我們認(rèn)為TCP和UDP端口是相互獨(dú)立的。也就是說,可以在端口8080同時(shí)運(yùn)行一個(gè)TCP和UDP服務(wù)程序,兩者之間不會產(chǎn)生沖突。<br>
<br>
大家也許認(rèn)為兩個(gè)構(gòu)建器創(chuàng)建了兩個(gè)不同的對象:一個(gè)用于接收數(shù)據(jù)報(bào),另一個(gè)用于發(fā)送它們。如果是好的面向?qū)ο蟮脑O(shè)計(jì)方案,會建議把它們創(chuàng)建成兩個(gè)不同的類,而不是具有不同的行為的一個(gè)類(具體行為取決于我們?nèi)绾螛?gòu)建對象)。這也許會成為一個(gè)嚴(yán)重的問題,但幸運(yùn)的是,DatagramPacket的使用相當(dāng)簡單,我們不需要在這個(gè)問題上糾纏不清。這一點(diǎn)在下例里將有很明確的說明。該例類似于前面針對TCP套接字的MultiJabberServer和MultiJabberClient例子。多個(gè)客戶都會將數(shù)據(jù)報(bào)發(fā)給服務(wù)器,后者會將其反饋回最初發(fā)出消息的同樣的客戶。<br>
為簡化從一個(gè)String里創(chuàng)建DatagramPacket的工作(或者從DatagramPacket里創(chuàng)建String),這個(gè)例子首先用到了一個(gè)工具類,名為Dgram:<br>
<br>
844-845頁程序<br>
<br>
Dgram的第一個(gè)方法采用一個(gè)String、一個(gè)InetAddress以及一個(gè)端口號作為自己的參數(shù),將String的內(nèi)容復(fù)制到一個(gè)字節(jié)緩沖區(qū),再將緩沖區(qū)傳遞進(jìn)入DatagramPacket構(gòu)建器,從而構(gòu)建一個(gè)DatagramPacket。注意緩沖區(qū)分配時(shí)的"+1"——這對防止截尾現(xiàn)象是非常重要的。String的getByte()方法屬于一種特殊操作,能將一個(gè)字串包含的char復(fù)制進(jìn)入一個(gè)字節(jié)緩沖。該方法現(xiàn)在已被“反對”使用;Java
1.1有一個(gè)“更好”的辦法來做這個(gè)工作,但在這里卻被當(dāng)作注釋屏蔽掉了,因?yàn)樗鼤氐鬝tring的部分內(nèi)容。所以盡管我們在Java
1.1下編譯該程序時(shí)會得到一條“反對”消息,但它的行為仍然是正確無誤的(這個(gè)錯(cuò)誤應(yīng)該在你讀到這里的時(shí)候修正了)。<br>
Dgram.toString()方法同時(shí)展示了Java 1.0的方法和Java 1.1的方法(兩者是不同的,因?yàn)橛幸环N新類型的String構(gòu)建器)。<br>
下面是用于數(shù)據(jù)報(bào)演示的服務(wù)器代碼:<br>
<br>
845-846頁程序<br>
<br>
ChatterServer創(chuàng)建了一個(gè)用來接收消息的DatagramSocket(數(shù)據(jù)報(bào)套接字),而不是在我們每次準(zhǔn)備接收一條新消息時(shí)都新建一個(gè)。這個(gè)單一的DatagramSocket可以重復(fù)使用。它有一個(gè)端口號,因?yàn)檫@屬于服務(wù)器,客戶必須確切知道自己把數(shù)據(jù)報(bào)發(fā)到哪個(gè)地址。盡管有一個(gè)端口號,但沒有為它分配因特網(wǎng)地址,因?yàn)樗婉v留在“這”臺機(jī)器內(nèi),所以知道自己的因特網(wǎng)地址是什么(目前是默認(rèn)的localhost)。在無限while循環(huán)中,套接字被告知接收數(shù)據(jù)(receive())。然后暫時(shí)掛起,直到一個(gè)數(shù)據(jù)報(bào)出現(xiàn),再把它反饋回我們希望的接收人——DatagramPacket
dp——里面。數(shù)據(jù)包(Packet)會被轉(zhuǎn)換成一個(gè)字串,同時(shí)插入的還有數(shù)據(jù)包的起源因特網(wǎng)地址及套接字。這些信息會顯示出來,然后添加一個(gè)額外的字串,指出自己已從服務(wù)器反饋回來了。<br>
大家可能會覺得有點(diǎn)兒迷惑。正如大家會看到的那樣,許多不同的因特網(wǎng)地址和端口號都可能是消息的起源地——換言之,客戶程序可能駐留在任何一臺機(jī)器里(就這一次演示來說,它們都駐留在localhost里,但每個(gè)客戶使用的端口編號是不同的)。為了將一條消息送回它真正的始發(fā)客戶,需要知道那個(gè)客戶的因特網(wǎng)地址以及端口號。幸運(yùn)的是,所有這些資料均已非常周到地封裝到發(fā)出消息的DatagramPacket內(nèi)部,所以我們要做的全部事情就是用getAddress()和getPort()把它們?nèi)〕鰜怼@眠@些資料,可以構(gòu)建DatagramPacket
echo——它通過與接收用的相同的套接字發(fā)送回來。除此以外,一旦套接字發(fā)出數(shù)據(jù)報(bào),就會添加“這”臺機(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í)候,這臺機(jī)器的地址以及端口才會寫入數(shù)據(jù)報(bào)。所以我們得到了運(yùn)用數(shù)據(jù)報(bào)時(shí)一項(xiàng)重要的原則:不必跟蹤一條消息的來源地!因?yàn)樗隙ū4嬖跀?shù)據(jù)報(bào)里。事實(shí)上,對程序來說,最可靠的做法是我們不要試圖跟蹤,而是無論如何都從目標(biāo)數(shù)據(jù)報(bào)里提取出地址以及端口信息(就象這里做的那樣)。<br>
為測試服務(wù)器的運(yùn)轉(zhuǎn)是否正常,下面這程序?qū)?chuàng)建大量客戶(線程),它們都會將數(shù)據(jù)報(bào)包發(fā)給服務(wù)器,并等候服務(wù)器把它們原樣反饋回來。<br>
<br>
847-849頁程序<br>
<br>
ChatterClient被創(chuàng)建成一個(gè)線程(Thread),所以可以用多個(gè)客戶來“騷擾”服務(wù)器。從中可以看到,用于接收的DatagramPacket和用于ChatterServer的那個(gè)是相似的。在構(gòu)建器中,創(chuàng)建DatagramPacket時(shí)沒有附帶任何參數(shù)(自變量),因?yàn)樗恍枰鞔_指出自己位于哪個(gè)特定編號的端口里。用于這個(gè)套接字的因特網(wǎng)地址將成為“這臺機(jī)器”(比如localhost),而且會自動分配端口編號,這從輸出結(jié)果即可看出。同用于服務(wù)器的那個(gè)一樣,這個(gè)DatagramPacket將同時(shí)用于發(fā)送和接收。<br>
hostAddress是我們想與之通信的那臺機(jī)器的因特網(wǎng)地址。在程序中,如果需要創(chuàng)建一個(gè)準(zhǔn)備傳出去的DatagramPacket,那么必須知道一個(gè)準(zhǔn)確的因特網(wǎng)地址和端口號。可以肯定的是,主機(jī)必須位于一個(gè)已知的地址和端口號上,使客戶能啟動與主機(jī)的“會話”。<br>
每個(gè)線程都有自己獨(dú)一無二的標(biāo)識號(盡管自動分配給線程的端口號是也會提供一個(gè)唯一的標(biāo)識符)。在run()中,我們創(chuàng)建了一個(gè)String消息,其中包含了線程的標(biāo)識編號以及該線程準(zhǔn)備發(fā)送的消息編號。我們用這個(gè)字串創(chuàng)建一個(gè)數(shù)據(jù)報(bào),發(fā)到主機(jī)上的指定地址;端口編號則直接從ChatterServer內(nèi)的一個(gè)常數(shù)取得。一旦消息發(fā)出,receive()就會暫時(shí)被“堵塞”起來,直到服務(wù)器回復(fù)了這條消息。與消息附在一起的所有信息使我們知道回到這個(gè)特定線程的東西正是從始發(fā)消息中投遞出去的。在這個(gè)例子中,盡管是一種“不可靠”協(xié)議,但仍然能夠檢查數(shù)據(jù)報(bào)是否到去過了它們該去的地方(這在localhost和LAN環(huán)境中是成立的,但在非本地連接中卻可能出現(xiàn)一些錯(cuò)誤)。<br>
運(yùn)行該程序時(shí),大家會發(fā)現(xiàn)每個(gè)線程都會結(jié)束。這意味著發(fā)送到服務(wù)器的每個(gè)數(shù)據(jù)報(bào)包都會回轉(zhuǎn),并反饋回正確的接收者。如果不是這樣,一個(gè)或更多的線程就會掛起并進(jìn)入“堵塞”狀態(tài),直到它們的輸入被顯露出來。<br>
大家或許認(rèn)為將文件從一臺機(jī)器傳到另一臺的唯一正確方式是通過TCP套接字,因?yàn)樗鼈兪恰翱煽俊钡摹H欢捎跀?shù)據(jù)報(bào)的速度非常快,所以它才是一種更好的選擇。我們只需將文件分割成多個(gè)數(shù)據(jù)報(bào),并為每個(gè)包編號。接收機(jī)器會取得這些數(shù)據(jù)包,并重新“組裝”它們;一個(gè)“標(biāo)題包”會告訴機(jī)器應(yīng)該接收多少個(gè)包,以及組裝所需的另一些重要信息。如果一個(gè)包在半路“走丟”了,接收機(jī)器會返回一個(gè)數(shù)據(jù)報(bào),告訴發(fā)送者重傳。<br>
<br>
15.5 一個(gè)Web應(yīng)用<br>
現(xiàn)在讓我們想想如何創(chuàng)建一個(gè)應(yīng)用,令其在真實(shí)的Web環(huán)境中運(yùn)行,它將把Java的優(yōu)勢表現(xiàn)得淋漓盡致。這個(gè)應(yīng)用的一部分是在Web服務(wù)器上運(yùn)行的一個(gè)Java程序,另一部分則是一個(gè)“程序片”或“小應(yīng)用程序”(Applet),從服務(wù)器下載至瀏覽器(即“客戶”)。這個(gè)程序片從用戶那里收集信息,并將其傳回Web服務(wù)器上運(yùn)行的應(yīng)用程序。程序的任務(wù)非常簡單:程序片會詢問用戶的E-mail地址,并在驗(yàn)證這個(gè)地址合格后(沒有包含空格,而且有一個(gè)@符號),將該E-mail發(fā)送給Web服務(wù)器。服務(wù)器上運(yùn)行的程序則會捕獲傳回的數(shù)據(jù),檢查一個(gè)包含了所有E-mail地址的數(shù)據(jù)文件。如果那個(gè)地址已包含在文件里,則向?yàn)g覽器反饋一條消息,說明這一情況。該消息由程序片負(fù)責(zé)顯示。若是一個(gè)新地址,則將其置入列表,并通知程序片已成功添加了電子函件地址。<br>
若采用傳統(tǒng)方式來解決這個(gè)問題,我們要創(chuàng)建一個(gè)包含了文本字段及一個(gè)“提交”(Submit)按鈕的HTML頁。用戶可在文本字段里鍵入自己喜歡的任何內(nèi)容,并毫無阻礙地提交給服務(wù)器(在客戶端不進(jìn)行任何檢查)。提交數(shù)據(jù)的同時(shí),Web頁也會告訴服務(wù)器應(yīng)對數(shù)據(jù)采取什么樣的操作——知會“通用網(wǎng)關(guān)接口”(CGI)程序,收到這些數(shù)據(jù)后立即運(yùn)行服務(wù)器。這種CGI程序通常是用Perl或C寫的(有時(shí)也用C++,但要求服務(wù)器支持),而且必須能控制一切可能出現(xiàn)的情況。它首先會檢查數(shù)據(jù),判斷是否采用了正確的格式。若答案是否定的,則CGI程序必須創(chuàng)建一個(gè)HTML頁,對遇到的問題進(jìn)行描述。這個(gè)頁會轉(zhuǎn)交給服務(wù)器,再由服務(wù)器反饋回用戶。用戶看到出錯(cuò)提示后,必須再試一遍提交,直到通過為止。若數(shù)據(jù)正確,CGI程序會打開數(shù)據(jù)文件,要么把電子函件地址加入文件,要么指出該地址已在數(shù)據(jù)文件里了。無論哪種情況,都必須格式化一個(gè)恰當(dāng)?shù)腍TML頁,以便服務(wù)器返回給用戶。<br>
作為Java程序員,上述解決問題的方法顯得非常笨拙。而且很自然地,我們希望一切工作都用Java完成。首先,我們會用一個(gè)Java程序片負(fù)責(zé)客戶端的數(shù)據(jù)有效性校驗(yàn),避免數(shù)據(jù)在服務(wù)器和客戶之間傳來傳去,浪費(fèi)時(shí)間和帶寬,同時(shí)減輕服務(wù)器額外構(gòu)建HTML頁的負(fù)擔(dān)。然后跳過Perl
CGI腳本,換成在服務(wù)器上運(yùn)行一個(gè)Java應(yīng)用。事實(shí)上,我們在這兒已完全跳過了Web服務(wù)器,僅僅需要從程序片到服務(wù)器上運(yùn)行的Java應(yīng)用之間建立一個(gè)連接即可。<br>
正如大家不久就會體驗(yàn)到的那樣,盡管看起來非常簡單,但實(shí)際上有一些意想不到的問題使局面顯得稍微有些復(fù)雜。用Java
1.1寫程序片是最理想的,但實(shí)際上卻經(jīng)常行不通。到本書寫作的時(shí)候,擁有Java
1.1能力的瀏覽器仍為數(shù)不多,而且即使這類瀏覽器現(xiàn)在非常流行,仍需考慮照顧一下那些升級緩慢的人。所以從安全的角度看,程序片代碼最好只用Java
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -