背景
- 最近學了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的
插件可以不讓圈轉了,谷歌用這技術用到了gmail與gtalk裡。插件就不說了,一般通過object标簽來插入。下面看代碼:htmlfile
服務端
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是
的縮寫,H5提供了個接口叫server-sent-event
,這個接口ie當然沒有實作,我看了下mdn,edge也沒實作。其他浏覽器都支援,chorme浏覽器在chorme6開始支援。這個接口可以允許用戶端去監聽服務端推送的事件。EventSource
- 代碼:
服務端
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)
}