轉帖:http://www.developersky.net/thread-81-1-1.html
除了Server-Sent Event之外,即將到來的HTML5標準還包含了WebSockets。WebSocket使得我們可以建立雙向的通訊通道。和Server-Sent Event相反,WebSocket協議不是建立在HTTP之上的。但是WebSocket協議訂立了HTTP握手的行為來將已經存在的HTTP串連轉換為WebSocket串連。WebSocket沒有試圖在HTTP之上類比server推送的通道,而是直接在TCP之上定義了幀協議,因此WebSocket能夠支援雙向的通訊。
和server-Sent Event規範相同,WebSocket定義了API和相應的協議。WebSocket API規範中包含一個新的HTML元素:WebSocket。下面的代碼就是一個使用WebSocket的HTML的例子:
<html><br /> <head><br /> <mce:script type='text/javascript'><!--<br /> var ws = new WebSocket('ws://localhost:8876/Channel', 'mySubprotocol.example.org');<br /> ws.onmessage = function (message) {<br /> var messages = document.getElementById('messages');<br /> messages.innerHTML += "<br>[in] " + message.data;<br /> };</p><p> sendmsg = function() {<br /> var message = document.getElementById('message_to_send').value<br /> document.getElementById('message_to_send').value = ''<br /> ws.send(message);<br /> var messages = document.getElementById('messages');<br /> messages.innerHTML += "<br>[out] " + message;<br /> };</p><p>// --></mce:script><br /> </head><br /> <body><br /> <form><br /> <input type="text" id="message_to_send" name="msg"/><br /> <input type="button" name="btn" id="sendMsg" value="Send" onclick="javascript:sendmsg();"><br /> <div id="messages"></div><br /> </form><br /> </body><br /></html>
當建立一個WebSocket執行個體的時候,相應的WebSocket串連就會被建立。建構函式需要兩個參數(第二個參數是可選的)。第一個參數是WebSocketURL,該參數定義了要串連的URL。WebSocketURL以ws或wss開頭。ws開頭的是普通的WebSocket串連,wss開頭的是安全的WebSocket串連(類似https)。第二個參數是要使用的子協議,該參數是可選的。
我們可以定義WebSocket執行個體的onMessage處理函數,每次收到訊息的時候,該處理函數都會被調用。如果要發送訊息到伺服器端,可以通過WebSocket的send()方法發送訊息。
當一個新的WebSocket執行個體被建立的時候,首先底層的user agent會建立一個普通的到指定URL的HTTP(S)串連,然後對該串連進行升級(upgrade)。HTTP規範在訊息頭中定義了upgrade域來進行該操作。Upgrade頭提供了簡單的機制來將HTTP協議轉換為其他的不相容的協議。WebSocket利用了HTTP協議的這個能力來講新建立的HTTP串連轉換為WebSocket串連。同時添加了WebSocket-Protocal來制定要使用的子協議。
下面是一個Upgrade的Request和Response訊息的例子。
REQUEST:<br />GET /Channel HTTP/1.1<br />Upgrade: WebSocket<br />Connection: Upgrade<br />Host: myServer:8876<br />Origin: http://myServer:8876<br />WebSocket-Protocol: mySubprotocol.example.org</p><p>RESPONSE:<br />HTTP/1.1 101 Web Socket Protocol Handshake<br />Upgrade: WebSocket<br />Connection: Upgrade<br />WebSocket-Origin: http://myServer:8876<br />WebSocket-Location: ws://myServer:8876/Channel<br />WebSocket-Protocol: mySubprotocol.example.org
當收到HTTP response訊息之後,只有WebSocket幀能夠通過該串連傳送,所有的資料都會根據WebSocket協議進行轉換。WebSocket幀能夠在任何時候向任何方向發送。WebSocket協議定義了兩種類型的幀:文本幀(text frame)和二進位幀(binary frame)。文本幀以位元組0x00開頭,以位元組0xFF結尾。當中的常值內容要轉換成UTF8編碼。所以為了打包,文本幀需要添加兩個額外的位元組。下面是兩個文本幀的例子:
Text frame of "GetDate":<br />0x00 0x47 0x65 0x74 0x44 0x61 0x74 0x65 0xFF</p><p>Text frame of "Sat Mar 13 14:00:25 CET 2010":<br />0x00 0x53 0x61 0x74 0x20 0x4D 0x61 0x72 0x20 0x31<br />0x33 0x20 0x31 0x34 0x3A 0x30 0x30 0x3A 0x32 0x35<br />0x20 0x43 0x45 0x54 0x20 0x32 0x30 0x31 0x30 0xFF
如果要傳送位元據,就需要使用二進位幀。二進位幀以位元組0x80開始。和文本幀相反,二進位幀沒有結束標誌。在二進位幀的開始標識位元組(0x80)之後就是長度位元組。長度位元組的位元組數是不固定的,根據需要來決定。下面是兩個例子,第一個例子中需要傳遞的資料量很小,因此長度位元組就只有一個位元組;第二個例子中需要傳遞的資料量比較大,就使用了2個位元組作為長度位元組。
binary frame of 0x00 0x44:<br />0x80 0x02 0x00 0x44</p><p>binary frame of 0x30 0x31 0x32 0x33 0x34 0x35 […] 0x39 (1000 bytes):<br />0x80 0x87 0x68 0x30 0x31 0x32 0x33 0x34 0x35 […] 0x39
因為JavaScript不能操作位元組數組形式的位元據,因此二進位幀目前無法被JavaScript使用。除了文本幀和二進位幀之外,WebSocket協議將來還有可能引入新的類型的框架格式。WebSocket幀在設計的時候就考慮了支援新的框架類型。
WebSocket的串連可以在任何時候關閉,不需要額外的‘結束串連’位元組或幀。
管理WebSocket的額外開銷是很小的。如Bayeux和BOSH這樣的Comet協議是建立在HTTP協議之上的,這就迫使這些協議要實現複雜的會話和串連的管理。而WebSocket是建立在TCP協議之上的,不會碰到這些由於HTTP協議的局限性引起的麻煩。
另一方面,WebSocket基本上沒有實現可靠性的功能。它既沒有包括重建串連的處理,也不支援向Server-Sent Event那樣的保證訊息成功傳遞的機制。而且,由於WebSocket不是基於HTTP協議的,因此也無法利用HTTP協議內建的可靠性的特性。例如HTTP協議支援當網路故障是的自動重試功能(一個Get方法應該不會改變任何伺服器端的資源的狀態,因此我們可以重複的執行Get方法而沒有任何的副作用。當網路故障的時候,瀏覽器或HttpClients可以自動重新執行Get方法)。
由於WebSocket沒有這些功能,因此在應用程式(或者說子協議)層面上我們就需要實現可靠性的功能,包括髮送“維持通訊”訊息來避免Proxy 伺服器在一段時間沒有訊息之後關閉串連。另外,在多個頁面之間共用WebSocket通常會帶來麻煩。和Server-Sent Event不同,WebSocket會包含一個難以共用的上遊的管道。例如,並發的讀寫操作必須要同步,這並不是一個簡單的任務。因此當使用WebSocket的時候,‘每個伺服器一個串連’必須要仔細考慮。
Web瀏覽器限制瀏覽器端的程式設計語言(如JavaScript)串連到其他的伺服器,因此web頁面上的WebSocket只能串連和當前頁面在同一個域中的伺服器。對於獨立的WebSocket用戶端則沒有這個限制。如下面的例子中我們通過Java用戶端來使用WebSocket:
class MyWebSocketHandler implements IWebSocketHandler {</p><p> public void onConnect(IWebSocketConnection wsCon) throws IOException {</p><p> }</p><p> public void onMessage(IWebSocketConnection wsCon) throws IOException {<br /> IWebMessage msg = wsCon.readMessage();<br /> System.out.println(msg.toString());<br /> }</p><p> public void onDisconnect(IWebSocketConnection wsCon) throws IOException {</p><p> }<br />}</p><p>MyWebSocketHandler hdl = new MyWebSocketHandler ();<br />IWebSocketConnection wsCon = httpClient.openWebSocketConnection(<br /> "ws://myServer:8876/WebSocketsExample",<br /> "mySubprotocol.example.org", hdl);</p><p>wsCon.writeMessage(new TextMessage("GetDate"));<br />// ...
下面是一個WebSocket伺服器實現的簡單例子。伺服器要實現兩個介面:IHttpRequestHandler和IWebSocketHandler。IHttpRequestHandler介面用於處理普通的HTTP請求,IWebSocketHandler據誒和用於處理WebSocket串連和訊息。當一個標準的HTTP請求到來的時候(不包含upgrade請求),IHttpRequestHandler的onRequest()方法會被調用。如果用戶端開啟WebSocket,伺服器會收到HTTP upgrade請求,IWebSocketHandler的onConnect()方法會被調用。每次收到WebSocket的訊息,IWebSocketHandler的onMessage()方法都會被調用。
在onConnect()方法中,我們可以檢查一些先決條件。例如檢查要求的子協議是否被支援,下面的例子中如果要求的子協議不被支援,則會返回一個錯誤狀態。下面的例子啊紅我們還駕車了要求標頭中的origin欄位。Origin欄位是HTTP Origin Header RFC(還是草案)定義的,該欄位由瀏覽器自動化佈建。
class ServerHandler implements IHttpRequestHandler, IWebSocketHandler {</p><p> // IHttpRequestHandler method<br /> public void onRequest(IHttpExchange exchange) throws IOException {<br /> String requestURI = exchange.getRequest().getRequestURI();</p><p> if (requestURI.equals("/WebSocketsExample")) {<br /> sendWebSocketPage(exchange, requestURI);</p><p> } else {<br /> exchange.sendError(404);<br /> }<br />}</p><p> private void sendWebSocketPage(IHttpExchange exchange, String uri)<br /> throws IOException {<br /> String page = "<html>/r/n " +<br /> " <head>/r/n" +<br /> " <mce:script type='text/javascript'><!--<br />/r/n" +<br /> " var ws = new WebSocket('ws://" +<br /> exchange.getRequest().getHost() + "/Channel',<br /> 'mySubprotocol.example.org');/r/n" +<br /> " ws.onmessage = function (message) {/r/n" +<br /> " var messages = document.getElementById('messages');/r/n" +<br /> " messages.innerHTML += /"<br>[in] /" +<br /> message.data;/r/n"+<br /> " };/r/n" +<br /> " /r/n" +<br /> " sendmsg = function() {/r/n" +<br /> " var message = document.getElementById<br /> ('message_to_send').value/r/n" +<br /> " document.getElementById('message_to_send').value = ''/r/n" +<br /> " ws.send(message);/r/n" +<br /> " var messages = document.getElementById('messages');/r/n" +<br /> " messages.innerHTML += /"<br>[out] /" + message;/r/n"+<br /> " };/r/n" +<br /> "<br />// --></mce:script>/r/n" +<br /> " </head>/r/n" +<br /> " <body>/r/n" +<br /> " <form>/r/n" +<br /> " <input type=/"text/" id=/"message_to_send/"<br /> name=/"msg/"/>/r/n" +<br /> " <input type=/"button/" name=/"btn/" id=/"sendMsg/"<br /> value=/"Send/" onclick=/"javascript:sendmsg();/">/r/n" +<br /> " <div id=/"messages/"></div>/r/n" +<br /> " </form>/r/n" +<br /> " </body>/r/n" +<br /> "</html>/r/n ";</p><p> exchange.send(new HttpResponse(200, "text/html", page));<br /> }</p><p> // IWebSocketHandler method<br /> public void onConnect(IWebSocketConnection webStream) throws IOException, BadMessageException {<br /> IHttpRequestHeader header = webStream.getUpgradeRequestHeader();</p><p> // check origin header<br /> String origin = header.getHeader("Origin");<br /> if (!isAllowed(origin)) {<br /> throw new BadMessageException(403);<br /> }</p><p> // check the subprotocol<br /> String subprotocol = header.getHeader("WebSocket-Protocol", "");<br /> if (!subprotocol.equalsIgnoreCase("mySubprotocol.example.org")) {<br /> throw new BadMessageException(403);<br /> }<br /> }</p><p> private boolean isAllowed(String origin) {<br /> // check the origin<br /> // ...<br /> return true;<br /> }</p><p> // IWebSocketHandler<br /> public void onMessage(IWebSocketConnection webStream) throws IOException {<br /> WebSocketMessage msg = webStream.readMessage();<br /> if (msg.toString().equalsIgnoreCase("GetDate")) {<br /> webStream.writeMessage(new TextMessage(new Date().toString()));<br /> } else {<br /> webStream.writeMessage(new TextMessage(<br /> "unknown command (supported: GetDate)"));<br /> }<br /> }</p><p> // IWebSocketHandler<br /> public void onDisconnect(IWebSocketConnection webStream)<br /> throws IOException { }</p><p>}</p><p>XHttpServer server = new XHttpServer(8876, new ServerHandler());<br />server.start();
上面的例子中,我們會使用內部的白名單來檢查origin header,並拒絕我們不期望的請求。這樣,我們可以防止某些攻擊者將公用頁面上的JavaScript代碼拷貝並添加到他們自己的頁面中。這種情況下,瀏覽器會將origin header設定為攻擊者自己的頁面所在的域,伺服器處理升級請求的時候就會拒絕該請求。這個技術用來防範Cross-Site Request攻擊。Origin header規範和WebSocket協議規範是相互獨立的,但是WebSocket協議定義了WebSocket-Origin header,該欄位必須被包含在WebSocket升級請求中。
因為WebSocket串連是有HTTP串連升級而來,因此WebSocket協議也可以和HTTPProxy 伺服器一起工作。瀏覽器總是和Proxy 伺服器通訊,Proxy 伺服器將HTTP request和response進行轉寄。當瀏覽器使用HTTP代理並開啟一個WebSocket的時候,首先瀏覽器會開啟一個到Proxy 伺服器的通道。通過發送HTTP/1.1串連請求,瀏覽器要求HTTP代理建立一個到被代理的伺服器(WebSocket伺服器)的TCP串連。當串連建立以後,HTTPProxy 伺服器的功能就被縮小為到WebSocket伺服器的TCP代理。使用這個被代理的串連,瀏覽器發送WebSocket升級請求到WebSocket伺服器。下面的列表描述了整個過程。
1. REQUEST:<br />CONNECT myServer:8876 HTTP/1.1<br />Host: myServer:8876<br />User-Agent: xLightweb/2.12-HTML5Preview6<br />Proxy-Connection: keep-alive</p><p>1. RESPONSE:<br />HTTP/1.1 200 Connection established<br />Proxy-agent: myProxy</p><p>2. REQUEST:<br />GET /Channel HTTP/1.1<br />Upgrade: WebSocket<br />Connection: Upgrade<br />Host: myServer:8876<br />Origin: http://myServer:8876<br />WebSocket-Protocol: mySubprotocol.example.org</p><p>2. RESPONSE:<br />HTTP/1.1 101 Web Socket Protocol Handshake<br />Upgrade: WebSocket<br />Connection: Upgrade<br />WebSocket-Origin: http://myServer:8876<br />WebSocket-Location: ws://myServer:8876/Channel<br />WebSocket-Protocol: mySubprotocol.example.org
轉帖:http://www.developersky.net/thread-81-1-1.html