背景
- 最近学了websocket,感觉很有趣,特别是不用websocket也能模拟出即时通讯效果的骚操作非常牛b。
Comet
- 这个Comet翻译成中文叫
服务器推
技术。 - 传统模式是客户端发请求,服务端返回就结束了,但这明显不能满足即时报价,即时通讯之类需求。
- 而comet技术解决这个痛点一般采用2种方式:
1、在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用。
这个本文不做研究,简单说就是浏览器装flash,flash里面有个XMLSocket接口,基于这个来进行即时通讯。或者通过 java.net.Socket 或 java.net.DatagramSocket 或 java.net.MulticastSocket 建立与服务器端的套接口连接,或者还可以去翻翻微软的sliverlight,应该也有这样的插件。
2、基于http的服务器推技术。
这个主要分为三种:轮询,长轮询,iframe流。
一、轮询
- 轮询原理很简单,正常人都能想到,就是开个定时器,然后定时问一下服务器有没有新数据。
- 代码:
客户端
setInterval(()=>{
let xhr= new XMLHttpRequest()
xhr.open('GET','http://localhost:8080/clock', true)
xhr.onreadystatechange=function(){
if(xhr.readyState===4&&xhr.status===200){
document.querySelector('.clock').innerHTML=xhr.responseText
}
}
xhr.send()
},1000)
服务端
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',(req,res)=>{
res.end(new Date().toLocaleTimeString());
})
app.listen(8080)
- 这个缺点就比较明显,很浪费性能和带宽。
二、长轮询
- 由于轮询有很大缺点,所以就有了长轮询。
- 长轮询的原理其实就是利用了http在请求发出去后会有个等待响应时间。比如没有新的报价之类的数据更新,那么请求就一直放我这不进行返回,或者延迟一段时间返回。但是一旦数据有变动,服务端立即返回相应,客户端收到响应后渲染页面。
- 代码:
服务端
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',(req,res)=>{
let timer = setInterval(() => {
let date = new Date()
let seconds = date.getSeconds()
if(seconds%5===0){//模拟数据变化更新满足条件
res.end(date.toLocaleString())
clearInterval(timer)//同时把定时器清除
}
}, 1000);
})
app.listen(8080)
客户端
function longConnect(){
let xhr= new XMLHttpRequest()
xhr.open('GET','http://localhost:8080/clock', true)
xhr.onreadystatechange=function(){
if(xhr.readyState===4&&xhr.status===200){
document.querySelector('.clock').innerHTML=xhr.responseText
longConnect()
}
}
xhr.send()
}
longConnect()
- 可以发现客户端请求就是个递归,服务端开定时器只是简单的策略,比如有数据更新了,可以通过回调函数把目前所有在服务端长轮询的请求提前返回,并不一定要开定时器。但请求仍非常多的堆积在服务端,所以这个缺点是可能对服务端压力比较大,另外我试了下ie无效,客户端会卡死,不知道是我ie问题还是本来就不行。
三、iframe流
- 这个技术可以说非常的骚,以前还没有ajax的时候,iframe就用来代替ajax的活,现在还能做实时更新。
- 原理是这样:利用iframe的src加载的特性来获取数据,这个iframe的src本质和页面的script的src差不多,会阻塞渲染,如果不想阻塞可以异步加载iframe,所以这个操作会让浏览器上方有个圈一直转。但是谷歌工程师比较牛逼,开发的ActiveX的
htmlfile
插件可以不让圈转了,谷歌用这技术用到了gmail与gtalk里。插件就不说了,一般通过object标签来插入。下面看代码:
服务端
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',(req,res)=>{
setInterval(() => {
res.write(`<script type="text/javascript">
parent.document.querySelector('#clock').innerHTML = "${new Date().toLocaleTimeString()}"
</script>
`)
}, 1000);
})
app.listen(8080)
客户端
<div id="clock" style="height: 100px;"></div>
<iframe src="http://localhost:8080/clock" style="display: none;"/>
<div>5</div>
- 可以试一下客户端这个5是出不来的,因为被iframe给阻塞了。
- 所以这个可以改造成异步加载个iframe,然后服务端再把数据写给客户端,有人会说这个不是有点像jsonp吗?这玩意确实跟jsonp很像,但是我拿script标签替换iframe标签发现无效,客户端收不到服务端写入的数据,虽然network里连接没有结束。所以必须只能拿iframe标签才行。
- 这个方法有个很大的优点,由于iframe标签早就有了所以,就是ie可用。。
- 另外还需要注意http1.1中不能使用超过2个以上长连接,否则会阻塞别的http请求,如果一个页面有多个地方需要这么搞,建议统一用一个长连接返回数据。
SSE
- SSE是
server-sent-event
的缩写,H5提供了个接口叫EventSource
,这个接口ie当然没有实现,我看了下mdn,edge也没实现。其他浏览器都支持,chorme浏览器在chorme6开始支持。这个接口可以允许客户端去监听服务端推送的事件。 - 代码:
服务端
let express = require('express')
let app = express()
app.use(express.static(__dirname))
app.get('/clock',(req,res)=>{
res.setHeader('Content-Type','text/event-stream')
setInterval(() => {
res.write(`event:xxxx\ndata:${new Date().toLocaleTimeString()}\n\n`)
}, 1000);
})
app.listen(8080)
客户端
<body>
<div>1</div>
<div id="clock" ></div>
</body>
<script>
var eventSource = new EventSource('/clock');
eventSource.addEventListener('open',function(){
console.log('connect');
})
eventSource.addEventListener('xxxx', function(e){
console.log(e);
document.getElementById('clock').innerHTML=e.data
})
eventSource.addEventListener('error' ,function(err){
console.log(err);
})
</script>
- 还有2个地方需要注意下,一个是服务端返回格式必须要
text/event-stream
不然客户端报错。另外就是写入数据时注意不能有空格,数据发完需要两下\n
,否则就不认。 - 这里事件名就是xxxx,对应服务端
event:xxxx
字符串。 - 此时打开控制台可以发现原来的preview按钮换成了EventStream按钮,可以看见服务端推送消息。
- 这个东西优点就是方便快捷好用,不需要额外装东西,还可以配置自动重连什么的。
- 由于写字符串比较麻烦,nodejs里也有一些人做了些sse包,我搜了一下大同小异,就是把字符串变成可以做个对象,然后只要传对象交给它处理就行了。例子:
const SseStream = require('ssestream')
function (req, res) {
const sse = new SseStream(req)
sse.pipe(res)
const message = {
data: 'hello\nworld',
retry:2000
}
sse.write(message)
}