問題
傳統的瀏覽器通訊方式主要是基於HTTP協議的請求/響應模式。早期必須通過重新整理瀏覽器來更新伺服器端的資料,後來出現的Ajax(XMLHttpRequest
是核心)技術可以不用重新整理瀏覽器更新伺服器端資料。但是這種模式的問題在於,只能通過客戶端主動請求,伺服器應答來獲得資料,而伺服器端有資料變化後無法通過推送方式主動告訴客戶端資料的變化。但是隨著網路的發展和需求的變化,越來越多的應用場景需要瀏覽器支援即時的可伺服器端推送的通訊方式。在HTML5出現之前,沒有一個官方的辦法可以做到真正意義上的基於web的通訊方案。
Hacks方式
以下就介紹幾種通過hack手段實現的web即時通訊方案。
- 應用場景:(除了hack方式裡的Flash Socket外,其餘hack方式實時性相對較差)對實時性要求較高和瀏覽器覆蓋面廣的應用,如微博私信等一些簡單即時聊天。
Ajax JSONP Polling(短輪詢)
實現思路:客戶端通過Ajax(jsonp實現跨域)的方式每隔一小段時間傳送一個請求到伺服器,伺服器立刻返回資料。
- 優點:短連線,伺服器處理簡單,支援跨域、瀏覽器相容性較好。
- 缺點:有一定延遲、伺服器壓力較大,浪費頻寬流量、大部分是無效請求。
Ajax Long Polling(長輪詢)
實現思路:客戶端通過Ajax(jsonp實現跨域)發起請求(request),伺服器不馬上返回,而是保持住這個連線,直到有資料要推送給客戶端時(或time out)才傳送響應(response)給客戶端。客戶端收到響應之後馬上再發起一個新的請求給伺服器,周而復始。
- 優點:減少輪詢次數,低延遲,瀏覽器相容性較好。
- 缺點:伺服器需要保持大量連線。
Forever Iframe(Comet Streaming)
實現思路:在客戶端(瀏覽器)中動態載入一個隱藏的iframe標籤,該標籤的src屬性指向請求的伺服器url(實際上向伺服器傳送了一個http請求),然後客戶端建立一個處理資料的函式,在伺服器通過iframe與客戶端的長連線定時輸出資料給客戶端,但是返回的資料是一個類似script標籤的文字,客戶端解析為js程式碼並執行其中的函式,從而達到通訊的目的(和jsonp類似)
程式碼示例:
- 優點:實現簡單,在所有支援iframe的瀏覽器上都可用、客戶端一次連線、伺服器多次推送。
- 缺點:無法準確知道連線狀態,IE瀏覽器在iframe請求期間,瀏覽器title一直處於載入狀態,底部狀態列也顯示正在載入,使用者體驗不好(
htmlfile
通過ActiveXObject
動態寫入記憶體可以解決此問題)。
AJAX multipart streaming(Comet Streaming)
實現思路:瀏覽器必須支援multi-part標誌,客戶端通過Ajax發出請求(request),伺服器保持住這個連線,然後可以通過HTTP1.1
的chunked encoding
機制(分塊傳輸編碼)不斷push資料給客戶端,直到timeout或者手動斷開連線。
- 優點:客戶端一次連線,伺服器資料可多次推送。
- 缺點:並非所有的瀏覽器都支援
multi-part
標誌。
Flash Socket
實現思路:在頁面中內嵌入一個使用了Socket類的Flash程式,JavaScript通過呼叫此Flash程式提供的Socket介面與伺服器端的Socket介面進行通訊,JavaScript通過Flash Socket接收到伺服器端傳送的資料。
- 優點:實現真正的即時通訊,而不是偽即時。
- 缺點:客戶端必須安裝Flash外掛;非HTTP協議,無法自動穿越防火牆。
Flash Socket API
程式碼示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package { import flash.display.Sprite; public class SocketExample extends Sprite { private var socket:CustomSocket; public function SocketExample() { socket = new CustomSocket("localhost", 80); } } } import flash.errors.*; import flash.events.*; import flash.net.Socket; class CustomSocket extends Socket { private var response:String; public function CustomSocket(host:String = null, port:uint = 0) { super(); configureListeners(); if (host && port) { super.connect(host, port); } } //監聽事件 private function configureListeners():void { addEventListener(Event.CLOSE, closeHandler); addEventListener(Event.CONNECT, connectHandler); addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); addEventListener(ProgressEvent.SOCKET_DATA, socketDataHandler); } //寫文字 private function writeln(str:String):void { str += "\n"; try { writeUTFBytes(str); } catch(e:IOError) { trace(e); } } //傳送資料 private function sendRequest():void { trace("sendRequest"); response = ""; writeln("GET /"); flush(); } //讀取資料 private function readResponse():void { var str:String = readUTFBytes(bytesAvailable); response += str; } //關閉連線 private function closeHandler(event:Event):void { trace("closeHandler: " + event); trace(response.toString()); } //連線建立成功 private function connectHandler(event:Event):void { trace("connectHandler: " + event); sendRequest(); } //io錯誤 private function ioErrorHandler(event:IOErrorEvent):void { trace("ioErrorHandler: " + event); } //安全錯誤 private function securityErrorHandler(event:SecurityErrorEvent):void { trace("securityErrorHandler: " + event); } //接收socket資料 private function socketDataHandler(event:ProgressEvent):void { trace("socketDataHandler: " + event); readResponse(); } } |
解決方案(推薦)
Websocket
WebSocket是HTML5開始提供的一種瀏覽器與伺服器間進行全雙工通訊的網路技術。依靠這種技術可以實現客戶端和伺服器端的長連線,雙向實時通訊。
- 優點:較少的控制開銷、更強的實時性、長連線,雙向通訊、更好的二進位制支援。與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易被遮蔽,能通過各種 HTTP 代理伺服器。
- 缺點:部分瀏覽器不支援(支援的瀏覽器會越來越多)。
- 應用場景:較新瀏覽器支援、不受框架限制、較高擴充套件性。
瀏覽器支援情況:
基於HTTP的握手請求(handshake)
Http、WebSocket等協議屬於應用層協議,IP協議工作在網路層,TCP協議工作在傳輸層。HTTP、WebSocket等應用層協議,都是基於TCP協議來傳輸資料的。WebSocket依賴一種升級的Http協議進行一次握手,握手成功後,資料就直接從TCP通道傳輸。
1、傳送握手請求
客戶端到伺服器的握手請求:
1 2 3 4 5 6 7 8 |
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 |
欄位:
- Origin:用來防止跨域攻擊
- Sec-WebSocket-Key:是伺服器端需要使用客戶端傳送的這個Key進行校驗,然後返回一個校驗過的字串給客戶端,客戶端驗證通過後才能正式建立Socket連線
2、返回握手應答
伺服器返回正確的相應頭後,客戶端驗證後將建立連線,此時狀態為OPEN。
伺服器響應頭如下:
1 2 3 4 5 |
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat |
欄位:
- Sec-WebSocket-Accept:伺服器端將加密處理後的握手Key通過這個欄位返回給客戶端表示伺服器同意握手建立連線。
心跳(heartbeat)
握手成功後,客戶端和伺服器端任何一方都可以間隔一定時間傳送ping請求到另外一方,當ping請求到達另外一方後,對方馬上傳送pong應答訊息,通過這種方式確保連線的存在,也可以在極端情況下(如拔網線)知道連線是否斷開。
客戶端程式碼示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var socket = new WebSocket("ws://localhost:8080"); socket.onopen = function(evt) { console.log("socket is open"); socket.send("Hello World!"); }; socket.onmessage = function(evt) { console.log( "Received data: " + evt.data); socket.close(); }; socket.onerror = function(evt) { console.log( "websocket error:" + evt.message); }; socket.onclose = function(evt) { console.log("socket is closed."); }; |
客戶端核心API
websocket所有API詳見這裡
WebSocket建構函式
var socket = new WebSocket(“ws://localhost:8080”);
webSocket.readyState
1 2 3 4 |
CONNECTING:0,正在連線。 OPEN:1,連線成功,可以通訊了。 CLOSING:2,連線正在關閉。 CLOSED:3,連線已經關閉,或者開啟連線失敗。 |
webSocket.onopen
用於指定連線成功後的回撥函式。
webSocket.onclose
用於指定連線關閉後的回撥函式。
webSocket.onmessage
用於指定收到伺服器資料後的回撥函式。伺服器資料可能是文字,也可能是二進位制資料(blob物件或Arraybuffer物件)。可以使用binaryType屬性,顯式指定收到的二進位制資料型別。
webSocket.send()
用於向伺服器傳送資料。
webSocket.onerror
用於指定報錯時的回撥函式。
socket.io(推薦)
socket.io 是一個為實時應用提供跨平臺實時通訊的庫。socket.io 旨在使實時應用在每個瀏覽器和移動裝置上成為可能,模糊不同的傳輸機制之間的差異。
socket.io 的名字源於它使用了瀏覽器支援並採用的 HTML5 WebSocket 標準,因為並不是所有的瀏覽器都支援 WebSocket ,所以該庫支援一系列降級功能:
- Websocket
- Adobe® Flash® Socket
- AJAX long polling
- AJAX multipart streaming
- Forever Iframe
- JSONP Polling
在大部分情境下,你都能通過這些功能選擇與瀏覽器保持類似長連線的功能。
- 優點:跨平臺、相容性好、具有降級功能、所有傳輸機制介面對外統一、自帶心跳。
- 缺點:要使用socket.io必須前後端都要用一套框架。
- 適用於:考慮更多相容性,後端可以使用基於socket.io的框架的情景。(常見服務端實現框架有node.js,Netty-socket.io)
客戶端程式碼示例:
1 2 3 4 5 6 7 8 |
<script src="/socket.io/socket.io.js"></script> <script> var socket = io('http://localhost'); socket.on('news', function (data) { console.log(data); socket.emit('my other event', { my: 'data' }); }); </script> |
伺服器端示例程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
var app = require('http').createServer(handler) var io = require('socket.io')(app); var fs = require('fs'); app.listen(80); function handler (req, res) { fs.readFile(__dirname + '/index.html', function (err, data) { if (err) { res.writeHead(500); return res.end('Error loading index.html'); } res.writeHead(200); res.end(data); }); } io.on('connection', function (socket) { socket.emit('news', { hello: 'world' }); socket.on('my other event', function (data) { console.log(data); }); }); |
客戶端核心API
socket.io所有API詳見這兒
io.connect 建立一個連線
var socket = io.connect('ws://127.0.0.1:3000');
socket.emit 傳送一個事件給伺服器端(傳送資料)
socket.on 監聽一個伺服器端emit傳送的事件(接收資料)
三種預設的事件(客戶端和伺服器都有):connect 、message 、disconnect。
伺服器端核心API
io.on 監聽客戶端的連線事件
1 2 |
io.on('connection', function (socket) { } |
socket.emit 傳送一個事件給客戶端(推送資料)
socket.on 監聽一個客戶端emit傳送的事件(接收資料)
聊天demo
websocket框架實現
常用的 Node 實現有以下幾種。
- µWebSockets
- Socket.IO
- WebSocket-Node
- websocketd
Java實現:
- netty-socketio