有關IM(InstantMessaging)聊天應用(如:微信,QQ)、消息推送技術(如:現今移動端APP標配的消息推送模塊)等即時通信應用場景下,大多數都是桌面應用程序或者native應用較爲流行,而網上關於原生IM(相關文章請參見:《IM架構篇》、《IM綜合資料》、《IM/推送的通訊格式、協議篇》、《IM心跳保活篇》、《IM安全篇》、《實時音視頻開發》)、消息推送應用(參見:《推送技術好文》)的通訊原理介紹也較多,此處再也不贅述。javascript
而web端的IM應用,因爲瀏覽器的兼容性以及其固有的「客戶端請求服務器處理並響應」的通訊模型,形成了要在瀏覽器中實現一個兼容性較好的IM應用,其通訊過程必然是諸多技術的組合,本文的目的就是要詳細探討這些技術並分析其原理和過程。php
Web端即時通信技術盤點請參見:html
《Web端即時通信技術盤點:短輪詢、Comet、Websocket、SSE》
關於Ajax短輪詢:
找這方面的資料沒什麼意義,除非忽悠客戶,不然請考慮其它3種方案便可。
有關Comet技術的詳細介紹請參見:
《Comet技術詳解:基於HTTP長鏈接的Web端實時通訊技術》
《WEB端即時通信:HTTP長鏈接、長輪詢(long polling)詳解》
《WEB端即時通信:不用WebSocket也同樣能搞定消息的即時性》
《開源Comet服務器iComet:支持百萬併發的Web端即時通信方案》
有關WebSocket的詳細介紹請參見:
《WebSocket詳解(一):初步認識WebSocket技術》
《WebSocket詳解(二):技術原理、代碼演示和應用案例》
《WebSocket詳解(三):深刻WebSocket通訊協議細節》
《Socket.IO介紹:支持WebSocket、用於WEB端的即時通信的框架》
《socket.io和websocket 之間是什麼關係?有什麼區別?》
有關SSE的詳細介紹文章請參見:
《SSE技術詳解:一種全新的HTML5服務器推送事件技術》
更多WEB端即時通信文章請見:
http://www.52im.net/forum.php?mod=collection&action=view&ctid=15java
瀏覽器自己做爲一個瘦客戶端,不具有直接經過系統調用來達到和處於異地的另一個客戶端瀏覽器通訊的功能。這和咱們桌面應用的工做方式是不一樣的,一般桌面應用經過socket能夠和遠程主機上另一端的一個進程創建TCP鏈接,從而達到全雙工的即時通訊。
瀏覽器從誕生開始一直走的是客戶端請求服務器,服務器返回結果的模式,即便發展至今仍然沒有任何改變。因此能夠確定的是,要想實現兩個客戶端的通訊,必然要經過服務器進行信息的轉發。例如A要和B通訊,則應該是A先把信息發送給IM應用服務器,服務器根據A信息中攜帶的接收者將它再轉發給B,一樣B到A也是這種模式,以下所示:
git
咱們認識到基於web實現IM軟件依然要走瀏覽器請求服務器的模式,這這種方式下,針對IM軟件的開發須要解決以下三個問題:
github
即時通信網注:關於瀏覽器跨域訪問致使的安全問題,有一個被稱爲CSRF網絡攻擊方式,請看下面的摘錄:
web
CSRF(Cross-site request forgery),中文名稱:跨站請求僞造,也被稱爲:one click attack/session riding,縮寫爲:CSRF/XSRF。
你這能夠這麼理解CSRF攻擊:攻擊者盜用了你的身份,以你的名義發送惡意請求。CSRF可以作的事情包括:以你名義發送郵件,發消息,盜取你的帳號,甚至於購買商品,虛擬貨幣轉帳......形成的問題包括:我的隱私泄露以及財產安全。
CSRF這種攻擊方式在2000年已經被國外的安全人員提出,但在國內,直到06年纔開始被關注,08年,國內外的多個大型社區和交互網站分別爆出CSRF漏洞,如:NYTimes.com(紐約時報)、Metafilter(一個大型的BLOG網站),YouTube和百度HI......而如今,互聯網上的許多站點仍對此毫無防備,以致於安全業界稱CSRF爲「沉睡的巨人」。chrome
基於以上分析,下面針對這三個問題給出解決方案。
json
這是最簡單的一種解決方案,其原理是在客戶端經過Ajax的方式的方式每隔一小段時間就發送一個請求到服務器,服務器返回最新數據,而後客戶端根據得到的數據來更新界面,這樣就間接實現了即時通訊。優勢是簡單,缺點是對服務器壓力較大,浪費帶寬流量(一般狀況下數據都是沒有發生改變的)。
客戶端代碼以下:
跨域
01
02
03
04
05
06
07
08
09
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
|
function
createXHR(){
if
(
typeof
XMLHttpRequest !=
'undefined'
){
return
new
XMLHttpRequest();
}
else
if
(
typeof
ActiveXObject !=
'undefined'
){
if
(
typeof
arguments.callee.activeXString!=
"string"
){
var
versions=[
"MSXML2.XMLHttp.6.0"
,
"MSXML2.XMLHttp.3.0"
,
"MSXML2.XMLHttp"
],
i,len;
for
(i=0,len=versions.length;i<len;i++){
try
{
new
ActiveXObject(versions[i]);
arguments.callee.activeXString=versions[i];
break
;
}
catch
(ex) {
}
}
}
return
new
ActiveXObject(arguments.callee.activeXString);
}
else
{
throw
new
Error(
"no xhr object available"
);
}
}
function
polling(url,method,data){
method=method ||
'get'
;
data=data ||
null
;
var
xhr=createXHR();
xhr.onreadystatechange=
function
(){
if
(xhr.readyState==4){
if
(xhr.status>=200&&xhr.status<300||xhr.status==304){
console.log(xhr.responseText);
}
else
{
console.log(
"fail"
);
}
}
};
xhr.open(method,url,
true
);
xhr.send(data);
}
setInterval(
function
(){
polling(
'http://localhost:8088/time'
,
'get'
);
},2000);
|
建立一個XHR對象,每2秒就請求服務器一次獲取服務器時間並打印出來。
服務端代碼(Node.js):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/time'
){
//res.writeHead(200, {'Content-Type': 'text/plain','Access-Control-Allow-Origin':'http://localhost'});
res.end(
new
Date().toLocaleString());
};
if
(req.url==
'/'
){
fs.readFile(
"./pollingClient.html"
,
"binary"
,
function
(err, file) {
if
(!err) {
res.writeHead(200, {
'Content-Type'
:
'text/html'
});
res.write(file,
"binary"
);
res.end();
}
});
}
}).listen(8088,
'localhost'
);
server.on(
'connection'
,
function
(socket){
console.log(
"客戶端鏈接已經創建"
);
});
server.on(
'close'
,
function
(){
console.log(
'服務器被關閉'
);
});
|
結果以下:
在上面的輪詢解決方案中,因爲每次都要發送一個請求,服務端無論數據是否發生變化都發送數據,請求完成後鏈接關閉。這中間通過的不少通訊是沒必要要的,因而又出現了長輪詢(long-polling)方式。這種方式是客戶端發送一個請求到服務器,服務器查看客戶端請求的數據是否發生了變化(是否有最新數據),若是發生變化則當即響應返回,不然保持這個鏈接並按期檢查最新數據,直到發生了數據更新或鏈接超時。同時客戶端鏈接一旦斷開,則再次發出請求,這樣在相同時間內大大減小了客戶端請求服務器的次數。代碼以下。(詳細技術文章請參見《WEB端即時通信:HTTP長鏈接、長輪詢(long polling)詳解》)
客戶端:
01
02
03
04
05
06
07
08
09
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
|
function
createXHR(){
if
(
typeof
XMLHttpRequest !=
'undefined'
){
return
new
XMLHttpRequest();
}
else
if
(
typeof
ActiveXObject !=
'undefined'
){
if
(
typeof
arguments.callee.activeXString!=
"string"
){
var
versions=[
"MSXML2.XMLHttp.6.0"
,
"MSXML2.XMLHttp.3.0"
,
"MSXML2.XMLHttp"
],
i,len;
for
(i=0,len=versions.length;i<len;i++){
try
{
new
ActiveXObject(versions[i]);
arguments.callee.activeXString=versions[i];
break
;
}
catch
(ex) {
}
}
}
return
new
ActiveXObject(arguments.callee.activeXString);
}
else
{
throw
new
Error(
"no xhr object available"
);
}
}
function
longPolling(url,method,data){
method=method ||
'get'
;
data=data ||
null
;
var
xhr=createXHR();
xhr.onreadystatechange=
function
(){
if
(xhr.readyState==4){
if
(xhr.status>=200&&xhr.status<300||xhr.status==304){
console.log(xhr.responseText);
}
else
{
console.log(
"fail"
);
}
longPolling(url,method,data);
}
};
xhr.open(method,url,
true
);
xhr.send(data);
}
longPolling(
'http://localhost:8088/time'
,
'get'
);
|
在XHR對象的readySate爲4的時候,表示服務器已經返回數據,本次鏈接已斷開,再次請求服務器創建鏈接。
服務端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/time'
){
setInterval(
function
(){
sendData(res);
},20000);
};
if
(req.url==
'/'
){
fs.readFile(
"./lpc.html"
,
"binary"
,
function
(err, file) {
if
(!err) {
res.writeHead(200, {
'Content-Type'
:
'text/html'
});
res.write(file,
"binary"
);
res.end();
}
});
}
}).listen(8088,
'localhost'
);
//用隨機數模擬數據是否變化
function
sendData(res){
var
randomNum=Math.floor(10*Math.random());
console.log(randomNum);
if
(randomNum>=0&&randomNum<=5){
res.end(
new
Date().toLocaleString());
}
}
|
在服務端經過生成一個在1到9之間的隨機數來模擬判斷數據是否發生了變化,當隨機數在0到5之間表示數據發生了變化,直接返回,不然保持鏈接,每隔2秒再檢測。
結果以下:
能夠看到返回的時間是沒有規律的,而且單位時間內返回的響應數相比polling方式較少。
上面的long-polling技術爲了保持客戶端與服務端的長鏈接採起的是服務端阻塞(保持響應不返回),客戶端輪詢的方式,在Comet技術中(詳細技術文章請參見《Comet技術詳解:基於HTTP長鏈接的Web端實時通訊技術》),還存在一種基於http-stream流的通訊方式。其原理是讓客戶端在一次請求中保持和服務端鏈接不斷開,而後服務端源源不斷傳送數據給客戶端,就比如數據流同樣,並非一次性將數據所有發給客戶端。它與polling方式的區別在於整個通訊過程客戶端只發送一次請求,而後服務端保持與客戶端的長鏈接,並利用這個鏈接在回送數據給客戶端。
這種方案有分爲幾種不一樣的數據流傳輸方式。
這種方式的思想是構造一個XHR對象,經過監聽它的onreadystatechange事件,當它的readyState爲3的時候,獲取它的responseText而後進行處理,readyState爲3表示數據傳送中,整個通訊過程尚未結束,因此它還在不斷獲取服務端發送過來的數據,直到readyState爲4的時候才表示數據發送完畢,一次通訊過程結束。在這個過程當中,服務端傳給客戶端的數據是分屢次以stream的形式發送給客戶端,客戶端也是經過stream形式來獲取的,因此稱做http-streaming數據流方式,代碼以下。
客戶端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
function
createStreamClient(url,progress,done){
//received爲接收到數據的計數器
var
xhr=
new
XMLHttpRequest(),received=0;
xhr.open(
"get"
,url,
true
);
xhr.onreadystatechange=
function
(){
var
result;
if
(xhr.readyState==3){
//console.log(xhr.responseText);
result=xhr.responseText.substring(received);
received+=result.length;
progress(result);
}
else
if
(xhr.readyState==4){
done(xhr.responseText);
}
};
xhr.send(
null
);
return
xhr;
}
var
client=createStreamClient(
"http://localhost:8088/stream"
,
function
(data){
console.log(
"Received:"
+data);
},
function
(data){
console.log(
"Done,the last data is:"
+data);
})
|
這裏因爲客戶端收到的數據是分段發過來的,因此最好定義一個遊標received,來獲取最新數據而捨棄以前已經接收到的數據,經過這個遊標每次將接收到的最新數據打印出來,而且在通訊結束後打印出整個responseText。
服務端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
count=0;
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/stream'
){
res.setHeader(
'content-type'
,
'multipart/octet-stream'
);
var
timer=setInterval(
function
(){
sendRandomData(timer,res);
},2000);
};
if
(req.url==
'/'
){
fs.readFile(
"./xhr-stream.html"
,
"binary"
,
function
(err, file) {
if
(!err) {
res.writeHead(200, {
'Content-Type'
:
'text/html'
});
res.write(file,
"binary"
);
res.end();
}
});
}
}).listen(8088,
'localhost'
);
function
sendRandomData(timer,res){
var
randomNum=Math.floor(10000*Math.random());
console.log(randomNum);
if
(count++==10){
clearInterval(timer);
res.end(randomNum.toString());
}
res.write(randomNum.toString());
}
|
服務端經過計數器count將數據分十次發送,每次生成一個小於10000的隨機數發送給客戶端讓它進行處理。
結果以下:
能夠看到每次傳過來的數據流都進行了處理,同時打印出了整個最終接收到的完整數據。這種方式間接實現了客戶端請求,服務端及時推送數據給客戶端。
因爲低版本的IE不容許在XHR的readyState爲3的時候獲取其responseText屬性,爲了達到在IE上使用這個技術,又出現了基於iframe的數據流通訊方式。具體來說,就是在瀏覽器中動態載入一個iframe,讓它的src屬性指向請求的服務器的URL,實際上就是向服務器發送了一個http請求,而後在瀏覽器端建立一個處理數據的函數,在服務端經過iframe與瀏覽器的長鏈接定時輸出數據給客戶端,可是這個返回的數據並非通常的數據,而是一個相似於<script type=\"text/javascript\">parent.process('"+randomNum.toString()+"')</script>腳本執行的方式,瀏覽器接收到這個數據就會將它解析成js代碼並找到頁面上指定的函數去執行,其實是服務端間接使用本身的數據間接調用了客戶端的代碼,達到實時更新客戶端的目的。
客戶端代碼以下:
1
2
3
4
5
6
7
8
9
|
function
process(data){
console.log(data);
}
var
dataStream =
function
(url) {
var
ifr = document.createElement(
"iframe"
),timer;
ifr.src = url;
document.body.appendChild(ifr);
};
dataStream(
'http://localhost:8088/htmlfile'
);
|
客戶端爲了簡單起見,定義對數據處理就是打印出來。
服務端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
count=0;
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/htmlfile'
){
res.setHeader(
'content-type'
,
'text/html'
);
var
timer=setInterval(
function
(){
sendRandomData(timer,res);
},2000);
};
if
(req.url==
'/'
){
fs.readFile(
"./htmlfile-stream.html"
,
"binary"
,
function
(err, file) {
if
(!err) {
res.writeHead(200, {
'Content-Type'
:
'text/html'
});
res.write(file,
"binary"
);
res.end();
}
});
}
}).listen(8088,
'localhost'
);
function
sendRandomData(timer,res){
var
randomNum=Math.floor(10000*Math.random());
console.log(randomNum.toString());
if
(count++==10){
clearInterval(timer);
res.end(
"<script type=\"text/javascript\">parent.process('"
+randomNum.toString()+
"')</script>"
);
}
res.write(
"<script type=\"text/javascript\">parent.process('"
+randomNum.toString()+
"')</script>"
);
}
|
服務端定時發送隨機數給客戶端,並調用客戶端process函數。
在IE5中測試結果以下:
能夠看到實如今低版本IE中客戶端到服務器的請求-推送的即時通訊。
又出現新問題了,在IE中,使用iframe請求服務端,服務端保持通訊鏈接沒有所有返回以前,瀏覽器title一直處於加載狀態,而且底部也顯示正在加載,這對於一個產品來說用戶體驗是很差的,因而谷歌的天才們又想出了一中hack方式。就是在IE中,動態生成一個htmlfile對象,這個對象ActiveX形式的com組件,它實際上就是一個在內存中實現的HTML文檔,經過將生成的iframe添加到這個內存中的HTMLfile中,並利用iframe的數據流通訊方式達到上面的效果。同時因爲HTMLfile對象並非直接添加到頁面上的,因此並無形成瀏覽器顯示正在加載的現象。代碼以下。
客戶端:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
function
connect_htmlfile(url, callback) {
var
transferDoc =
new
ActiveXObject(
"htmlfile"
);
transferDoc.open();
transferDoc.write(
"<!DOCTYPE html><html><body><script type=\"text/javascript\">"
+
"document.domain='"
+ document.domain +
"';"
+
"<\/script><\/body><\/html>"
);
transferDoc.close();
var
ifrDiv = transferDoc.createElement(
"div"
);
transferDoc.body.appendChild(ifrDiv);
ifrDiv.innerHTML =
"<iframe src='"
+ url +
"'><\/iframe>"
;
transferDoc.callback=callback;
setInterval(
function
() {}, 10000);
}
function
prograss(data) {
alert(data);
}
connect_htmlfile(
'http://localhost:8088/htmlfile'
,prograss);
|
服務端傳送給iframe的是這樣子:
1
|
<
script
type=\"text/javascript\">callback.process('"+randomNum.toString()+"')</
script
>
|
這樣就在iframe流的原有方式下避免了瀏覽器的加載狀態。
爲了解決瀏覽器只可以單向傳輸數據到服務端,HTML5提供了一種新的技術叫作服務器推送事件SSE(關於該技術詳細介紹請參見《SSE技術詳解:一種全新的HTML5服務器推送事件技術》),它可以實現客戶端請求服務端,而後服務端利用與客戶端創建的這條通訊鏈接push數據給客戶端,客戶端接收數據並處理的目的。從獨立的角度看,SSE技術提供的是從服務器單向推送數據給瀏覽器的功能,可是配合瀏覽器主動請求,實際上就實現了客戶端和服務器的雙向通訊。它的原理是在客戶端構造一個eventSource對象,該對象具備readySate屬性,分別表示以下:
同時eventSource對象會保持與服務器的長鏈接,斷開後會自動重連,若是要強制鏈接能夠調用它的close方法。能夠它的監聽onmessage事件,服務端遵循SSE數據傳輸的格式給客戶端,客戶端在onmessage事件觸發時就可以接收到數據,從而進行某種處理,代碼以下。
客戶端:
01
02
03
04
05
06
07
08
09
10
|
var
source=
new
EventSource(
'http://localhost:8088/evt'
);
source.addEventListener(
'message'
,
function
(e) {
console.log(e.data);
},
false
);
source.onopen=
function
(){
console.log(
'connected'
);
}
source.onerror=
function
(err){
console.log(err);
}
|
服務端:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
count=0;
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/evt'
){
//res.setHeader('content-type', 'multipart/octet-stream');
res.writeHead(200, {
"Content-Type"
:
"tex"
+
"t/event-stream"
,
"Cache-Control"
:
"no-cache"
,
'Access-Control-Allow-Origin'
:
'*'
,
"Connection"
:
"keep-alive"
});
var
timer=setInterval(
function
(){
if
(++count==10){
clearInterval(timer);
res.end();
}
else
{
res.write(
'id: '
+ count +
'\n'
);
res.write(
"data: "
+
new
Date().toLocaleString() +
'\n\n'
);
}
},2000);
};
if
(req.url==
'/'
){
fs.readFile(
"./sse.html"
,
"binary"
,
function
(err, file) {
if
(!err) {
res.writeHead(200, {
'Content-Type'
:
'text/html'
});
res.write(file,
"binary"
);
res.end();
}
});
}
}).listen(8088,
'localhost'
);
|
注意:這裏服務端發送的數據要遵循必定的格式,一般是id:(空格)數據(換行符)data:(空格)數據(兩個換行符),若是不遵循這種格式,實際上客戶端是會觸發error事件的。這裏的id是用來標識每次發送的數據的id,是強制要加的。
結果以下:
以上就是比較經常使用的客戶端服務端雙向即時通訊的解決方案,下面再來看如何實現跨域。
關於跨域是什麼,限於篇幅所限,這裏不作介紹,網上有不少詳細的文章,這裏只列舉解決辦法。
CORS(跨域資源共享)是一種容許瀏覽器腳本向出於不一樣域名下服務器發送請求的技術,它是在原生XHR請求的基礎上,XHR調用open方法時,地址指向一個跨域的地址,在服務端經過設置'Access-Control-Allow-Origin':'*'響應頭部告訴瀏覽器,發送的數據是一個來自於跨域的而且服務器容許響應的數據,瀏覽器接收到這個header以後就會繞過日常的跨域限制,從而和平時的XHR通訊沒有區別。該方法的主要好處是在於客戶端代碼不用修改,服務端只須要添加'Access-Control-Allow-Origin':'*'頭部便可。適用於ff,safari,opera,chrome等非IE瀏覽器。跨域的XHR相比非跨域的XHR有一些限制,這是爲了安全所須要的,主要有如下限制:
以上這些措施都是爲了安全考慮,防止常見的跨站點腳本攻擊(XSS)和跨站點請求僞造(CSRF)。
客戶端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
var
polling=
function
(){
var
xhr=
new
XMLHttpRequest();
xhr.onreadystatechange=
function
(){
if
(xhr.readyState==4)
if
(xhr.status==200){
console.log(xhr.responseText);
}
}
xhr.open(
'get'
,
'http://localhost:8088/cors'
);
xhr.send(
null
);
};
setInterval(
function
(){
polling();
},1000);
|
服務端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
var
http=require(
'http'
);
var
fs = require(
"fs"
);
var
server=http.createServer(
function
(req,res){
if
(req.url==
'/cors'
){
res.writeHead(200, {
'Content-Type'
:
'text/plain'
,
'Access-Control-Allow-Origin'
:
'http://localhost'
});
res.end(
new
Date().toString());
}
if
(req.url==
'/jsonp'
){
}
}).listen(8088,
'localhost'
);
server.on(
'connection'
,
function
(socket){
console.log(
"客戶端鏈接已經創建"
);
});
server.on(
'close'
,
function
(){
console.log(
'服務器被關閉'
);
});
|
注意服務端須要設置頭部Access-Control-Allow-Origin爲須要跨域的域名。
這裏爲了測試在端口8088上監聽請求,而後讓客戶端在80端口上請求服務,結果以下:
對於IE8-10,它是不支持使用原生的XHR對象請求跨域服務器的,它本身實現了一個XDomainRequest對象,相似於XHR對象,可以發送跨域請求,它主要有如下限制:
客戶端請求代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
var
polling=
function
(){
var
xdr=
new
XDomainRequest();
xdr.onload=
function
(){
console.log(xdr.responseText);
};
xdr.onerror=
function
(){
console.log(
'failed'
);
};
xdr.open(
'get'
,
'http://localhost:8088/cors'
);
xdr.send(
null
);
};
setInterval(
function
(){
polling();
},1000);
|
服務端代碼和同上,在IE8中測試結果以下:
這種方式不須要在服務端添加Access-Control-Allow-Origin頭信息,其原理是利用HTML頁面上script標籤對跨域沒有限制的特色,讓它的src屬性指向服務端請求的地址,實際上是經過script標籤發送了一個http請求,服務器接收到這個請求以後,返回的數據是本身的數據加上對客戶端JS函數的調用,其原理相似於咱們上面所說的iframe流的方式,客戶端瀏覽器接收到返回的腳本調用會解析執行,從而達到更新界面的目的。
客戶端代碼以下:
01
02
03
04
05
06
07
08
09
10
11
12
|
function
callback(data){
console.log(
"得到的跨域數據爲:"
+data);
}
function
sendJsonp(url){
var
oScript=document.createElement(
"script"
);
oScript.src=url;
oScript.setAttribute(
'type'
,
"text/javascript"
);
document.getElementsByTagName(
'head'
)[0].appendChild(oScript);
}
setInterval(
function
(){
sendJsonp(
'http://localhost:8088/jsonp?cb=callback'
);
},1000);
|
服務端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
var
http=require(
'http'
);
var
url=require(
'url'
);
var
server=http.createServer(
function
(req,res){
if
(/\/jsonp/.test(req.url)){
var
urlData=url.parse(req.url,
true
);
var
methodName=urlData.query.cb;
res.writeHead(200,{
'Content-Type'
:
'application/javascript'
});
//res.end("<script type=\"text/javascript\">"+methodName+"("+new Date().getTime()+");</script>");
res.end(methodName+
"("
+
new
Date().getTime()+
");"
);
//res.end(new Date().toString());
}
}).listen(8088,
'localhost'
);
server.on(
'connection'
,
function
(socket){
console.log(
"客戶端鏈接已經創建"
);
});
server.on(
'close'
,
function
(){
console.log(
'服務器被關閉'
);
});
|
注意這裏服務端輸出的數據content-type首部要設定爲application/javascript,不然某些瀏覽器會將其當作文本解析。
結果以下:
在上面的這些解決方案中,都是利用瀏覽器單向請求服務器或者服務器單向推送數據到瀏覽器這些技術組合在一塊兒而造成的hack技術,在HTML5中,爲了增強web的功能,提供了websocket技術,它不只是一種web通訊方式,也是一種應用層協議。它提供了瀏覽器和服務器之間原生的雙全工跨域通訊,經過瀏覽器和服務器之間創建websocket鏈接(其實是TCP鏈接),在同一時刻可以實現客戶端到服務器和服務器到客戶端的數據發送。關於該技術的原理,請參見:《WebSocket詳解(一):初步認識WebSocket技術》、《WebSocket詳解(二):技術原理、代碼演示和應用案例》、《WebSocket詳解(三):深刻WebSocket通訊協議細節》,此處就不在贅述了,直接給出代碼。在看代碼以前,須要先了解websocket整個工做過程。
首先是客戶端new 一個websocket對象,該對象會發送一個http請求到服務端,服務端發現這是個webscoket請求,會贊成協議轉換,發送回客戶端一個101狀態碼的response,以上過程稱之爲一次握手,通過此次握手以後,客戶端就和服務端創建了一條TCP鏈接,在該鏈接上,服務端和客戶端就能夠進行雙向通訊了。這時的雙向通訊在應用層走的就是ws或者wss協議了,和http就沒有關係了。所謂的ws協議,就是要求客戶端和服務端遵循某種格式發送數據報文(幀),而後對方纔可以理解。
關於ws協議要求的數據格式官網指定以下:
其中比較重要的是FIN字段,它佔用1位,表示這是一個數據幀的結束標誌,同時也下一個數據幀的開始標誌。opcode字段,它佔用4位,當爲1時,表示傳遞的是text幀,2表示二進制數據幀,8表示須要結束這次通訊(就是客戶端或者服務端哪一個發送給對方這個字段,就表示對方要關閉鏈接了)。9表示發送的是一個ping數據。mask佔用1位,爲1表示masking-key字段可用,masking-key字段是用來對客戶端發送來的數據作unmask操做的。它佔用0到4個字節。Payload字段表示實際發送的數據,能夠是字符數據也能夠是二進制數據。
因此無論是客戶端和服務端向對方發送消息,都必須將數據組裝成上面的幀格式來發送。
首先來看服務端代碼:
01
02
03
04
05
06
07
08
09
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
78
79
80
|
//握手成功以後就能夠發送數據了
var
crypto = require(
'crypto'
);
var
WS =
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
;
var
server=require(
'net'
).createServer(
function
(socket) {
var
key;
socket.on(
'data'
,
function
(msg) {
if
(!key) {
//獲取發送過來的Sec-WebSocket-key首部
key = msg.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
key = crypto.createHash(
'sha1'
).update(key + WS).digest(
'base64'
);
socket.write(
'HTTP/1.1 101 Switching Protocols\r\n'
);
socket.write(
'Upgrade: WebSocket\r\n'
);
socket.write(
'Connection: Upgrade\r\n'
);
//將確認後的key發送回去
socket.write(
'Sec-WebSocket-Accept: '
+ key +
'\r\n'
);
//輸出空行,結束Http頭
socket.write(
'\r\n'
);
}
else
{
var
msg=decodeData(msg);
console.log(msg);
//若是客戶端發送的操做碼爲8,表示斷開鏈接,關閉TCP鏈接並退出應用程序
if
(msg.Opcode==8){
socket.end();
server.unref();
}
else
{
socket.write(encodeData({FIN:1,
Opcode:1,
PayloadData:
"接受到的數據爲"
+msg.PayloadData}));
}
}
});
});
server.listen(8000,
'localhost'
);
//按照websocket數據幀格式提取數據
function
decodeData(e){
var
i=0,j,s,frame={
//解析前兩個字節的基本數據
FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
PayloadLength:e[i++]&0x7F
};
//處理特殊長度126和127
if
(frame.PayloadLength==126)
frame.length=(e[i++]<<8)+e[i++];
if
(frame.PayloadLength==127)
i+=4,
//長度通常用四字節的整型,前四個字節一般爲長整形留空的
frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
//判斷是否使用掩碼
if
(frame.Mask){
//獲取掩碼實體
frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
//對數據和掩碼作異或運算
for
(j=0,s=[];j<frame.PayloadLength;j++)
s.push(e[i+j]^frame.MaskingKey[j%4]);
}
else
s=e.slice(i,frame.PayloadLength);
//不然直接使用數據
//數組轉換成緩衝區來使用
s=
new
Buffer(s);
//若是有必要則把緩衝區轉換成字符串來使用
if
(frame.Opcode==1)s=s.toString();
//設置上數據部分
frame.PayloadData=s;
//返回數據幀
return
frame;
}
//對發送數據進行編碼
function
encodeData(e){
var
s=[],o=
new
Buffer(e.PayloadData),l=o.length;
//輸入第一個字節
s.push((e.FIN<<7)+e.Opcode);
//輸入第二個字節,判斷它的長度並放入相應的後續長度消息
//永遠不使用掩碼
if
(l<126)s.push(l);
else
if
(l<0x10000)s.push(126,(l&0xFF00)>>2,l&0xFF);
else
s.push(
127, 0,0,0,0,
//8字節數據,前4字節通常沒用留空
(l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF
);
//返回頭部分和數據部分的合併緩衝區
return
Buffer.concat([
new
Buffer(s),o]);
}
|
服務端經過監聽data事件來獲取客戶端發送來的數據,若是是握手請求,則發送http 101響應,不然解析獲得的數據並打印出來,而後判斷是否是斷開鏈接的請求(Opcode爲8),若是是則斷開鏈接,不然將接收到的數據組裝成幀再發送給客戶端。
客戶端代碼:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
window.onload=
function
(){
var
ws=
new
WebSocket(
"ws://127.0.0.1:8088"
);
var
oText=document.getElementById(
'message'
);
var
oSend=document.getElementById(
'send'
);
var
oClose=document.getElementById(
'close'
);
var
oUl=document.getElementsByTagName(
'ul'
)[0];
ws.onopen=
function
(){
oSend.onclick=
function
(){
if
(!/^\s*$/.test(oText.value)){
ws.send(oText.value);
}
};
};
ws.onmessage=
function
(msg){
var
str=
"<li>"
+msg.data+
"</li>"
;
oUl.innerHTML+=str;
};
ws.onclose=
function
(e){
console.log(
"已斷開與服務器的鏈接"
);
ws.close();
}
}
|
客戶端建立一個websocket對象,在onopen時間觸發以後(握手成功後),給頁面上的button指定一個事件,用來發送頁面input當中的信息,服務端接收到信息打印出來,並組裝成幀返回給日客戶端,客戶端再append到頁面上。
客戶結果以下:
服務端輸出結果:
從上面能夠看出,WebSocket在支持它的瀏覽器上確實提供了一種全雙工跨域的通訊方案,因此在各以上各類方案中,咱們的首選無疑是WebSocket。
上面論述了這麼多對於IM應用開發所涉及到的通訊方式,在實際開發中,咱們一般使用的是一些別人寫好的實時通信的庫,好比socket.io、sockjs,他們的原理就是將上面(還有一些其餘的如基於Flash的push)的一些技術進行了在客戶端和服務端的封裝,而後給開發者一個統一調用的接口。這個接口在支持websocket的環境下使用websocket,在不支持它的時候啓用上面所講的一些hack技術。
從實際來說,單獨使用本文上述所講的任何一種技術(WebSocket除外)達不到咱們在文章開頭提出的低延時,雙全工、跨域的所有要求,只有把他們組合起來纔可以很好地工做,因此一般狀況下,這些庫都是在不一樣的瀏覽器上採用各類不一樣的組合來實現實時通信的。
下面是sockjs在不一樣瀏覽器下面採起的不一樣組合方式:
從圖上能夠看出,對於現代瀏覽器(IE10+,chrome14+,Firefox10+,Safari5+以及Opera12+)都是可以很好的支持WebSocket的,其他低版本瀏覽器一般使用基於XHR(XDR)的polling(streaming)或者是基於iframe的的polling(streaming),對於IE6\7來說,它不只不支持XDR跨域,也不支持XHR跨域,因此只可以採起jsonp-polling的方式。
(本文同步發佈於:http://www.52im.net/thread-338-1-1.html)