有关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.cnblogs.com/helloim/p/5663660.html