Comet(服务器推送)的两种方式
短轮询
页面定时向服务器发送请求, 步骤为:建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
//前端js
var xhr = new XMLHttpRequest();
setInterval(()=>{
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
let result = xhr.responseText
console.log(result);
}
}
xhr.open('get', '/front/test');
xhr.send(null);
},3000)
}
//后端 node
const Koa = require('koa');
const app = new Koa();
const router = require('koa-router')()
const koaBody = require('koa-body');
router.get('/front/test', koaBody(), ctx=>{
// ctx.set('Connection', 'close'); 不设置response.header,默认长连接,否则为短链接
ctx.body = { title: 'chuchur'}
});
app.use(router.routes());
app.listen(7005, () => {
console.log('server run in http://localhost:7005');
});
长轮询
长轮询的方式是,页面向服务器发起一个请求,服务器一直保持tcp连接打开,知道有数据可发送。发送完数据后,页面关闭该连接,随即又发起一个新的服务器请求,在这一过程中循环,步骤为:建立连接——数据传输...(保存连接)...数据传输——关闭连接
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
let result = xhr.responseText
console.log(result);
xhr.open('get', '/front/test'); //在获得数据后重新向服务器发起请求
xhr.send(null);
}
}
xhr.open('get', '/front/test');
xhr.send(null);
短轮询和长轮询的区别是:短轮询中服务器对请求立即响应,而长轮询中服务器等待新的数据到来才响应,因此实现了服务器向页面推送实时,并减少了页面的请求次数。
http流
//前端
var xhr = new XMLHttpRequest();
var received = 0; //最新消息在响应消息的位置
xhr.onreadystatechange = function () {
if (xhr.readyState == 3) {
let result = xhr.responseText.substring(received);
console.log(result);
received += result.length;
} else if (xhr.readyState == 4) {
console.log("完成消息推送");
}
}
xhr.open('get', '/front/test');
xhr.send(null);
//node
router.get('/front/test', koaBody(), ctx=>{
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
ctx.body = { title: 'chuchur'}
});
http流不同于上述两种轮询,因为它在页面整个生命周期内只使用一个HTTP连接,具体使用方法即页面向浏览器发送一个请求,而服务器保持tcp连接打开,然后不断向浏览器发送数据。
SSE eventSource
eventSource是用来解决web上服务器端向客户端推送消息的问题的。不同于ajax轮询的复杂和websocket的资源占用过大,eventSource(sse)是一个轻量级的,易使用的消息推送API ,大多数浏览器实现了SSE(Server-Sent Events,服务器发送事件) API,SSE支持短轮询、长轮询和HTTP流
前端实现
//生成EventSource对象,url必须同源
var evtSource = new EventSource("/front/test");
//收到服务器发生的事件时触发,如果连接断开,还会自动重新连接
evtSource.onmessage = function (e) {
console.log(e.data);
}
//成功与服务器发生连接时触发
evtSource.onopen = function () {
console.log("Server open")
}
//出现错误时触发
evtSource.onerror = function (e) {
console.log("Error")
}
//自定义事件
evtSource.addEventListener("test", function (e) {
console.log(e.data);
}, false)
服务端实现
//node
router.get('/front/test', koaBody(), ctx=>{
ctx.set({
'Content-Type': 'text/event-stream', //事件流的对应MIME格式为text/event-stream
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
ctx.body =
'id:11
'+
'retry: 100
'+
'event:test
'+
'data: hello word
'
});
服务端返回数据需要特殊的格式,它分为四种消息类型:event, data, id, retry
- event指定自定义消息的名称
- data指定具体的消息体,可以是对象或者字符串
- id为当前消息的标识符,可以不设置
- retry设置当前http连接失败后,重新连接的间隔。EventSource规范规定,客户端在http连接失败后默认进行重新连接,重连间隔为3s,通过设置retry字段可指定重连间隔;
每个字段都有名称,紧接着有个”:“。当出现一个没有名称的字段而只有”:“时,这就会被服务端理解为”注释“,并不会被发送至浏览器端,如: commision
WebSocket 全双工通讯
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
前端实现
let socket = new WebSocket('ws://localhost:7005/ws/test')
// 建立连接
socket.onopen = () => {
console.log('connection')
//发送消息,告诉服务器,我上线了.
socket.send(JSON.stringify({ name: 'chuchur' + Date.now(), msg: '我来了' }))
}
//接收消息
socket.onmessage = (evt) => {
let data = JSON.parse(evt.data);
console.log(data)
}
//socket 断开
socket.onclose = () => {
console.log('close')
}
//socket 发生错误
socket.onerror = () => {
console.log('error')
}
后端的实现
//引入相关ws库
....
const WebSocketServer = require('ws').Server;
//此处定义变量
const server = app.listen(7005, () => {
console.log('server run in http://localhost:7005');
});
//拿到ws对象
const wss = new WebSocketServer({ server });
//链接成功
wss.on('connection', ws => {
console.log('有人来了') //当客户端连接之后
//接收消息
ws.on('message', data => {
data = JSON.parse(data)
console.log(data)
if (data.name) {
//广播,告诉大家,有人来了
wss.broadcast(JSON.stringify({ msg: data.name + '来了,大家欢迎' }))
//记录ID,可以实现客户端一对一,或一对多通讯,ID为唯一值
ws.id = data.name
}
//实现一对一通话
if (data.toID) {
wss.clients.forEach(client => {
if (data.toID == client.id) {
client.send(JSON.stringify({ msg: ws.name + '对你说:' + data.msg }))
return;
}
})
}
})
//发送消息
ws.send(JSON.stringify({ msg: '你来了, 奖励一颗糖' }));
})
//广播
wss.broadcast = data => {
wss.clients.forEach(client => {
client.send(data);
});
};
setInterval(() => {
console.log('当前在线人数:' + wss.clients.size)
}, 1000);
ws转发配置
//webpack 开发配置
devServer: {
...
proxy: {
'/ws': {
target: 'http://120.0.0.1:7005', //转发到node
changeOrigin: true,
}
}
}
//nginx 部署
server {
listen 80;
server_name localhost;
# 处理WebSocket连接:
location ^~ /ws/ {
proxy_pass http://127.0.0.1:7005;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 其他所有请求:
location / {
proxy_pass http://127.0.0.1:7005;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
关于更多的ws相关配置,可以参考 WS
开发过程中需要自行做转发
总结
- ajax短轮询:省事,最简单,不论服务端是否返回数据,埋头苦干,任劳任怨
- 长轮询:也是埋头,只不过是,拿到数据才做出反应. 服务器有个阻塞的过程.
- SSE:可以接收服务端推送.接收http流,双向可控
- Socket:全双工通讯,功能强大, 耗资源
各有优缺点, 主要是看什么场景用什么.