websocket推送
使用Nodejs搭建一个Websocket的Server,然后浏览器端,使用ws协议,根据推送过来的消息,来触发页面更新。
想法源自于自己使用php搭建的一个简单的签到后端,某按键软件执行过程中,将执行过程中的日志发送到php的签到.php,然后签到php将数据入库,并根据发送过来的内容,如”任务结束”,决定是否要发送钉钉推送消息。而同时,为了展示消息,采用easyui的表格展示。由于每次更新数据,都需要自己手动来刷新视图。按之前做过的业务,便采用了js的定时器,来定时刷新视图。我们知道,定时器的更新效率,比较低,而且还存在一定的延时。于是乎,某天我有时间了,便想采用websocket来优化更新的机制。便有了本文探索的内容。(在以前的业务中,也有采用过mqtt来更新)
废话不多说,先来一个demo。
示例一
server端的:
const WebSocket = require('ws');
const WebSocketServer = WebSocket.Server
const wss = new WebSocketServer({
port:4000
});
wss.on('connection',function(ws){
console.log(`[SERVER] connect`);
ws.on('message',function(message){
console.log(`[SERVER] message : ${message}`);
ws.send(`ECHO: ${message}`,function(err){
if(err){
console.log(err);
}
});
});
});
nodejs客户端:
const WebSocket = require('ws');
let ws = new WebSocket('ws://localhost:3000/');
ws.on('open',function(){
// console.log(`[CLIENT] open`);
// ws.send('Hello');
});
ws.on('message',function(message){
console.log(`[CLIENT] Received:${message}`);
});
// 下面能防止,无法连接服务端,报错。
ws.on('error',function(error){
console.log(`[ERROR] :${error}`);
});
// let index = 0;
// setInterval(function(){
// index++;
// ws.send(`[INTERVAL] hello world ${index}`);
// },1000);
上面的例子比较简单,都是在nodejs环境中运行。在运行代码之前,先使用npm init来初始化项目,并使用以下命令安装相应的扩展:
node install -S ws
示例二
这个示例,便是运用到签到里面的例子。首先,服务端如下:
考虑到php中,每次更新msg时,使用http请求通知websockerServer比较方便。于是,便采用koa同时搭建了一个web服务。便于php发送更新事件。只需要在文件中增加
file_get_contents('localhost:30900/msgupdatenowsendwebsock');一行代码搞定。
安装:
node install -S ws
node install -S koa
node install -S koa-router
nodejs服务端:
服务端我们保存到了clients数组中,实际上,用对象保存更合适。我们可以给ws变量增加一个属性,如cliendid,然后clients = { clientId:ws,……} 这样保存,可能更高效一些。
onclose事间,再测试的时候,基本上没有触发过,现在才想明白,如果客户端调用了ws.close(),是不是,服务端close事件就会触发了?
const Koa = require('koa');
const WebSocket = require('ws');
const WebSocketServer = WebSocket.Server;
const router = require('koa-router')();
const app = new Koa();
var clients = [];
function broadcast(){
// console.log('客户端数量:'+clients.length);
clients.forEach(function(ws,index){
ws.send('update',function(err){
if(err){
console.log(err);
clients.splice(index,1); //删除不存在的客户端
}
});
});
}
//中间键、路由等操作
router.get('/',async function(ctx,next){
ctx.response.body = '<h1>hello world</h1>';
});
router.get('/scc',async function(ctx,next){
ctx.response.body = '<h1>你好,世界</h1>';
});
router.get('/msgupdatenowsendwebsock',async function(ctx,next){
ctx.response.body = 'ok';
//执行 ws 通知
broadcast();
});
app.use(router.routes());
let server = app.listen(3000);
let wss = new WebSocketServer({
server: server,
});
wss.on('connection',function(ws){
clients.push(ws);
// console.log(ws);
console.log('[SERVER] 客户端连接');
ws.on('message',function(message){
console.log(message);
ws.send(`ECHO: ${message}`,function(err){
if(err){
console.log(err);
}
});
});
});
wss.on('close',function(ws){
console.log('[SERVER] 客户端断开');
});
浏览器客户端代码:
主要是在onmessage事件中,触发更新即可。当然,也可以更具消息的不同,对应不同的动作。ws主要起到触发左右,当然数据是可以通过ws传送的,但是没有必要,依然采用之前的ajax,即可。所以的逻辑,不用便。
// websocket 初始化
function ws_init(){
var ws = new WebSocket('ws://bb.chaofml.cn:30900/');
ws.onopen =function(){
console.log(`[CLIENT] open`);
};
ws.onmessage = function(message){
console.log(message);
console.log(`[CLIENT] Received:${message.data}`);
update_data();
};
// 下面能防止,无法连接服务端,报错。
ws.onerror = function(error){
console.log(`[ERROR] :${error}`);
};
}
function update_data(){
//更新数据
$('a.l-btn.l-btn-small.l-btn-plain').eq(4).click();
}
总结:
上面整体过程都比较简单,但是koa由于长时间未用,有些生疏。另外,还卡在wss创建这一步。以为不用接受app的返回值,是一样的。错误的代码如下:
app.listen(3000);
let wss = new WebSocketServer({
server: app,
});
正确的示例如下;
let server = app.listen(3000);
let wss = new WebSocketServer({
server: server,
});
除之之外,php部分也非常简单:
存储文件,主要用来接收请求,写入数据库,并判断消息内容,决定是否要发送钉钉通知。然后请求一下koa服务,触发一下更新事间。
而展示页面,使用easyui的表格展示,通过ws事件来动态的更新页面。整体非常的简单。但是也用到了几种不同的技术。为啥不直接用js来完成整个项目呢?这是因为,刚开始没有想那么多,觉得php来示现比较简单。
全文完。
改进三
断开重连
浏览器端的代码,只是达到了websocket的及时推送,但是没有解决,断网的情况下,重连的机制。昨天,我在家用ipad测试效果,然后由于使用的共享WiFi的信号太弱,WiFi可能断开了。而websocket无法重连,结果,websocket的及时推送就消失了。网上搜了一下解决方式,比如下面这个文章,首先文章比较长,只是治标,效果不好。
方式1:浏览器端的改进代码如下:
// websocket 初始化
function ws_init(){
var ws = new WebSocket('ws://bb.chaofml.cn:30900/');
var count = 0;
ws.onopen =function(){
console.log(`[CLIENT] open`);
};
ws.onmessage = function(message){
console.log(message);
console.log(`[CLIENT] Received:${message.data}`);
update_data();
};
// 重连机制
ws.onerror = function(error){
if(++count === 1) setTimeout(ws_init,2000);
};
// 重连机制
ws.onclose = function(error){
if(++count === 1) setTimeout(ws_init,2000);
};
}
说明一下上面代码的作用:
假如websocket因网络触发了close事件,
1、如果不加延时setTimeout,那么,上面就像函数调用一样,瞬间,反复的执行,而且本身就如同一个循环般,去执行,直到连上,不再触发新的close或error事件,即setTimeout控制调用速度。
2、if(++count === 1)作用,保证,一个websocket,在连接失败后,可能会同时触发onerror、onclose事件,但是二者之一,触发了就可以了。避免同时触发。否则就像原子弹一样,1->2->4,翻倍的调用。另外,就算反复触发close事件,只有第一个,会起左右,后续的将不再起左右。
假设、推论:
如果不想使用count来控制,我猜测只响应onclose或onerror事件即可。即不要让其裂变发展。但是,可但是,如果触发了error,我们又新建了新的websocket代替它,旧的websocket是否还会继续连?
或者我们不用count来控制,我们直接删除该websocket,用新的websocket取而代之,这样,因为被删除了,则不会继续触发事件了呢?尝试以下两种方法,感觉都不行。ws.close可能还会接着触发close或error事件。而delete不确定是否删除了原作用域的东西。
下面的两种方法,感觉就像是,setTimeout创面的的websocket,在拥有网络的一瞬间,瞬间多个都连成功了。
ws.onerror = function(error){
// ws.close(); //不行
delete ws; //也不行
setTimeout(ws_init,2000);
};
那我们再反过来,试试,用新的取代旧的websocket的时候,我们直接取消原来的事件绑定呢?
方式2:发现下面取消绑定事件的方法比较好,而且也更容易理解,推荐使用:
ws.onerror = function(error){
ws.onopen = ws.onerror = ws.onmessage = ws.onclose = null; //该方法也是有效的。
setTimeout(ws_init,2000);
};
// 当然,我们也可以为了美化代码,将取消绑定,写成一个函数。还是同一个原理
优雅关闭
应该使用下面的方式,进行优雅关闭。(但实际上,我之前的页面没有这样做,也能工作的较好)
window.onbeforeunload = function() {
websocket.onclose = function () {}; // disable onclose handler first
websocket.close()
};
安全创建
摘抄一下,安全创建WebSocket的方法:
// 实例websocket
function createWebSocket(url) {
try {
if ('WebSocket' in window) {
ws = new WebSocket(url);
} else if ('MozWebSocket' in window) {
ws = new MozWebSocket(url);
} else {
_alert("当前浏览器不支持websocket协议,建议使用现代浏览器",3000)
}
initEventHandle();
} catch (e) {
reconnect(url);
}
}
如果是同个浏览器中多个页面共用一个连接来进行通信,那么就需要使用浏览器缓存/数据路(localStorage/indexedDB)去加这把锁。
总结
上面采用了两种方法,控制多次重链,个人感觉两者都行。反正都比直接搜到的答案好。所以,遇到问题,找到网上的解答,也应该再继续想想,也许自己理解透了,做得比网上的答案更好。
关于websocket的机制,我还是不知道,断网、弱网环境下,什么时候触发error、close的机制(或条件),是否触发了close机制,就该websocket就报废了,不会起左右了?
示例四
基于原生的http的server来搭建websocket服务。
主要的变化是:server直接用原生的server来代理。
const http = require('http');
const WebSocket = require('ws');
const WebSocketServer = WebSocket.Server;
var server = http.createServer(function(request,response){
console.log(request.url);
response.writeHead(200,{'Content-Type': 'text/plain; charset=utf-8'});
response.end('Hello world');
});
server = server.listen(3000);
let wss = new WebSocketServer({
server:server
});
wss.on('connection',function(ws){
console.log('客户端连接');
ws.on('message',function(message){
console.log(message);
ws.send(`ECHO: ${message}`,function(err){
if(err){
console.log(err);
}
});
});
});
wss.on('close',function(ws){
console.log('[SERVER] 客户端断开');
});
反向代理
首先要保证Nginx版本>=1.3,然后,通过proxy_set_header指令,设定:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
从廖雪峰js教程上摘抄的如下配置:自己测试过,确实能有效:
server {
listen 80;
server_name localhost;
# 处理静态资源文件:
location ^~ /static/ {
root /path/to/ws-with-koa;
}
# 处理WebSocket连接:
location ^~ /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# 其他所有请求:
location / {
proxy_pass http://127.0.0.1:3000;
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/ 开头的连接,都会进行转发到websocket,所以呢,我们的socket地址,应该符合匹配规则,一个示例如下:
var ws = new WebSocket('ws://mywebset.cn/ws/sjkjk');