NodeJS的底层通信

更新于 2016-03-16  约 25 分钟

net模块其实是对TCP协议的一层封装。 在前端,我们经常打交道的无非是HTTP/HTTPS 协议。他们只是高层的通信协议(单向),主要应对的是网络不稳定的情况,当客户端请求数据时,服务器便返回相应的信息即可,当完成数据交流之后,便会自动断开连接。当然,HTTP也只是对TCP的一层封装而已。在nodeJS 里面我们可以实现底层TCP,我们可以自定义任何我们想要的效果,比如,我们可以实现 可持续(keep-alive)的效果,形式实时通信。而里面用到的主要通讯技术就是"套字节 socket"的双向通信.

核心API

net主要有两大类,一个是Server,一个是Socket.

Server相关

Serve继承了EventEmitter, 也应该说是JS 最精华的所在。 当然Server也有他自己的一些方法。

  • 事件

    • close: 当服务器关闭的时候回触发。要注意是当且仅当所有的连接关闭的时候才会触发。
      server.on('close',cb)

    • connection: 这应该是server中最重要的一个事件。 表示当一个新连接建立时触发。 即,通常处理的具体业务逻辑,我们都是写在connection里面的。回调函数里的参数就是socket的实例.
      server.on("connection",function(socket){...})

当使用createServer直接创建时,就可以省略connection事件的监听了。 回调函数里自带一个socket对象

    • listening: 当使用server.listen()方法成功绑定端口的时候触发,不过没什么卵用.

    • error: 这个同理,表示出现什么错误的时候会触发。并且close事件,会立即触发。

    • 方法

      • address()[Object]: 返回服务器绑定的地址和端口号,以及一些相应信息.{ port: 12346, family: 'IPv4', address: '127.0.0.1'}
        具体demo:

        server.listen(() => {
        address = server.address();
        console.log(`opened server on ${address}`);
        });
      • listen() . 说起来,这个应该算是最奇葩的一个方法了。他接受的参数,真的可以玩死你。

        1. listen(port,hostname[,callback])
          用来设置监听的端口.server.listen(3000,'http://localhost'). 当然,如果你不设置hostname的话,nodeJS会默认以IP6的方式进行获取,如果IP6也没有的话,则会给你随机分配一个,即采用,0.0.0.0 方式来。其中要说的是最后一个callback,当你的server端口开启监听时,就是触发该函数。其实他就是listening事件的回调函数,有兴趣的同学可以翻上去看看。

        2. server.listen(options[,callback])
          options是一个Object类型,里面包含的参数有:

           &emsp;port <Number>:端口号
           &emsp;host <String>: 主机
           &emsp;backlog <Number>: log号
           &emsp;path <String> 监听路径
           &emsp;exclusive <Boolean> 是否共享handle函数

          callback: 和上面说的callback是一个东西

      不过一样就是用第一个就可以了。这些我也不知道什么时候会使用,写的时候突然就用上了。

      1. server.listen(path,backlog)
        这个主要针对的是本地(Unix)的socket server的监听。举一个demo吧:server.listen('/tmp/echo.sock',cb); 里面的路径就是Unix下的sock文件。

      2. server.listen(handle,backlog)
        本来可以在createServer里面直接指定handle不过,这里在listen里面指定也是可以的。

      function handleRequest(request, response){
          response.end('It Works!! Path Hit: ' +                request.url);
      }
      server.listen(handleRequest);
      
             不过,这个真的没什么卵用。。。
      
      • server.maxConnections 用来设置最大连接数。

    Socket相关

    创建完Server之后,就到了具体的实现逻辑。Socket来帮助我们实现信息的通信.

    • 事件

      • close 当发生传输错误的时候会触发close事件

      • connect: 当socket连接成功创建的时候,会触发该事件

      • data: 非常重要的一个事件,用来接收数据。

      • end: 当完成数据发送的时候,会触发end事件。 但是如果你设置了比如allowHalfOpen=true的时,这时候情况就有些复杂,需要你手动使用下文end()方法进行中断.

      • timeout: 如果 你长期没有进行数据的传输(inactivity), 这时候,你可以手动的关闭连接。而,这里的事件是通过socket.setTimteout()来进行设置的。

    • 方法

      • address(); 同样,返回绑定的地址,和server.address()是一样的效果

      • bufferSize: 检查现在已用的内存大小

      • bytesRead: 已经接受的字节数

      • bytesWritten 已经发送的总字节数

      • localAddress/localPort: 就是返回 你的地址(比如127.0.0.1)和端口号(比如:87)

      • setEncoding([encoding]): 设置接受数据的格式,有('ascii', 'utf8', 'base64').通常设置 utf8就可以了

      • end(data): 调用这个方法,并不是立即发送FIN包,该会在data传送完之后发送.

      • setTimeout(timeout[, callback]) 当有多长时间你不活动是(inactivity),就会触发callback,当然你可以手动关闭连接。

      • write(data, encoding): 用来写入信息,如果写入成功,会返回true, 否则返回false,这此时data会排在内存中,当完成是,Buffer重新为空,此时触发drain事件,表示可以继续写入。

      • connect(): 这同样和server.listen一样,也是一个要死人的方法. 他的格式和listern的格式很接近(废话,不都是TCP连接吗?).

      • destroy(): 确保没有额外的I/O操作,然后关闭连接即可。

        • socket.connect(port, host)
          同样,里面的参数,我就不介绍了,后面的connnectionListerner就是替代connect事件的回调。 不过一般使用createConnection代替,这个实际生产中并没有什么卵用。

            • socket.connect(options[, connectListener])
              options里面的参数,和上面还是有些区别的。这里说一下大致的参数吧。port(端口),host,localAddress,localPort,family,lookup. 就这几个,不会的请google.

            • socket.connect(path[, connectListener])
              这个和server.listen就是一个道理了。好累,,不解释了

    net相关

    上面说的就是net里面全部的内容,但是,我们应该怎么创建Socket或是Server呢?

    • 方法

      • net.createServer(options)
        options里面的参数,就是allowHalfOpenpauseOnConnect 默认都是false.这里我就不详述了。

      因为,这。。。确实没什么用(maybe 以后有用呢?那有需求再说呗). 好了,我们直接来说connectionListener. 其实,他就是connection事件的回调函数,这里,方便书写,就直接放在createServer里面来了。

    并且该listen还会自动创建一个socket对象,用来进行和数据的传输.
    具体来看一个demo

        var net = require('net');
        //自动创建socket
        var server = net.createServer(function(socket) {     //'connection' listener
            console.log('server connected');
            socket.on('end', function() {
                console.log('server disconnected');
            });
            socket.on('data', function(){
                socket.end('hello\r\n');
            });
        });
        //开启端口的监听
        server.listen(8124, function() { //'listening'     listener
            console.log('server bound');
        });

    其实探究源码,我们知道net其实就是两个部分,一个socket一个是Server,其实在使用createServer创建的源码其实应该是这样的.

    //from 阎王
    net.createServer = function(callback){
        // 每次客户端连接都会新建一个 socket
        var socket = new Socket();
        callback && callback(socket);
    };
      • net.createConnection(port, host)
        用来创建一个socket连接,该和socket.connect()方法一样变态,也有很多的变形参数传入,不过这里就只介绍这一种,有兴趣(找虐)的童鞋可以去参考具体文档。如果你的host没有设置,则默认为localhost. 当然,这不是最常用的,因为太长了--最常用的是connect

    • net.connect(port, host)
      使用方法同上,这里就不赘述了。

    ok~ 基本的就是这些了。(md~ 好累呀~) 经过我认认真真的板书,大概知道了net内部模块的相关的API了吧(快,夸一下宝宝). 开头说道,TCP 其实是靠底层的通信协议,而且他有支持双工的 socket的诶~
    这里俺们来实践一下,如果造一个实时通信的轮子。

    轮子哥--双工通信

    首先,肯定是需要先造一个服务器的。 很简单啦。直接上代码:

    const net = require('net');
    const server = net.createServer(function(socket) { //'connection' listener
        console.log('server connected');
        socket.on('end', function() {
            console.log('server disconnected');
        });
        socket.on('data', function(data){
            console.log("[Client]:"+data);  //输出返回的信息
            setTimeout(()=>{socket.write("Hi, I'm your Server");},1000);
        });
    });
    server.listen(3133, function() { //'listening' listener
        console.log('server bound');
    });

    上面主要用来接受客户端信息的返回,并且打印接受的信息。
    其实你也可以这么写》

    var net = require('net');
    var server = net.createServer();
    server.on('connnection',function(){
        //... 上面createServer的内部逻辑
    });
    //下面就是一样的了。

    对照API看看,大家应该就知道了。

    来,接着写一下客户端》

    const net = require('net');
    
    const client = new net.Socket();
    client.connect(3133, function() {
        console.log('Connect to Server');
        // 建立连接后立即向服务器发送数据,服务器将收到这些数据
        client.write('Hi Server');
    
    });
    
    // 为客户端添加“data”事件处理函数
    // data是服务器发回的数据
    client.on('data', function(data) {
    
        console.log('[Server] ' + data);
        setTimeout(()=>{client.write("Hi Server");},1000)
        //我们暂时不关闭:client.destroy();
    
    });
    
    // 为客户端添加“close”事件处理函数
    client.on('close', function() {
        console.log('Connection closed');
    });

    首先运行server.js,然后运行client.js。 正常情况下, 会每2s输出内容》
    比如:

    //Client控制台
    [Client]:Hi Server
    ...
    
    //Server 控制台
    [Server] Hi, I'm your Server
    ...
    

    当你手动断开时,就会触发close事件。 当然,你也可以使用client.destroy或者client.end() 进行关闭。 而在服务端可以使用server.close(cb)进行关闭。
    艹,大哥,这么简单,你还写出来,简直浪费呀~~~~
    恩,小兄弟,你有所不知,我想说的,下面才是干货。23333

    冷门双工通信

    亲,你有没有考虑这个问题嘞?
    当你网络情况比较慢的,时候,你的请求可能还在路上,而Server看你还不来,就把连接断了。
    还有
    当你看上次请求过时了,你又发一次请求,那该请求和上次请求谁会被处理呢?
    OK~ 现在是答题解惑时间
    熟悉TCP 协议的童鞋,应该知道TCP的3次握手,挥手以及相应的header. 母鸡? OK, 传送门.
    这里,我们针对上面的问题做一些解答
    在数据发送过程时,A向B发送一次请求. 如果B就收到包后会向A发送ACK 确认包。如果A在指定时间没有接收到的话则会重新发送包。 当然, 如果你的时间过期的过分的话,老子就不和你说话啦!!!(直接断开连接)
    其实,就涉及到两个时间的设定。

    • 重发超时时间

    • 连接超时时间

    这里主要利用到NodeJS的两个API 一个是setKeepAlive(Boolean,time), 一个是setTimeout(time[,cb]);
    setKeepAlive
    这个API主要是给我们一种途径,防止对方的时限过短而断开连接。使用setKeepAlive 会向对方发送 一个空的ACK包,来保持通信。

    const server = net.createServer(function(socket) { 
        socket.setKeepAlive(true,3000); //如果3s内没有包的交流的话,会发送空包,进行交互
    });

    这里,你的时间当然可以设置高一点,比如20s/30s。
    setTimeout
    当你的inactivity时间过长的话,则作为服务器,我可以断你的线,我可以发送一个包提醒你。 这里,也可以模仿setKeepAlive. 不过,这个API灵活性更高,当设置的时间到了,则会触发timeout事件。不过一般我们就写在回调函数里,这样方便一些。

    const server = net.createServer(function(socket) { 
        socket.setTimeout(3000,()=>{
            socket.end("you gonna be killed");
        });
    });

    其实,上面两个API,本人最喜欢用的还是setTimeout(大部分情况). 而且,如果系统不设置timeout的话,他是不会断线的。
    另外,还有一个比较冷门的API,我这里也顺带提一提吧。

    socket.setNoDelay([noDelay])
    该API主要涉及的是关于网络传输的一个算法--Nagle algorithm. 该算法主要针对的是网络传输时资源节省的算法。 有时候,当你在发送数据包的时候,有时候传输的数据很小,就几个b,老子TCP都比你大不止10倍(通常一个TCP大小是40 byt)。所以为了针对这样的数据包,使用Nagle 可以将小包 缓存起来,等到下一次 打包来的时候,一起发送过去。 but!!! 我们得想想这个算法出来的背景,那时候我们还在使用核桃机,用的是2G网。 能节约1b 是1b 啊,针对那时候 这个算法,是极其有价值的。 but now, 我觉得应该被淘汰了,因为现在4G 已经风靡, 我已经不想再放那张洗脑的2G,3G,4G图了。 大家可以自行脑补。 不过,系统是默认开启的,所以,这样造成的后果是,信息的延迟过大。当今,推荐是关闭该算法,在nodeJS中可以使用setNoDelay(true) 来实现。

    const server = net.createServer(function(socket) { 
        socket.setNoDelay(true);
    });

    ok~ 我们可以使用上述的API就可以构建一个强健的 双通道通信了。
    当然,有的童鞋,可能会吐槽了。md~
    别别别别别别别别别... 骂宝宝
    艹~ 你这是前端诶, front-end 端都不给,差评~~
    墨迹, 我正要介绍嘞, 目前前端界最火的一个插件--socket.io.js 这 应该算是很牛逼的一个库了。ok~ 我们现在来具体学习一下吧.

    socket.io

    以前,在前端要做到双向通信,只能使用长轮询(ajax,iframe)等 相关的trick. 不过H5 推出了一个新的通信方式websocket. 目前的兼容性是IE10+. 对于目前兼容IE8+来说,还是有一定的距离的。所以这里推荐使用一个库--socket.io,可以实现双向的通信. 他的降级方式这式样的.

    • websocket

    • Socket over Flash API

    • XHR Polling 长连接

    • XHR Multipart Streaming

    • Forever Iframe //持续刷新iframe

    • JSONP Polling //跨域的长连接

    正式由于socket.io实现了这一强大的兼容性,所以,在通信时,非常的火。而且,socket.io有这一套的强大的通信机制,从前端到后端,使用的API和事件都是完全一致。 这里我们先浅浅的舔一舔socket.io的脚毛。
    很简单,两个端的代码如下:

    //浏览器
    const io = require('socket.io'),
    socket = io('http://localhost:8080');
    socket.on('connection', function (data) {
        console.log(data);
        socket.emit('communicate', { my: 'data' });
      });
    
    
    //nodeJS端
    const app = require('http').createServer(handler);
    const io = require('socket.io')(app);
    
    app.listen(8080,'http://localhost');
    
    function handler (req, res) {
        res.writeHead(200, {'content-type': 'text/plain'});
        res.end(data);
    }
    //使用io监听connection事件, 用来处理当有连接连上时发生的事.
    io.on('connection', function (socket) {
        //这里的connection其实是自定义事件,相当于,另外定义一个端口,进行通信
      socket.emit('connection', { hello: 'baby' });
      socket.on('communicate', function (data) {
        console.log(data);
      });
    });

    差不多就是这样,细心的同学,可能会看见nodeJS端,为什么会写两个绑定的请求嘞? 一方面是由于降级的原因,另外一方面,由于HTTP进行的是短连接,不能很好的解决长连接的问题,而另外加的一个监听(connection),其实就是对net模块的封装,以及完美的API转化。 如果我们抛开socket.io, 通常的做法就是在页面中再嵌套一个iframe 然后,实现长连接的效果。
    ok,我们继续。 总结一下,其实socket.io最核心的API 其实就两个一个是on.一个是emit. on用来接收信息,emit用来发送信息。比如,我们现在想要进行多端口通信的话,应该怎么做呢?
    这里有两种方法,一个是使用socket.io提供的API--of,另外是使用自定义事件。 两者应用不同的场景,比如我们要进行多对多的通信,那么,就需要使用of来进行区分。

    of 通信

    其实,就和express框架是一样的。使用of,就可以很方便的定义一个路由的通信.

    //客户端
    var chat = io.connect('http://localhost/chat')
        , news = io.connect('http://localhost/news');
      
      chat.on('connection', function () {
        chat.emit('hi!'); //针对不同路由,这里emit直接跟data
      });
      
      news.on('news', function () {
        news.emit('woot'); 
      });
    
    
    //服务器
    var io = require('socket.io')(80);  //相当于http://localhost:80
    var chat = io
      .of('/chat')  //这里监听的就是http://localhost:80/chat 路由
      .on('connection', function (socket) {
        socket.emit('a message', {  //只能一对一的传信息
            that: 'only'
          , '/chat': 'will get'
        });
        chat.emit('a message', {  //给chat下面所有监听的a message的事件发送信息
            everyone: 'in',
            '/chat': 'will get'
        });
      });
    
    var news = io
      .of('/news')
      .on('connection', function (socket) {
        socket.emit('item', { news: 'item' }); //发送信息
      });

    推荐,不要使用这个多路由通信,因为实在没有自定义事件用起来方便,容易理解。

    自定义事件通信

    很简单,就相当于自定义事件一样,如果大家使用过订阅发布这模式,这个理念就很简单了。

    //客户端
    socket.on('connection',()=>{
         socket.on('firstTunnel',(data)=>{
            console.log(`the first data is ${data}`);
         socket.emit('firstTunnel',{info:"[Client] Hi Server, this is first Client"});
         });
         socket.on('secondTunnel',(data)=>{
            console.log(`the second data is ${data}`);
             socket.emit('secondTunnel',{info:'[Client] Hi Server, this is second Client'});
         })
    });
    
    //服务端
    io.on('connection', (socket) => {
        socket.on('connection', () => {
            socket.on('firstTunnel', (data) => {
                console.log(`the first data is ${data}`);
            });
            socket.on('secondTunnel', (data) => {
                    console.log(`the second data is ${data}`);
                })
                //...
            socket.emit('firstTunnel', { info: "[Client] Hi Server, this is first Client" });
            socket.emit('secondTunnel', { info: '[Client] Hi Server, this is second Client' });
        });
    })
    

    OK, 现在你可以使用firstTunnel和secondTunnel进行信息的传递了。 我之所以喜欢socket.io 是因为 他的理念就是.

    code once run anywhere

    其实,这个nodeJS的理念是一木一样的,前端的代码,在后端可以复用。 举个例子吧, 就比如我们写验证的时候,有个原则就是永远不要相信用户的数据。 所以,导致的结果解释,前端检测之后,后端还需要自己自定义一套一模一样的检测机制---真· 鸡肋. 如果我们前端使用nodeJS 自己写后端的话,结果又不一样了,只需要做两件事,copy+paste. Over~ 然后就是调整一下格式就行了。

    转载请注明作者和原文链接
    链接地址:https://segmentfault.com/a/1190000004598004

    阅读 8.6k更新于 2016-03-16

    推荐阅读
    前端的bigboom
    用户专栏

    个人博客: [链接] 公众号:前端小吉米

    1053 人关注
    111 篇文章
    专栏主页
    目录