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的及时推送就消失了。网上搜了一下解决方式,比如下面这个文章,首先文章比较长,只是治标,效果不好。

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');