Comet,SSE,WebSocket前后端的实现
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\n" + "retry: 100\n" + "event:test\n" + "data: hello word\n\n";
});
服务端返回数据需要特殊的格式,它分为四种消息类型: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
:全双工通讯,功能强大, 耗资源
各有优缺点, 主要是看什么场景用什么.