分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow
也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!
有关IM(InstantMessaging)聊天应用(如:微信,QQ)、消息推送技术(如:现今移动端APP标配的消息推送模块)等即时通讯应用场景下,大多数都是桌面应用程序或者native应用较为流行,而网上关于原生IM(相关文章请参见:《IM架构篇》、《IM综合资料》、《IM/推送的通信格式、协议篇》、《IM心跳保活篇》、《IM安全篇》、《实时音视频开发》)、消息推送应用(参见:《推送技术好文》)的通信原理介绍也较多,此处不再赘述。
而web端的IM应用,由于浏览器的兼容性以及其固有的“客户端请求服务器处理并响应”的通信模型,造成了要在浏览器中实现一个兼容性较好的IM应用,其通信过程必然是诸多技术的组合,本文的目的就是要详细探讨这些技术并分析其原理和过程。
Web端即时通讯技术盘点请参见:
《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=15
浏览器从诞生开始一直走的是客户端请求服务器,服务器返回结果的模式,即使发展至今仍然没有任何改变。所以可以肯定的是,要想实现两个客户端的通信,必然要通过服务器进行信息的转发。例如A要和B通信,则应该是A先把信息发送给IM应用服务器,服务器根据A信息中携带的接收者将它再转发给B,同样B到A也是这种模式,如下所示:
我们认识到基于web实现IM软件依然要走浏览器请求服务器的模式,这这种方式下,针对IM软件的开发需要解决如下三个问题:
即时通讯网注:关于浏览器跨域访问导致的安全问题,有一个被称为CSRF网络攻击方式,请看下面的摘录:
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为“沉睡的巨人”。
基于以上分析,下面针对这三个问题给出解决方案。
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;itry{ 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);
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('服务器被关闭');});
结果如下:
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;itry{ 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');
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()); }}
可以看到返回的时间是没有规律的,并且单位时间内返回的响应数相比polling方式较少。
上面的long-polling技术为了保持客户端与服务端的长连接采取的是服务端阻塞(保持响应不返回),客户端轮询的方式,在Comet技术中(详细技术文章请参见《Comet技术详解:基于HTTP长连接的Web端实时通信技术》),还存在一种基于http-stream流的通信方式。其原理是让客户端在一次请求中保持和服务端连接不断开,然后服务端源源不断传送数据给客户端,就好比数据流一样,并不是一次性将数据全部发给客户端。它与polling方式的区别在于整个通信过程客户端只发送一次请求,然后服务端保持与客户端的长连接,并利用这个连接在回送数据给客户端。
这种方案有分为几种不同的数据流传输方式。
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。
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());}
可以看到每次传过来的数据流都进行了处理,同时打印出了整个最终接收到的完整数据。这种方式间接实现了客户端请求,服务端及时推送数据给客户端。
可以看到实现在低版本IE中客户端到服务器的请求-推送的即时通信。
function connect_htmlfile(url, callback) { var transferDoc = new ActiveXObject("htmlfile"); transferDoc.open(); transferDoc.write( ""); 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('服务器被关闭');});
其中比较重要的是FIN字段,它占用1位,表示这是一个数据帧的结束标志,同时也下一个数据帧的开始标志。opcode字段,它占用4位,当为1时,表示传递的是text帧,2表示二进制数据帧,8表示需要结束此次通信(就是客户端或者服务端哪个发送给对方这个字段,就表示对方要关闭连接了)。9表示发送的是一个ping数据。mask占用1位,为1表示masking-key字段可用,masking-key字段是用来对客户端发送来的数据做unmask操作的。它占用0到4个字节。Payload字段表示实际发送的数据,可以是字符数据也可以是二进制数据。
所以不管是客户端和服务端向对方发送消息,都必须将数据组装成上面的帧格式来发送。
首先来看服务端代码:
//握手成功之后就可以发送数据了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=[];j4]); }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]);}
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="" +msg.data+""; oUl.innerHTML+=str; }; ws.onclose=function(e){ console.log("已断开与服务器的连接"); ws.close(); } }
服务端输出结果:
从上面可以看出,WebSocket在支持它的浏览器上确实提供了一种全双工跨域的通信方案,所以在各以上各种方案中,我们的首选无疑是WebSocket。
从图上可以看出,对于现代浏览器(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)
作者:Jack Jiang (点击作者姓名进入Github)