node 基础学习(四) -- 中间件

博客分类: 原创

node 基础学习(四) -- 中间件

完全是扯淡,大部分的例子都太老了。

当学习了一点点后端知识之后,才发现自己是多么的无能,httptcp/ip自己对其底层完全不清楚,如果仅仅是知道大概,后续的学习也会造成影响。目前node是有提供对应的API,所以自己还是继续学习node,后面有时间一定要好好学习这里的知识。

中间件

简单的理解就是需要公共处理的方法。比如说,对于http请求而言。当后端接受到请求之后。会接到一个request对象,这个对象里的url参数可能是一段string。但是对于业务而言,业务只想配置一个路由规则。符合这个路由规则就进入某一个特定的方法。而中间件扮演的角色就是,业务告诉中间件一堆规则和规则对应的方法,业务就不再关心了,中间件接受到请求,处理url,然后匹配规则,一旦命中就执行规则对应的方法。

尝试来写一个例子:达到的要求是请求超时,记录日志,这里涉及到一个connect模块。这个项目分为三个文件。

package.json:
{
    "name": "request-time",
    "version": "0.0.1",
    "dependencies": {
        "connect": "^3.4.0"
    }
}

request-time.js:
module.exports = function(opts){
    var time = opts.time || 1000;
    return function (req, res, next){
        var timer = setTimeout(function(){
            console.log('\033[91m 请求超时 \033[39m \n', req.method, req.url);
        }, time);

        var end = res.end;
        res.end = function(chunk, enconding){
            res.end = end;
            res.end(chunk, enconding);
            clearTimeout(timer);
        }
        next(); // 将会继续执行下一个中间件
    }
}

demo.js:
var connect = require('connect'),
    time = require('./request-time.js');
// 获取 connect 对象
var server = connect();
// 将时间控制器假如中间件队列中
server.use(time({time: 1000}));
// 将访问 /a 的结果加入中间件队列中,url再增加一点判断就失效了,应该有更严格的匹配
server.use(function(req, res, next){
    if(req.url === '/a'){
        res.writeHead(200);
        res.end('Fast!');
    }else {
        next();
    }
})
// 将访问 /b 的结果加入中间件队列中
server.use(function(req, res, next){
    if(req.url === '/b'){
        setTimeout(function(){
            res.writeHead(200);
            res.end('Slow!');
        },1000)
    }else {
        next();
    }
})

server.listen(2000)

node demo.js,访问http://localhost:2000/a或者http://localhost:2000/b会得到相对应的回溯。这里就有两个中间件。connectrequest-time,通过包引入,和自己编写两种形式。而且connect是流式处理,根据注册方法的顺序会产生一个队列,上一个方法执行最后调用next,下一个队列中的方法立即被调用。

connect配套的中间件

serve-staticvar serveStatic = require('serve-static');server.use('/karyn', serveStatic('./images'));,封装在包serve-static中,使用之后所有访问/karyn的请求都会打到该文件下的images目录下,如果访问的图片文件存在,即可返回该图片。

queryvar serveQuery = require('connect-query'); server.use(serveQuery());,封装在包connect-query中,使用之后可以直接通过req.query拿到url上的query数据。

loggervar connectLogger = require('connect-logger'); server.use(connectLogger());,封装在包connect-logger中,使用之后可以通过connectLogger来打印请求信息,可是不是很好使,应该是自己使用不当。

websocket

websocket客户端基于事件的编程模型与node中自定义事件差不多。 websocket实现了客户端与服务器端之间的长连接,而node事件驱动的方式十分擅长与大量客户端保持高并发。

有点:

  • 客户端与服务器端只建立一个 TCP 连接,可以使用更少的连接。
  • Websocket 服务器端可以推送数据到客户端,这远比 HTTP 请求响应模式更灵活、更高效。
  • 有更轻量级的协议头,减少数据传送量。

以前页面需要频繁通信都是通过轮询的方案,现在可以使用websocket的方式,前后端一但建立通信之后,这个通信是双向的,双方都可以通过send方法向对方push数据,再结合事件的机制的话,就能做非常多的事儿了。

// node代码,引入包 websocket.io和express
var ws = require('websocket.io'),
    express = require('express'),
    app = express();
// 对访问当前目录直接打到静态资源文件html中
app.use(express.static('html'));
// 监听2000端口
http = app.listen(2000);
// websocket 也监听http的端口
var server = ws.attach(http);
// 绑定事件,建立链接后的处理
server.on('connection', function (socket) {
    socket.on('message', function (msg) {
        console.log(msg)
    });
    setTimeout(function(){
        socket.send('pang')
    },3000)
});

// js 代码
var ws = new WebSocket('ws://127.0.0.1:2000');
ws.onopen = function(){
    ping();
}
ws.onmessage = function(ev){
    console.log('got: ' + ev.data);
}
function ping(){
    ws.send('ping')
}

上面就实现了一个简单websocket通信。websocket这么方便,这么好,为啥没有得到大面积使用呢?肯定有问题。下面是使用websocket遇到的问题。

  • 首先客户端的支持性需要一定的程度
  • 服务器并不知道消息推送是否成功,(可以通过消息确认及超时来判断)
  • 客户端直接关掉浏览器,服务端是不知道客户端已经关闭了,还会继续推送(可通过唤醒来判断)
  • 比如手机的客户端,待机就会断掉,对手机而言,非常耗电
  • 对服务器而言,websocket 比 http 消耗大。服务器的压力增大
  • 没有降级处理方案
  • 并且在我的使用中,对应多个客户端时,有时候某个客户端关闭,服务端会报错,目前没有查到原因
  • 广播方式需要自己实现

socket.io

既然websocket非常好,只要有好的,我们就应该想办法克服那些不好的。Guillermo Rauch就对此做了一系列的封装,前后端都出了相应的方案,就是socket.io,实现和上面相同的功能。

// 引入 socket.io 包
var sio = require('socket.io'),
    express = require('express'),
    app = express();
// 对访问当前目录直接打到静态资源文件html中
app.use(express.static('html'));
// 监听2000端口
http = app.listen(2000);
// websocket 也监听http的端口
var server = sio.listen(http);
// 绑定事件,建立链接后的处理
server.on('connection', function(socket) {
    socket.on('socketName1', function(msg) {
        console.log(msg);
    });
    socket.on('close', function(msg) {
        console.log(msg);
    });
    setInterval(function() {
        socket.emit('socketName2', 'pang');
    }, 3000);
    setInterval(function() {
        socket.broadcast.emit('socketName2', 'pang2');
    }, 5000);
});
// 前端代码:
// 引入js文件:socket.io.js
var ws = io.connect('ws://127.0.0.1:2000');
ws.emit('socketName1', 'ping');
ws.on('socketName2', function(msg){
    console.log('got: ' + msg);
})

简单的通信就建立起来了。socket.io会采用多种方式与后端进行链接,最终采用的方式一定是最优的。如果不支持最优解有一定的降级处理,socket.io会尝试采用以下方式去建立连接:

  • websocket
  • 长轮询
  • falsh socket
  • http 长连接

socket.io支持命名空间,一个页面可以支持多个socket连接,这样带来的好处就是多个功能可以区分。还支持全局广播,这个全局广播,所有建立了socket的连接都会接受到。如果服务器断掉了,所有连接中断,但是当服务器重启的时候,所有连接都会尝试重连。连上之后还是会继续之前的通信,但是如果此时客户端没有更新,还是会去找之前的连接。如果实际项目中,一定要考虑兼容。

但是socket.io还是无法确认每次消息的推送都成功了。所以需要自己做一个确认的机制。实现思路:发送消息后,需要接受放给一个回执,如果超时没有得到回执再次发送,接受端需要做去重的操作。确定该消息只接受一次,其他的都丢掉