當(dāng)今的Web應(yīng)用在我們的個(gè)人生活與商業(yè)應(yīng)用中的各個(gè)方面已經(jīng)表現(xiàn)出愈發(fā)重要的作用。這些應(yīng)用包括社交媒體網(wǎng)絡(luò)、在線購(gòu)物、商業(yè)應(yīng)用,乃至家用電器的配置程序。雖然它的增長(zhǎng)勢(shì)頭依然迅猛,但Web應(yīng)用的用戶體驗(yàn)與原生應(yīng)用或桌面應(yīng)用相比仍然相形見絀,其主要原因是Web應(yīng)用的設(shè)計(jì)依賴于單向的HTTP協(xié)議。而WebSocket將改變這一現(xiàn)狀,它為瀏覽器與服務(wù)端的交互帶來了一種新的基礎(chǔ)元素,為創(chuàng)建一種能夠提供真正的交互性體驗(yàn)的Web應(yīng)用提供了必要的基礎(chǔ)。
早期的Web技術(shù)都是基于HTTP協(xié)議而發(fā)展起來的,而HTTP只是一個(gè)簡(jiǎn)單的基于請(qǐng)求 —— 響應(yīng)操作的協(xié)議,所有的請(qǐng)求都是由客戶端發(fā)起的。這套框架原本足以滿足用戶的需求,但在如今開發(fā)者所設(shè)計(jì)的web應(yīng)用中,由客戶端發(fā)起通信這種方式有著很大的制約。雖然人們提出了各種臨時(shí)方案,但它們都是基于HTTP協(xié)議的,只是應(yīng)用了輪詢或長(zhǎng)輪詢技術(shù)(例如Comet)。Comet能夠讓負(fù)責(zé)處理請(qǐng)求的線程得到釋放,以防止服務(wù)器資源耗盡。由于輪詢這種機(jī)制并不可靠,因此在2007年時(shí),有人提出了一種名為WebSocket的全雙工(full-duplex)類型的通信方式。這項(xiàng)提議用了整整4年的時(shí)間才成為一個(gè)標(biāo)準(zhǔn)。但是,盡管它已成為一種標(biāo)準(zhǔn),但它的使用率卻相當(dāng)有限。本文將為讀者解釋妨礙WebSocket應(yīng)用的兩大原因,并且提出了一個(gè)設(shè)計(jì)框架,開發(fā)者可以使用這套框架快速地發(fā)揮WebSocket的潛能,并且極大地豐富應(yīng)用的體驗(yàn)。
導(dǎo)致WebSocket使用率低下的第一個(gè)原因在于應(yīng)用服務(wù)器與瀏覽器對(duì)其支持不足。但隨著新一代應(yīng)用服務(wù)器與瀏覽器的出現(xiàn),這種狀況得到了很大的改善。而第二個(gè)原因比起前一點(diǎn)來說其影響更大,亦即要充分利用WebSocket的全部潛能,必須對(duì)Web應(yīng)用進(jìn)行顛覆性的重新設(shè)計(jì)。而這個(gè)重新設(shè)計(jì)過程需要將基礎(chǔ)的請(qǐng)求 —— 響應(yīng)這一結(jié)構(gòu)轉(zhuǎn)變?yōu)楦鼜?fù)雜的雙向消息傳遞結(jié)構(gòu)。應(yīng)用程序的重新設(shè)計(jì)往往是一個(gè)開銷很大的過程,而軟件供應(yīng)商很難從這一過程中看到任何顯著的利益。
我們首先將對(duì)WebSocket做個(gè)簡(jiǎn)單的介紹,隨后展示一種使用WebSocket重新構(gòu)建應(yīng)用程序的方法,最后通過一個(gè)簡(jiǎn)單的示例表現(xiàn)這一方法的各種要點(diǎn)。
WebSocket是在TCP/ip協(xié)議之上創(chuàng)建的一種幀協(xié)議,客戶端將通過向服務(wù)器發(fā)送一種特殊的HTTP請(qǐng)求以啟動(dòng)WebSocket。在最初的握手過程之后,客戶端與服務(wù)器就能夠自由地以異步方式互相進(jìn)行幀的傳送了。幀分為兩種類型:即控制幀與數(shù)據(jù)幀。最小的控制幀僅有2比特的大小,而在數(shù)據(jù)幀方面,客戶端的數(shù)據(jù)幀最小為6比特,服務(wù)端的數(shù)據(jù)幀最小為2比特。數(shù)據(jù)幀既可以是文本型,也可以是二進(jìn)制的。文本幀都經(jīng)過了UTF-8的編碼。幀可以實(shí)現(xiàn)分塊,因此一個(gè)大數(shù)據(jù)集可以分解為多個(gè)幀。WebSocket不會(huì)為幀附加任何標(biāo)識(shí)信息,因此不同類型的信息對(duì)應(yīng)的幀不可混用。只有控制幀能夠在處理一個(gè)大消息時(shí)的一系列中間幀中出現(xiàn)。在這些基礎(chǔ)的幀之上,還可以定義更復(fù)雜的協(xié)議。比方說,一個(gè)幀能夠帶有校驗(yàn)和或是它的序列號(hào)等相關(guān)信息。
WebSocket并不限定于僅在某個(gè)特定的編程語(yǔ)言、系統(tǒng)或是操作系統(tǒng)中使用。多數(shù)主流的編程語(yǔ)言以及許多瀏覽器都已開始支持WebSocket的編程。雖然在不同的平臺(tái)與編程語(yǔ)言中存在著大量的標(biāo)準(zhǔn),但本文僅關(guān)注javaScript HTML5以及Java(J2EE)對(duì)WebSocket的支持。在瀏覽器這方面有兩種實(shí)現(xiàn)標(biāo)準(zhǔn),其最新版本分別為Hixie-76和HyBi-17(不久之后發(fā)展為IETF RFC 6455)。HyBi的實(shí)現(xiàn)相對(duì)更高級(jí),并且得到了目前所有主流瀏覽器的支持。而在服務(wù)端方面,基于Java的實(shí)現(xiàn)則是目前最為流行的。早些時(shí)候在Java上曾經(jīng)出現(xiàn)過幾種WebSocket的實(shí)現(xiàn),它們之后已發(fā)展為JSR 356這種實(shí)現(xiàn)。JSR代表Java規(guī)范請(qǐng)求,對(duì)規(guī)范請(qǐng)求的說明有幫于讓之后的各種實(shí)現(xiàn)保持一致性,并且易于使用。JSR也讓開發(fā)者不必依賴于某個(gè)特定的實(shí)現(xiàn)。JSR 356與servlet規(guī)范是相互分離的,但它也允許開發(fā)者訪問某些servlet對(duì)象。JSR 356的內(nèi)容涵蓋了WebSocket連接的客戶端與服務(wù)端, 我們稍后的討論將集中于配合瀏覽器端的Javascript所實(shí)現(xiàn)的服務(wù)端。JSR 356目前屬于J2EE 7的一部分,所有流行的開源Java應(yīng)用服務(wù)器都支持它,包括Tomcat、Jetty、Glassfish以及TJWS等等。除此之外,在Java環(huán)境中還存在著大約20種各自獨(dú)立的WebSocket服務(wù)端解決方案,其中有些方案也支持JSR 356。由于WebSocket是J2EE 7的一部分,因而在由Oracle與IBM所推出的商業(yè)應(yīng)用服務(wù)器上同樣也得到支持。
正如我之前所說,WebSocket是一種消息傳遞協(xié)議。它的API提供了各種在通信雙方進(jìn)行消息傳遞與接收的方法。這里并不存在經(jīng)典的訂閱者與發(fā)布者的關(guān)系。消息只有兩種類型,即文本型與二進(jìn)制型。不過,在這些類型的消息處理函數(shù)中可以對(duì)消息進(jìn)行邏輯上的分離。在Java中能夠以某種方式處理被分解為多個(gè)塊的部分消息,而JavaScript尚未支持這種程度的控制能力。如同之前所說,WebSocket是一種非常泛用的協(xié)議,它可以在握手時(shí)指定所需的邏輯子協(xié)議。當(dāng)不同的系統(tǒng)能夠驗(yàn)證所連到的系統(tǒng)支持這種邏輯子協(xié)議及擴(kuò)展時(shí),使用WebSocket進(jìn)行系統(tǒng)集成就變得容易很多。WebSocket幀格式允許在它的基礎(chǔ)上使用可協(xié)商的擴(kuò)展,這與意味著一般來說幀可能會(huì)提供更多的信息,并且可能會(huì)引入不同的幀類型。
由于WebSocket協(xié)議的握手過程是由客戶端發(fā)起的,因此需要通過包含了WebSocket接口的JavaScript代碼對(duì)所有WebSocket操作進(jìn)行封裝。
該接口已經(jīng)實(shí)現(xiàn)了標(biāo)準(zhǔn)化1,并通過接口定義語(yǔ)言(IDL)進(jìn)行定義,如以下代碼所示:
[Constructor(in DOMString url, in optional DOMString PRotocols)][Constructor(in DOMString url, in optional DOMString[] protocols)]interface WebSocket { readonly attribute DOMString url; // ready state const unsigned short CONNECTING = 0; const unsigned short OPEN = 1; const unsigned short CLOSING = 2; const unsigned short CLOSED = 3; readonly attribute unsigned short readyState; readonly attribute unsigned long bufferedAmount; // networking attribute Function onopen; attribute Function onmessage; attribute Function onerror; attribute Function onclose; readonly attribute DOMString protocol; void send(in DOMString data); void close();};WebSocket implements EventTarget;
WebSocket的構(gòu)建函數(shù)包含兩個(gè)參數(shù):
WebSocket的URL都是以“ws”為前兩個(gè)字符,它代表所使用的是WebSocket協(xié)議,而其余部分與HTTP協(xié)議中的URL相同,包括主機(jī)、端口、路徑以及查詢字符串。如果需要使用安全連接,可以在協(xié)議名稱上加一個(gè)額外的“s”字符。
可以指定的消息處理函數(shù)共有四種:onopen、onmessage、onclose和onerror。在傳遞消息時(shí)需要調(diào)用send方法,而在關(guān)閉連接時(shí)則需要調(diào)用close方法。由于不存在類似于connect這樣的方法,因此客戶端必須監(jiān)聽onopen消息,以確認(rèn)連接已建立,隨后才能夠進(jìn)行send操作。另一種選擇是對(duì)WebSocket對(duì)象的readyState屬性進(jìn)行輪詢,但這種方式并不推薦使用。顯然,在onmessage處理函數(shù)中總是能夠調(diào)用send操作的。send操作由客戶端異步執(zhí)行,這也意味著JavaScript在將消息傳遞給接收者的過程中無(wú)須等待其結(jié)果,而是直接返回。文本消息或二進(jìn)制消息在接收時(shí)不存在任何差別,因此在onmessage處理函數(shù)中必須對(duì)事件的data參數(shù)進(jìn)行檢查。WebSocket提供了一些屬性,可用于獲取狀態(tài)、判斷二進(jìn)制消息的格式等目的。而其它瀏覽器廠商的特定實(shí)現(xiàn)中還可以包含更多的屬性,因此請(qǐng)記得仔細(xì)閱讀瀏覽器的文檔,以了解詳細(xì)的信息。
Java中的JSR 356定義了常見的(客戶端)與服務(wù)端的Java WebSocket通信API。在Java的實(shí)現(xiàn)中會(huì)指定終結(jié)點(diǎn)與服務(wù)端終結(jié)點(diǎn)對(duì)象,這與JavaScript中的WebSocket實(shí)現(xiàn)頗為類似。可以通過注解的方式將某個(gè)Java類指定為一個(gè)終結(jié)點(diǎn)對(duì)象,而通過OnOpen、OnMessage、OnError和 OnClose等注解信息指定事件處理函數(shù)。在每種類型的處理函數(shù)中,都可以將重要的session對(duì)象作為一個(gè)傳入?yún)?shù)。Session對(duì)象讓開發(fā)者能夠訪問發(fā)送消息的功能,并且能夠保持與WebSocket連接相關(guān)的狀態(tài)特性。消息的發(fā)送可以使用同步或異步機(jī)制,并且在兩種類型的發(fā)送機(jī)制中都可以指定超時(shí)時(shí)間。通過指定相應(yīng)的解碼器,二進(jìn)制與文本數(shù)據(jù)都能夠自動(dòng)轉(zhuǎn)換為任意的Java對(duì)象,而編碼器則允許WebSocket發(fā)送任意類型的Java對(duì)象。對(duì)于某個(gè)特定的WebSocket URL路徑,消息處理函數(shù)只能對(duì)應(yīng)文本消息類型或二進(jìn)制消息類型的其中一種。Java中未提供消息鏈的功能,但也可以通過編程的方式對(duì)其進(jìn)行組織。Java端的API很容易上手,它提供了一種可自定義的配置對(duì)象,能夠影響最初的握手過程,決定所支持的子協(xié)議、版本,并且提供訪問重要的servlet對(duì)象API的功能。終結(jié)點(diǎn)不僅能夠通過注解的方式進(jìn)行部署,也能夠通過編程的方式所生成。
WebSocket對(duì)于以下類型的應(yīng)用程序的開發(fā)是一種非常自然的選擇:
其實(shí),WebSocket在傳統(tǒng)的Web應(yīng)用中也能夠展現(xiàn)其優(yōu)勢(shì)。大多數(shù)Web應(yīng)用都是基于請(qǐng)求 - 響應(yīng)這一范式進(jìn)行設(shè)計(jì)的。雖然Ajax能夠?qū)崿F(xiàn)異步操作,但在繼續(xù)處理下一步操作之間,仍然必須等待響應(yīng)返回。而由于WebSocket連接只需建立一次,從而避免了為每次數(shù)據(jù)交換重建連接的過程,并且在后續(xù)的通信中也無(wú)需發(fā)送多余的HTTP頭信息。這種優(yōu)勢(shì)在SSL類型的連接上體現(xiàn)得尤為明顯,因?yàn)樽畛醯倪B接握手是一個(gè)開銷很大的操作。瀏覽器端的WebSocket發(fā)送操作是完全異步的,而Java的服務(wù)端代碼在發(fā)送消息后無(wú)需進(jìn)行等待。由于發(fā)送消息的這種自由度,在應(yīng)用中或許需要對(duì)某些操作進(jìn)行手動(dòng)記錄,以保持應(yīng)用狀態(tài)的一致性。在使用WebSocket時(shí)也能夠模擬請(qǐng)求 - 響應(yīng)這一范式,但如此一來,WebSocket作為一種真正的異步雙向消息傳遞系統(tǒng)的優(yōu)勢(shì)也被大大消減了。由于以上所描述的這些特性,因此應(yīng)當(dāng)鼓勵(lì)開發(fā)者在某些場(chǎng)景中對(duì)應(yīng)用程序的設(shè)計(jì)方式進(jìn)行重新思考。
假設(shè)某一個(gè)應(yīng)用程序包含了復(fù)雜的用戶界面,其中某些區(qū)域的功能需要通過服務(wù)端的大量計(jì)算才能夠生成對(duì)應(yīng)的內(nèi)容。傳統(tǒng)的基于AJAX的實(shí)現(xiàn)方式可以選擇一種延遲調(diào)用的機(jī)制,通過某個(gè)內(nèi)容請(qǐng)求調(diào)用以生成這一區(qū)域的內(nèi)容。而在使用WebSocket的場(chǎng)合下,服務(wù)端可以在瀏覽器做好準(zhǔn)備的情況下直接發(fā)送內(nèi)容,而無(wú)需對(duì)某個(gè)AJAX請(qǐng)求進(jìn)行響應(yīng)。AJAX請(qǐng)求這一方式的缺陷在于,由于瀏覽器所發(fā)送的請(qǐng)求是串行的,因此服務(wù)端的處理過程無(wú)法針對(duì)請(qǐng)求的順序進(jìn)行相應(yīng)的優(yōu)化。而WebSocket為服務(wù)端提供了一個(gè)自行決定最佳的內(nèi)容生成方式的機(jī)會(huì),因而能夠提升Web應(yīng)用的整體響應(yīng)性。
要用效地利用WebSocket的功能,還需要仔細(xì)考慮幾個(gè)額外的要點(diǎn)。由于在WebSocket中隨時(shí)可能出現(xiàn)網(wǎng)絡(luò)連接的丟失,使數(shù)據(jù)無(wú)法正確地傳遞,因此對(duì)于一些至關(guān)重要的數(shù)據(jù)需要進(jìn)行一些額外的手動(dòng)記錄操作。一般來說,所收到的每條消息都必須提供足夠的信息,以指示如何對(duì)其進(jìn)行處理。但沒有有效的手段能夠了解信息的請(qǐng)求者是誰(shuí),是來自客戶端的請(qǐng)求,還是說服務(wù)端想要更新某些內(nèi)容。在具體使用WebSocket的過程中,可能需要對(duì)Web應(yīng)用的設(shè)計(jì)進(jìn)行更深入的重新思考。此外,JavaScript代碼的功能可以遷移至服務(wù)端,打個(gè)比方,用戶的輸入可以立即發(fā)送給服務(wù)端進(jìn)行處理,通過這種方式能夠?qū)崿F(xiàn)一些復(fù)雜的數(shù)據(jù)校驗(yàn)操作,而這些校驗(yàn)功能或許是JavaScript所無(wú)法處理的。用戶的輸入還能夠即時(shí)地保存在后臺(tái)系統(tǒng)中,因此瀏覽器就無(wú)需將最終的數(shù)據(jù)傳遞給服務(wù)器進(jìn)行額外的數(shù)據(jù)校驗(yàn),因?yàn)閿?shù)據(jù)在保存在后臺(tái)期間已經(jīng)經(jīng)過了校驗(yàn)。如果要使某個(gè)應(yīng)用從富Web客戶端轉(zhuǎn)為一種輕量級(jí)的客戶端,就可以考慮以這種方式增加服務(wù)端代碼的職責(zé)。
在Web應(yīng)用開發(fā)時(shí)使用WebSocket也會(huì)面對(duì)一些特別的挑戰(zhàn),WebSocket的Session與HTTP的Session之間并無(wú)任何關(guān)聯(lián),雖然也可將其用作類似的目標(biāo)。在Session中可以附加某些通用的數(shù)據(jù),因此所有的消息處理過程都可以依賴于Session中所維護(hù)的某些狀態(tài)和數(shù)據(jù)。WebSocket的Session也可以根據(jù)空閑(不活躍)時(shí)間間隔的配置產(chǎn)生超時(shí)情況,正如HTTP Session一樣。不過有些系統(tǒng)會(huì)自動(dòng)地持續(xù)發(fā)送Ping這一控制消息,以防止出現(xiàn)超時(shí)。JSR 356建議將HTTP Session與WebSocket Session的超時(shí)進(jìn)行同步。一旦HTTP Session超時(shí),在其范圍內(nèi)所創(chuàng)建的所有WebSocket連接也都必須關(guān)閉。但有些Web應(yīng)用的設(shè)計(jì)不會(huì)產(chǎn)生任何HTTP Session,而有些應(yīng)用的Session超時(shí)不依賴于HTTP Session,而是由JavaScript所管理的,因此這種機(jī)制并不能夠進(jìn)行可靠的推廣。
另一種需要注意的要點(diǎn)在于,某些瀏覽器會(huì)維護(hù)一個(gè)連接池,以重用連接的方式訪問相同的網(wǎng)站,因此這種流程可以被串行化。而如果瀏覽器為WebSocket連接也創(chuàng)建一個(gè)連接池,那么它會(huì)受到嚴(yán)重的制約。因?yàn)槿绻麤]有某種機(jī)制保持WebSocket連接的關(guān)閉,這個(gè)連接就永遠(yuǎn)處于活躍狀態(tài),而其它任何創(chuàng)建新連接的嘗試都會(huì)產(chǎn)生死鎖。因此,最佳實(shí)踐的推薦做法是只使用一個(gè)WebSocket連接。
瀏覽器無(wú)法對(duì)通過WebSocket進(jìn)行傳遞的數(shù)據(jù)進(jìn)行緩存,因此通過WebSocket傳遞可以在瀏覽器中緩存的資源
(例如圖片、CSS等)并非一種有效的途徑。
在網(wǎng)絡(luò)上對(duì)于RESTful與WebSocket之間的討論從未停歇2。不過,這些比較中的大部分都不是在一個(gè)層面上的比較,好比關(guān)公戰(zhàn)秦瓊。REST是指表述性狀態(tài)轉(zhuǎn)換,多數(shù)情況下它需要依賴底層的HTTP協(xié)議實(shí)現(xiàn),也就是說REST是一個(gè)基于請(qǐng)求 - 響應(yīng)的協(xié)議。REST這種風(fēng)格沒有經(jīng)過標(biāo)準(zhǔn)化,因此任何一種通過HTTP進(jìn)行通信的方式在某些范圍內(nèi)都可以稱為REST。REST通常會(huì)將新增、讀取、更新和刪除操作(CRUD)與對(duì)應(yīng)的HTTP方法PUT、GET、DELETE之間建立映射關(guān)系。而WebSocket所處理的是消息,因此對(duì)于單一的RPC來說不存在一個(gè)確定的范圍。REST的通信數(shù)據(jù)格式通常僅限JSON格式以及請(qǐng)求參數(shù),而一個(gè)WebSocket消息體可以表現(xiàn)為任何類型,包括純粹的二進(jìn)制數(shù)據(jù)3。
當(dāng)然,WebSocket也能夠用于與REST相似的目的,但在大多數(shù)情況下,這種做法有些刻意為之了。正如上文所述,在使用WebSocket過程中需要應(yīng)用一些不同的設(shè)計(jì)原則。下表描述了這兩者之間的主要區(qū)別4。
以下示例展現(xiàn)了如何通過使用WebSocket將一個(gè)文件上傳至服務(wù)器,首先最好定義一個(gè)服務(wù)端的終結(jié)點(diǎn)。
@ServerEndpoint("/upload/{file}")public class UploadServer {
其中要定義兩個(gè)消息處理函數(shù),一個(gè)用于接收上傳文件的二進(jìn)制數(shù)據(jù),而另一個(gè)則用于命令接口。由于在WebSocket中允許分離文本與二進(jìn)制消息,因此在定義兩個(gè)處理函數(shù)時(shí)無(wú)需進(jìn)行額外的操作。用于接收命令的OnMessage處理函數(shù)定義如下:
@OnMessage public void processCmd(CMD cmd, Session ses) {
static class CMD { public int cmd; public String data; }
為了將文本消息轉(zhuǎn)換為CMD對(duì)象,需要指定一個(gè)解碼器,其定義如下:
public static class CmdDecoder implements Decoder.Text<CMD> {
將文本信息編碼為JSON格式并不是一種強(qiáng)制性的要求,只是在這個(gè)示例中需要用到JSON。大文件的上傳是分多個(gè)塊進(jìn)行的,以減少內(nèi)存的開銷。在瀏覽器中無(wú)法利用WebSocket的部分幀,因此需要用到完整的幀來模擬塊傳送。由于瀏覽器以異步的方式發(fā)送所有的消息,因此無(wú)法得知服務(wù)端是否已經(jīng)接收到了一個(gè)完整的文件。命令接口的作用是完成以下工作:
同樣的CMD對(duì)象可以進(jìn)行重用,以滿足各種需求。傳入的命令是按照以下方式進(jìn)行處理的:
@OnMessage public void processCmd(CMD cmd, Session ses) { switch (cmd.cmd) { case 1: // start fileName = cmd.data; break; case 2: // finish close(ses); cmd.cmd = 3; ses.getAsyncRemote().sendObject(cmd); break; } }
這種實(shí)現(xiàn)方式假設(shè)瀏覽器端會(huì)將所有發(fā)送消息的活動(dòng)進(jìn)行串行化,即所有消息的到達(dá)順序與發(fā)送順序是一致的。但是如果某個(gè)客戶端使用了某些并行方式進(jìn)行發(fā)送,那么就需要一種更為復(fù)雜的實(shí)現(xiàn)方式,讓每個(gè)所發(fā)送的消息都帶有一個(gè)ID。另一種方案是為每個(gè)收到的文件塊都發(fā)送一次確認(rèn)消息,只是這樣一來WebSocket的優(yōu)勢(shì)也就喪失殆盡了。由于CMD對(duì)象的目標(biāo)是將消息發(fā)送至客戶端,因此必須提供一個(gè)編碼器:
public static class CmdEncoder implements Encoder.Text<CMD> {
在ServerEndpoint的注解中必須指定解碼器與編碼器信息,如下所示:
@ServerEndpoint(value = "/upload/{file}", decoders = UploadServer.CmdDecoder.class, encoders=UploadServer.CmdEncoder.class)public class UploadServer {
二進(jìn)制消息的處理函數(shù)定義如下:
@OnMessagepublic void savePart(byte[] part, Session ses) { if (uploadFile == null) { if (fileName != null) try { uploadFile = new RandomaccessFile(fileName, "rw"); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); return; } } if (uploadFile != null) try { uploadFile.write(part); System.err.printf("Stored part of %db%n", part.length); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }}
此外還可以為OnClose事件加入一個(gè)處理函數(shù),萬(wàn)一出現(xiàn)連接異常關(guān)閉的情況,它將負(fù)責(zé)刪除不完整的文件。
客戶端的實(shí)現(xiàn)利用了HTML5中的工作線程(Worker)功能,不幸的是,Firefox沒有采用在Worker中實(shí)現(xiàn)文件對(duì)象克隆的方式,因此這個(gè)示例只能在IE或Chrome中進(jìn)行測(cè)試。如果該解決方案對(duì)于瀏覽器的可移植性有很高的要求,那么可以用一個(gè)不使用Worker的JavaScript代碼段來代替這個(gè)基于Worker的解決方案。但由于未使用獨(dú)立的線程(即Worker),因此這種方案的性能會(huì)有所下降。Worker的代碼如下所示:
var files = [];var endPoint = "ws" + (self.location.protocol == "https:" ? "s" : "") + "://" + self.location.hostname + (self.location.port ? ":" + self.location.port : "") + "/echoserver/upload/*";var socket;var ready;function upload(blobOrFile) { if (ready) socket.send(blobOrFile);}function openSocket() { socket = new WebSocket(endPoint); socket.onmessage = function(event) { self.postMessage(JSON.parse(event.data)); }; socket.onclose = function(event) { ready = false; }; socket.onopen = function() { ready = true; process(); };}function process() { while (files.length > 0) { var blob = files.shift(); socket.send(JSON.stringify({ "cmd" : 1, "data" : blob.name })); const BYTES_PER_CHUNK = 1024 * 1024 * 2; // 1MB chunk sizes. const SIZE = blob.size; var start = 0; var end = BYTES_PER_CHUNK; while (start < SIZE) { if ('mozSlice' in blob) { var chunk = blob.mozSlice(start, end); } else if ('slice' in blob) { var chunk = blob.slice(start, end); } else { var chunk = blob.webkitSlice(start, end); } upload(chunk); start = end; end = start + BYTES_PER_CHUNK; } socket.send(JSON.stringify({ "cmd" : 2, "data" : blob.name })); //self.postMessage(blob.name + " Uploaded Succesfully"); }}self.onmessage = function(e) { for (var j = 0; j < e.data.files.length; j++) files.push(e.data.files[j]); //self.postMessage("Job size: "+files.length); if (ready) { process(); } else openSocket();}
很方便的一點(diǎn)在于,與Worker進(jìn)行交互的JavaScript代碼也能夠利用消息傳遞機(jī)制。當(dāng)用戶在瀏覽器中選擇文件進(jìn)行上傳時(shí),這一操作的信息就會(huì)傳遞給Worker。后者會(huì)以批量的方式處理第一個(gè)準(zhǔn)備上傳的文件,它將文件分成多個(gè)片段,即多個(gè)塊,然后通過WebSocket將這些塊依次上傳。最后發(fā)送一個(gè)cmd = 2的命令消息。而命令消息的處理函數(shù)會(huì)將消息重新發(fā)送給主JavaScript代碼,通知所上傳的文件已經(jīng)完成了。如果客戶端選擇上傳許多大文件,那么這段代碼會(huì)對(duì)瀏覽器端帶來相當(dāng)大的壓力。為此需要對(duì)代碼進(jìn)行重新調(diào)整,讓它在收到上一個(gè)文件上傳成功的消息后才繼續(xù)上傳下一個(gè)文件。這部分內(nèi)容的修改就留給各位讀者作為一個(gè)練習(xí)吧。在附錄1中可以找到本示例的完整源代碼。
全能程序員交流QQ群290551701,群內(nèi)程序員都是來自,百度、阿里、京東、小米、去哪兒、餓了嗎、藍(lán)港等高級(jí)程序員 ,擁有豐富的經(jīng)驗(yàn)。加入我們,直線溝通技術(shù)大牛,最佳的學(xué)習(xí)環(huán)境,了解業(yè)內(nèi)的一手的資訊。如果你想結(jié)實(shí)大牛,那 就加入進(jìn)來,讓大牛帶你超神!
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注