node-network

Node.js – Network

Node是一个面向网络而生的平台,它具有事件驱动、无阻塞、单线程等特性,具有良好的可伸缩性,使得它十分轻量,适合在分布式网络中扮演各种各样的角色。

Node提供了net、dgram、http、https这4个模块,分别用于处理tcp、udp、http、https,适用于服务器端和客户端。

  • TCP
    TCP全名为传输控制协议,是面向连接的协议。其显著的特征就是在传输之间需要3次握手形成会话。

    tcp-server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    let net = require('net');
    let server = net.createServer(function (socket) {
    // new connect
    // socket.setNoDelay(true); // 关闭Nagle算法
    socket.on('data', function (buffer) {
    console.log('socket event data', new Date().getTime(), buffer.toString());
    socket.write('Hello World');
    });
    // close
    socket.on('end', function () {
    console.log('socket event end', new Date().getTime());
    });
    socket.on('close', function (had_error) {
    console.log('socket event close', had_error);
    });
    socket.on('error', function (error) {
    console.log('socket event error', error);
    });
    socket.on('drain', function () {
    console.log('socket event drain');
    });
    socket.on('timeout', function () {
    console.log('socket event timeout');
    });
    });

    server.listen(5000, () => {
    console.log('start server 5000');
    });

    tcp-client.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    let net = require('net');

    let client = net.connect({port: 5000}, function () {
    console.log('socket event connect');
    client.write('Hello');
    });
    // client.on('connect', function (buffer) {
    // console.log('socket event connect');
    // client.write('Hello');
    // });
    // client.setTimeout(3000);
    client.on('ready', function () {
    console.log('socket event ready');
    });
    client.on('data', function (buffer) {
    console.log('socket event data', buffer.toString());
    client.end();
    });
    // client.setNoDelay(true);// 关闭Nagle算法
    // FIN
    client.on('end', function () {
    console.log('socket event end');
    });
    // close
    client.on('close', function (had_error) {
    console.log('socket event close', had_error);
    });
    client.on('lookup', function (err, address, family, host) {
    console.log('socket event lookup', address, family, host, err);
    });
    client.on('error', function (error) {
    console.log('socket event error', error);
    });
    client.on('drain', function () {
    console.log('socket event drain');
    });
    client.on('timeout', function () {
    console.log('socket event timeout');
    });

    Nagle算法

    如果每次只发一个字节的内容而不优化,网络中将充满只有极少数有效数据的数据包,将十分浪费网络资源。为此,Node引入了Nagle算法。Nagle要求缓冲区的数据达到一定数量或者一定时间后才将其发送,所以小数据包将会被Nagle算法合并,以此来优化网络。这种优化虽然使网络带宽被有效使用,但数据有可能被延迟发送。

  • UDP

    UDP又称为用户数据包协议,与TCP一样同属于网络传输层。UDP与TCP最大的区别是UDP不是面向连接的。在UDP中,一个套接字可以跟多个UDP服务通信,它虽然提供面向事务的简单不可靠传输服务,在网络差的情况下存在丢包严重的问题,但是由于它无需连接,资源消耗低,处理快速且灵活,所以常常应用在那种丢一两个数据包也不会产生重大的场景,比如音频、视频等。DNS就是基于它实现的.

    udp-server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let dgram = require('dgram');
    let server = dgram.createSocket('udp4');
    // 当有新的数据包被 socket 接收时, 'message' 事件会被触发。msg 和 rinfo 会作为参数传递到该事件的处理函数中。
    server.on('message', function (msg, rinfo) {
    console.log('server event -> message msg, remoteInfo', msg.toString(), rinfo);
    server.send('Server World', rinfo.port, rinfo.address, function () {
    server.close();
    });
    });
    // 当一个 socket 开始监听数据包信息时, 'listening' 事件将被触发。 该事件会在创建 UDP socket 之后被立即触发。
    server.on('listening', function () {
    console.log('server event -> listening', server.address())
    });
    // 当有任何错误发生时, 'error' 事件将被触发。 事件发生时,事件处理函数仅会接收到一个 Error 对象。
    server.on('error', function (error) {
    console.log('server event -> error', error);
    server.close();
    });
    server.on('close', function () {
    console.log('server event -> close')
    });

    server.bind(8000);

    udp-client.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let dgram = require('dgram');
    let client = dgram.createSocket('udp4');
    client.on('message', function (msg, rinfo) {
    console.log('client event -> message msg, remoteInfo', msg.toString(), rinfo);
    });
    client.on('listening', function () {
    console.log('client event -> listening', client.address())
    });
    client.on('error', function (error) {
    console.log('client event -> error', error);
    client.close();
    });
    client.on('close', function () {
    console.log('client event -> close')
    });
    client.send('Client Hello', 8000);
  • HTTP
    HTTP的全称是超文本传输协议,HTTP构建在TCP之上,属于应用层协议

    在Node中,HTTP服务继承于TCP服务(net模块),它能够与多个客户端保持连接,由于其采用事件驱动的形式,并不为每一个连接创建额外的线程或进程,保持很低的内存占用,所以能实现高并发。HTTP服务和TCP服务模型有区别的地方在于,在开启keepalive后,一个TCP会话可以用于多次请求和响应。TCP服务以connection为单位进行服务,HTTP服务会话以request为单位进行服务。HTTP模块即是将connection到request的过程进行了封装。

    在Node中构建Http服务器及其容易,寥寥几行代码就可以实现一个HTTP服务器,代码如下:

    1
    2
    3
    4
    5
    6
    7
    let http = require('http');
    http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type':'text/plain'});
    res.end('Hello HTTP\n');
    }).listen(8080 , '127.0.0.1', function () {
    console.log('HTTP Server running at http://127.0.0.1/8080');
    });

    启动上述服务器代码后,我们通过工具curl进行一次报文的获取,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    $ curl -v http://127.0.0.1:8080
    * Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
    > GET / HTTP/1.1
    > Host: 127.0.0.1:8080
    > User-Agent: curl/7.64.1
    > Accept: */*
    >
    < HTTP/1.1 200 OK
    < Content-Type: text/plain
    < Date: Sat, 07 Dec 2019 10:32:24 GMT
    < Connection: keep-alive
    < Transfer-Encoding: chunked
    <
    Hello HTTP
    * Connection #0 to host 127.0.0.1 left intact
    * Closing connection 0

    从上述报文中,我们可以看到这次网络通信的报文信息分为几个部分,第一部分内容为经典的TCP的三次握手过程,如下所示:

    1
    2
    3
    * Trying 127.0.0.1...
    * TCP_NODELAY set
    * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)

    第二部分是在完成握手之后,客户端向服务端发送请求报文,如下所示:

    1
    2
    3
    4
    5
    > GET / HTTP/1.1
    > Host: 127.0.0.1:8080
    > User-Agent: curl/7.64.1
    > Accept: */*
    >

    第三部分是服务端完成处理后,向客户端发送响应内容,包括响应头和响应体,如下所示:

    1
    2
    3
    4
    5
    6
    7
    < HTTP/1.1 200 OK
    < Content-Type: text/plain
    < Date: Sat, 07 Dec 2019 10:32:24 GMT
    < Connection: keep-alive
    < Transfer-Encoding: chunked
    <
    Hello HTTP

    最后部分是结束本次会话的信息,如下所示:

    1
    2
    * Connection #0 to host 127.0.0.1 left intact
    * Closing connection 0

    从上述的报文信息中,可以看出HTTP是基于请求响应式的,以一问一答的形式实现服务,虽然基于TCP会话,但是本身却无会话的特点。简而言之,HTTP服务只做两件事情:处理HTTP请求和发送HTTP响应


  • WebSocket
    WebSocket实现了客户端与服务端之间的长连接。推荐库 socket.io
    WebSocket与传统HTTP有如下好处:

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

    WebSocket协议主要由握手和数据传输两部分组成。这里我们用Node模拟浏览器发起协议切换的行为,代码如下:
    WebSocket.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    let http = require('http');
    class WebSocket {
    constructor(host, port) {
    this.host = host;
    this.port = port;
    if (!!host && !!port)
    this.connect();
    }

    onopen() {
    console.log('WebSocket onopen');
    }
    onmessage(data) {
    console.log('WebSocket onmessage', data.toString());
    }
    setSocket(socket) {
    this.socket = socket;
    this.socket.on('data', this.onmessage);
    }

    send(data){
    data && this.socket.write(data);
    }

    connect() {
    // WebSocket 握手协议部分是在 HTTP实现的
    let req = http.request({
    port: this.port,
    hostname: this.host,
    headers: {'Connection': 'Upgrade', 'Upgrade': 'WebSocket'}
    });
    req.on('upgrade', (response, socket, head) => {
    console.log('WebSocket event->upgrade');
    this.setSocket(socket);
    this.onopen();
    });
    req.end();
    }
    }

    服务端
    Server.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let server = http.createServer( function (req, res) {
    res.writeHead(200, {'Content-Type':'text/plain'});
    res.end('hello world\n');
    });

    server.on('upgrade', function (response, socket, head) {
    console.log('server event->upgrade', head);
    let header = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: WebSocket', 'Connection: Upgrade'];
    socket.setNoDelay(true);
    socket.write(header.concat('', '').join('\r\n'));
    let websocket = new WebSocket();
    websocket.setSocket(socket);
    websocket.send('Hello Client');
    });
    server.listen(8000);

    客户端
    Client.js

    1
    2
    3
    4
    5
    let wb = new WebSocket('127.0.0.1', 8000);
    wb.onopen = function () {
    console.log('onopen');
    wb.send('Hello Server');
    };