原文地址java
即時通信,在 Web 早先開發就有,那時最多見的實現手段是輪詢(polling)。輪詢是在某個時間間隔(如1秒),由瀏覽器向服務器發出 HTTP request,而後服務器返回最新的數據給客戶端的瀏覽器。這種方式最明顯的缺點,是瀏覽器須要不斷的向服務器發出請求,即使都沒有消息了,並且,HTTP request 的 header 很長,實際有用數據可能只是一個很小的值,無形中佔用帶寬。以後,出現了 Comet,但這種技術必定程度上只是模擬全雙工通訊,效率較低,並須要服務器有較好的支持。git
(數據通訊中,數據在線路上的傳送方式能夠分爲單工通訊、半雙工通訊和全雙工通訊三種,具體不解釋,好比,遙控器是單工的,對講機是半雙工的,電話是全雙工的。)github
WebSocket protocol 是 HTML5 一種新的協議。它實現了瀏覽器與服務器全雙工通訊(full-duplex),能更好的節省服務器資源和帶寬並達到實時通信。瀏覽器經過 Http 僅能實現單向通訊,輪詢也好,comet 也罷,都不是很理想;flash 中的 socket 和 xmlsocket 能夠實現真正的雙向通訊。經過 flex ajax bridge,能夠在 Javascript 中使用這兩項功能。若是 Websocket 一旦在瀏覽器中獲得實現,將取代上面兩項技術,獲得普遍的使用。web
早期的Web技術都是基於HTTP協議而發展起來的,而HTTP只是一個簡單的基於請求 —— 響應操做的協議,全部的請求都是由客戶端發起的。這套框架本來足以知足用戶的需求,但在現在開發者所設計的web應用中,由客戶端發起通訊這種方式有着很大的制約。雖然人們提出了各類臨時方案,但它們都是基於HTTP協議的,只是應用了輪詢或長輪詢技術(例如Comet)。Comet可以讓負責處理請求的線程獲得釋放,以防止服務器資源耗盡。因爲輪詢這種機制並不可靠,所以在2007年時,有人提出了一種名爲WebSocket的全雙工(full- duplex)類型的通訊方式。這項提議用了整整4年的時間才成爲一個標準。可是,儘管它已成爲一種標準,但它的使用率卻至關有限。本文將爲讀者解釋妨礙 WebSocket應用的兩大緣由,而且提出了一個設計框架,開發者可使用這套框架快速地發揮WebSocket的潛能,而且極大地豐富應用的體驗。 ajax
致使 WebSocket 使用率低下的第一個緣由在於應用服務器與瀏覽器對其支持不足。但隨着新一代應用服務器與瀏覽器的出現,這種情況獲得了很大的改善。而第二個緣由比起前一點來講其影響更大,亦即要充分利用WebSocket的所有潛能,必須對Web應用進行顛覆性的從新設計。而這個從新設計過程須要將基礎的請求 —— 響應這一結構轉變爲更復雜的雙向消息傳遞結構。應用程序的從新設計每每是一個開銷很大的過程,而軟件供應商很難從這一過程當中看到任何顯著的利益。 apache
咱們首先將對WebSocket作個簡單的介紹,隨後展現一種使用WebSocket從新構建應用程序的方法,最後經過一個簡單的示例表現這一方法的各類要點。 編程
WebSocket 是在 TCP/IP 協議之上建立的一種幀協議,客戶端經過向服務器發送一種特殊的 HTTP 請求來啓動 WebSocket。在最初的握手過程以後,客戶端與服務器就可以自由地以異步方式互相進行幀的傳送了。幀分爲兩種類型:即控制幀與數據幀。最小的控制幀僅有2比特的大小,客戶端最小的數據幀爲6比特,服務端最小的數據幀爲2比特。數據幀既能夠是文本型,也能夠是二進制的。文本幀都通過了UTF-8的編碼。幀能夠實現分塊,所以一個大數據集能夠分解爲多個幀。WebSocket不會爲幀附加任何標識信息,所以不一樣類型的信息對應的幀不可混用。只有控制幀可以在處理一個大消息時的一系列中間幀中出現。在這些基礎的幀之上,還能夠定義更復雜的協議。比方說,一個幀可以帶有校驗和或是它的序列號等相關信息。 api
WebSocket 並不限定於僅在某個特定的編程語言、系統或是操做系統中使用。多數主流的編程語言以及許多瀏覽器都已開始支持WebSocket 的編程。雖然在不一樣的平臺與編程語言中存在着大量的標準,但本文僅關注JavaScript HTML5以及Java(J2EE)對WebSocket的支持。在瀏覽器這方面有兩種實現標準,其最新版本分別爲Hixie-76和HyBi-17(不久以後發展爲IETF RFC 6455)。HyBi的實現相對更高級,而且獲得了目前全部主流瀏覽器的支持。而在服務端方面,基於Java的實現則是目前最爲流行的。早些時候在 Java上曾經出現過幾種WebSocket的實現,它們以後已發展爲JSR 356這種實現。JSR表明Java規範請求,對規範請求的說明有幫於讓以後的各類實現保持一致性,而且易於使用。JSR也讓開發者沒必要依賴於某個特定的實現。JSR 356與servlet規範是相互分離的,但它也容許開發者訪問某些servlet對象。JSR 356的內容涵蓋了WebSocket鏈接的客戶端與服務端, 咱們稍後的討論將集中於配合瀏覽器端的JavaScript所實現的服務端。JSR 356目前屬於J2EE 7的一部分,全部流行的開源Java應用服務器都支持它,包括Tomcat、Jetty、Glassfish以及TJWS等等。除此以外,在Java環境中還存在着大約20種各自獨立的WebSocket服務端解決方案,其中有些方案也支持JSR 356。因爲WebSocket是J2EE 7的一部分,於是在由Oracle與IBM所推出的商業應用服務器上一樣也獲得支持。 數組
正如我以前所說,WebSocket是一種消息傳遞協議。它的API提供了各類在通訊雙方進行消息傳遞與接收的方法。這裏並不存在經典的訂閱者與發佈者的關係。消息只有兩種類型,即文本型與二進制型。不過,在這些類型的消息處理函數中能夠對消息進行邏輯上的分離。在Java中可以以某種方式處理被分解爲多個塊的部分消息,而JavaScript還沒有支持這種程度的控制能力。如同以前所說,WebSocket是一種很是泛用的協議,它能夠在握手時指定所需的邏輯子協議。當不一樣的系統可以驗證所連到的系統支持這種邏輯子協議及擴展時,使用WebSocket進行系統集成就變得容易不少。 WebSocket幀格式容許在它的基礎上使用可協商的擴展,這與意味着通常來講幀可能會提供更多的信息,而且可能會引入不一樣的幀類型。 瀏覽器
因爲WebSocket協議的握手過程是由客戶端發起的,所以須要經過包含了WebSocket接口的JavaScript代碼對全部WebSocket操做進行封裝。
該接口已經實現了標準化1,並經過接口定義語言(IDL)進行定義,如如下代碼所示:
[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的構建函數包含兩個參數:
WebSocket的URL都是以「ws」爲前兩個字符,它表明所使用的是WebSocket協議,而其他部分與HTTP協議中的URL相同,包括主機、端口、路徑以及查詢字符串。若是須要使用安全鏈接,能夠在協議名稱上加一個額外的「s」字符。
能夠指定的消息處理函數共有四種:onopen、onmessage、onclose和onerror。在傳遞消息時須要調用send方法,而在關閉鏈接時則須要調用close方法。因爲不存在相似於connect這樣的方法,所以客戶端必須監聽onopen消息,以確認鏈接已創建,隨後纔可以進行send操做。另外一種選擇是對WebSocket對象的readyState屬性進行輪詢,但這種方式並不推薦使用。顯然,在onmessage處理函數中老是可以調用send操做的。send操做由客戶端異步執行,這也意味着JavaScript在將消息傳遞給接收者的過程當中無須等待其結果,而是直接返回。文本消息或二進制消息在接收時不存在任何差異,所以在onmessage處理函數中必須對事件的data參數進行檢查。WebSocket提供了一些屬性,可用於獲取狀態、判斷二進制消息的格式等目的。而其它瀏覽器廠商的特定實現中還能夠包含更多的屬性,所以請記得仔細閱讀瀏覽器的文檔,以瞭解詳細的信息。
Java中的JSR 356定義了常見的(客戶端)與服務端的Java WebSocket通訊API。在Java的實現中會指定終結點與服務端終結點對象,這與JavaScript中的WebSocket實現頗爲相似。能夠經過註解的方式將某個Java類指定爲一個終結點對象,而經過OnOpen、OnMessage、OnError和 OnClose等註解信息指定事件處理函數。在每種類型的處理函數中,均可以將重要的Session對象做爲一個傳入參數。Session對象讓開發者可以訪問發送消息的功能,而且可以保持與WebSocket鏈接相關的狀態特性。消息的發送可使用同步或異步機制,而且在兩種類型的發送機制中均可以指定超時時間。經過指定相應的解碼器,二進制與文本數據都可以自動轉換爲任意的Java對象,而編碼器則容許WebSocket發送任意類型的Java對象。對於某個特定的WebSocket URL路徑,消息處理函數只能對應文本消息類型或二進制消息類型的其中一種。Java中未提供消息鏈的功能,但也能夠經過編程的方式對其進行組織。 Java端的API很容易上手,它提供了一種可自定義的配置對象,可以影響最初的握手過程,決定所支持的子協議、版本,而且提供訪問重要的servlet 對象API的功能。終結點不只可以經過註解的方式進行部署,也可以經過編程的方式所生成。
WebSocket對於如下類型的應用程序的開發是一種很是天然的選擇:
其實,WebSocket在傳統的Web應用中也可以展示其優點。大多數Web應用都是基於請求 - 響應這一範式進行設計的。雖然AJAX可以實現異步操做,但在繼續處理下一步操做之間,仍然必須等待響應返回。而因爲WebSocket鏈接只需創建一次,從而避免了爲每次數據交換重建鏈接的過程,而且在後續的通訊中也無需發送多餘的HTTP頭信息。這種優點在SSL類型的鏈接上體現得尤其明顯,由於最初的鏈接握手是一個開銷很大的操做。瀏覽器端的WebSocket發送操做是徹底異步的,而Java的服務端代碼在發送消息後無需進行等待。因爲發送消息的這種自由度,在應用中或許須要對某些操做進行手動記錄,以保持應用狀態的一致性。在使用WebSocket時也可以模擬請求 - 響應這一範式,但如此一來,WebSocket做爲一種真正的異步雙向消息傳遞系統的優點也被大大消減了。因爲以上所描述的這些特性,所以應當鼓勵開發者在某些場景中對應用程序的設計方式進行從新思考。
假設某一個應用程序包含了複雜的用戶界面,其中某些區域的功能須要經過服務端的大量計算纔可以生成對應的內容。傳統的基於AJAX的實現方式能夠選擇一種延遲調用的機制,經過某個內容請求調用以生成這一區域的內容。而在使用WebSocket的場合下,服務端能夠在瀏覽器作好準備的狀況下直接發送內容,而無需對某個AJAX請求進行響應。AJAX請求這一方式的缺陷在於,因爲瀏覽器所發送的請求是串行的,所以服務端的處理過程沒法針對請求的順序進行相應的優化。而WebSocket爲服務端提供了一個自行決定最佳的內容生成方式的機會,於是可以提高Web應用的總體響應性。
要用效地利用WebSocket的功能,還須要仔細考慮幾個額外的要點。因爲在WebSocket中隨時可能出現網絡鏈接的丟失,使數據沒法正確地傳遞,所以對於一些相當重要的數據須要進行一些額外的手動記錄操做。通常來講,所收到的每條消息都必須提供足夠的信息,以指示如何對其進行處理。但沒有有效的手段可以瞭解信息的請求者是誰,是來自客戶端的請求,仍是說服務端想要更新某些內容。在具體使用WebSocket的過程當中,可能須要對 Web應用的設計進行更深刻的從新思考。此外,JavaScript代碼的功能能夠遷移至服務端,打個比方,用戶的輸入能夠當即發送給服務端進行處理,經過這種方式可以實現一些複雜的數據校驗操做,而這些校驗功能或許是JavaScript所沒法處理的。用戶的輸入還可以即時地保存在後臺系統中,所以瀏覽器就無需將最終的數據傳遞給服務器進行額外的數據校驗,由於數據在保存在後臺期間已經通過了校驗。若是要使某個應用從富Web客戶端轉爲一種輕量級的客戶端,就能夠考慮以這種方式增長服務端代碼的職責。
在Web應用開發時使用WebSocket也會面對一些特別的挑戰,WebSocket的Session與HTTP的Session之間並沒有任何關聯,雖然也可將其用做相似的目標。在Session中能夠附加某些通用的數據,所以全部的消息處理過程均可以依賴於Session中所維護的某些狀態和數據。WebSocket的Session也能夠根據空閒(不活躍)時間間隔的配置產生超時狀況,正如HTTP Session同樣。不過有些系統會自動地持續發送Ping這一控制消息,以防止出現超時。JSR 356建議將HTTP Session與WebSocket Session的超時進行同步。一旦HTTP Session超時,在其範圍內所建立的全部WebSocket鏈接也都必須關閉。但有些Web應用的設計不會產生任何HTTP Session,而有些應用的Session超時不依賴於HTTP Session,而是由JavaScript所管理的,所以這種機制並不可以進行可靠的推廣。
另外一種須要注意的要點在於,某些瀏覽器會維護一個鏈接池,以重用鏈接的方式訪問相同的網站,所以這種流程能夠被串行化。而若是瀏覽器爲 WebSocket鏈接也建立一個鏈接池,那麼它會受到嚴重的制約。由於若是沒有某種機制保持WebSocket鏈接的關閉,這個鏈接就永遠處於活躍狀態,而其它任何建立新鏈接的嘗試都會產生死鎖。所以,最佳實踐的推薦作法是隻使用一個WebSocket鏈接。
瀏覽器沒法對經過WebSocket進行傳遞的數據進行緩存,所以經過WebSocket傳遞能夠在瀏覽器中緩存的資源
(例如圖片、CSS等)並不是一種有效的途徑。
在網絡上對於RESTful與WebSocket之間的討論從未停歇2。不過,這些比較中的大部分都不是在一個層面上的比較,比如關公戰秦瓊。REST是指表述性狀態轉換,多數狀況下它須要依賴底層的HTTP協議實現,也就是說REST是一個基於請求 - 響應的協議。REST這種風格沒有通過標準化,所以任何一種經過HTTP進行通訊的方式在某些範圍內均可以稱爲REST。REST一般會將新增、讀取、更新和刪除操做(CRUD)與對應的HTTP方法PUT、GET、DELETE之間創建映射關係。而WebSocket所處理的是消息,所以對於單一的 RPC來講不存在一個肯定的範圍。REST的通訊數據格式一般僅限JSON格式以及請求參數,而一個WebSocket消息體能夠表現爲任何類型,包括純粹的二進制數據3。
固然,WebSocket也可以用於與REST類似的目的,但在大多數狀況下,這種作法有些刻意爲之了。正如上文所述,在使用WebSocket過程當中須要應用一些不一樣的設計原則。下表描述了這二者之間的主要區別4。
WebSocket |
REST |
已實現標準化 |
獲得普遍支持 |
異步消息傳遞 |
(同步)請求/響應 |
基於幀 |
基於HTTP方法(get,put,delete,post) |
子協議 |
可發現的操做 |
二進制與文本 |
目前大都爲 JSON 數據 |
並行雙向更新 |
目前大都爲 CRUD 操做(Create、Retrieve、Update 和 Delete) |
如下示例展示瞭如何經過使用WebSocket將一個文件上傳至服務器,首先最好定義一個服務端的終結點。
@ServerEndpoint("/upload/{file}")
public class UploadServer {
其中要定義兩個消息處理函數,一個用於接收上傳文件的二進制數據,而另外一個則用於命令接口。因爲在WebSocket中容許分離文本與二進制消息,所以在定義兩個處理函數時無需進行額外的操做。用於接收命令的OnMessage處理函數定義以下:
@OnMessage
public void processCmd(CMD cmd, Session ses) {
CMD類的定義以下
static class CMD {
public int cmd;
public String data;
}
爲了將文本消息轉換爲CMD對象,須要指定一個解碼器,其定義以下:
public static class CmdDecoder implements Decoder.Text<CMD> {
將文本信息編碼爲JSON格式並非一種強制性的要求,只是在這個示例中須要用到JSON。大文件的上傳是分多個塊進行的,以減小內存的開銷。在瀏覽器中沒法利用WebSocket的部分幀,所以須要用到完整的幀來模擬塊傳送。因爲瀏覽器以異步的方式發送全部的消息,所以沒法得知服務端是否已經接收到了一個完整的文件。命令接口的做用是完成如下工做:
一樣的CMD對象能夠進行重用,以知足各類需求。傳入的命令是按照如下方式進行處理的:
@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;
}
}
這種實現方式假設瀏覽器端會將全部發送消息的活動進行串行化,即全部消息的到達順序與發送順序是一致的。可是若是某個客戶端使用了某些並行方式進行發送,那麼就須要一種更爲複雜的實現方式,讓每一個所發送的消息都帶有一個ID。另外一種方案是爲每一個收到的文件塊都發送一次確認消息,只是這樣一來WebSocket的優點也就喪失殆盡了。因爲CMD對象的目標是將消息發送至客戶端,所以必須提供一個編碼器:
public static class CmdEncoder implements Encoder.Text<CMD> {
在ServerEndpoint的註解中必須指定解碼器與編碼器信息,以下所示:
@ServerEndpoint(value = "/upload/{file}", decoders = UploadServer.CmdDecoder.class, encoders=UploadServer.CmdEncoder.class)
public class UploadServer {
二進制消息的處理函數定義以下:
@OnMessage
public 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事件加入一個處理函數,萬一出現鏈接異常關閉的狀況,它將負責刪除不完整的文件。
客戶端的實現利用了HTML5中的工做線程(Worker)功能,不幸的是,Firefox沒有采用在Worker中實現文件對象克隆的方式,所以這個示例只能在IE或Chrome中進行測試。若是該解決方案對於瀏覽器的可移植性有很高的要求,那麼能夠用一個不使用Worker的 JavaScript代碼段來代替這個基於Worker的解決方案。但因爲未使用獨立的線程(即Worker),所以這種方案的性能會有所降低。 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();
}
很方便的一點在於,與Worker進行交互的JavaScript代碼也可以利用消息傳遞機制。當用戶在瀏覽器中選擇文件進行上傳時,這一操做的信息就會傳遞給Worker。後者會以批量的方式處理第一個準備上傳的文件,它將文件分紅多個片斷,即多個塊,而後經過WebSocket將這些塊依次上傳。最後發送一個cmd = 2的命令消息。而命令消息的處理函數會將消息從新發送給主JavaScript代碼,通知所上傳的文件已經完成了。若是客戶端選擇上傳許多大文件,那麼這段代碼會對瀏覽器端帶來至關大的壓力。爲此須要對代碼進行從新調整,讓它在收到上一個文件上傳成功的消息後才繼續上傳下一個文件。這部份內容的修改就留給各位讀者做爲一個練習吧。在附錄1中能夠找到本示例的完整源代碼。
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |
jetty | 7.0.1 包含了一個初步的實現 |
resin | |
pywebsocket | apache http server 擴展 |
apache tomcat | 7.0.27 版本 |
Nginx | 1.3.13 版本 |
jWebSocket | java實現版 |