Skip to content

Latest commit

 

History

History
1084 lines (792 loc) · 52 KB

File metadata and controls

1084 lines (792 loc) · 52 KB

二、使用 Node.js 访问网络

Node.js 被设计为在网络环境中运行良好。它的非阻塞、事件驱动架构允许使用高度可伸缩的网络应用。在这一章中,您将发现许多围绕 Node.js 及其网络功能的实现细节。特别是,您将看到的食谱将涵盖这些主题:

  • 设置服务器
  • 创建到服务器的连接
  • 配置服务器默认值
  • 创建客户端
  • 使用套接字在服务器之间进行通信
  • 正在检索有关已连接服务器的详细信息
  • 控制套接字详细信息

一旦你阅读了这一章,你应该不仅有能力构建一个简单的网络应用,而且可能有一个健壮的解决方案来整合到你的工作流程中。

2-1.设置服务器

问题

您需要设置一个服务器来提供联网的 Node.js 应用。

解决办法

在 Node.js 中,构建在端点之间提供数据的网络应用的标准解决方案是利用一个名为net的内置 Node.js 模块。该模块提供了设置 Node.js TCP 服务器所需的全部内容。要设置一个 Web 服务器,你必须首先需要这个模块(见清单 2-1 )。

清单 2-1 。需要net模块

var net = require('net');

在需要这个模块之后,使用createServer()方法创建服务器。这个方法带有一个可选参数,它将在服务器上设置默认选项,还有一个connectionListener参数,它将监听到您的服务器的连接。要真正启用新创建的服务器,您需要告诉您的服务器监听哪个端口。这是通过调用由net模块提供的listen()方法来完成的。清单 2-2 中显示了一个完全运行的服务器。

清单 2-2 。一个简单的 TCP 服务器

var net = require('net');

var server = net.createServer(function(connectionListener) {
    console.log('connected');

    //Get the configured address for the server
    console.log(this.address());

        //get connections takes callback function
    this.getConnections(function(err, count) {
        if (err) {
            console.log('Error getting connections');
        } else {
            console.log('Connections count: ' + count);
        }
    });

    connectionListener.on('end', function() {
        console.log('disconnected');
    });
    //Write to the connected socket
    connectionListener.write('heyyo\r\n');
});

server.on('error', function(err) {
    console.log('Server error: ' + err);
});
server.on('data', function(data) {
    console.log(data.toString());
});

/**
* listen()
*/
server.listen(8181, function() {
    console.log('server is listening');
});

现在您已经创建了一个简单的服务器。假设您已经将您的服务器文件命名为 server.js,您可以很容易地用 Nodeserver.js运行它。

它是如何工作的

让我们更详细地检查一下这个服务器。首先,回忆一下 Node.js 模块是如何加载的,如第 1 章中的所述。这就是 native Node.js 模块net的加载方式,require('net') ;.服务器是通过模块导出的createServer()方法创建的,该方法在net模块中实例化一个内部服务器对象,如清单 2-3 所示。

清单 2-3 。net 模块创建服务器方法

exports.createServer = function() {
  return new Server(arguments[0], arguments[1]);
};

这个方法有两个参数,所以在服务器函数中,一定要确定哪个参数代表 options 对象,这个对象可以选择传递给createServer()方法,也就是连接监听器。如果您进一步研究这个函数,您会发现 Node.js 用来确定这些参数的是对它们的属性的简单检查。如果确定第一个参数的类型是函数,则第一个参数不可能是 options 对象,从而使第一个参数成为连接侦听器。或者,如果第一个参数不是函数,则假定它是 options 对象,如果第二个参数是函数,则将其用作连接监听器。

连接监听器,像 Node.js 编程中的许多函数一样,是一个简单的回调函数。一旦net模块中的服务器对象将它标识为一个函数,它就会作为回调传递给服务器连接监听器,其形式类似于server.on('connection', connectionListener);。这会将任何新连接传递回应用中的侦听器。这个逻辑如清单 2-4 所示。

清单 2-4 。确定服务器选项和连接监听器

var self = this;

var options;

if (typeof arguments[0] == 'function') {
  options = {};
  self.on('connection', arguments[0]);
} else {
  options = arguments[0] || {};

  if (typeof arguments[1] == 'function') {
    self.on('connection', arguments[1]);
  }
}

在您创建的服务器开始监听端口后,会出现一个新的连接。端口由传递给服务器的listen()函数 的第一个参数决定。如果您的服务器要监听 UNIX 路径或任何可连接的句柄对象,那么listen()函数也可以接受一个路径。在清单 2-2 中的示例服务器中,端口被设置为 8181。第二个参数是回调,一旦服务器成功开始监听定义它的端口或路径,就会执行回调。listen()事件也假设了一个宿主。主机可以是任何 IPv4 地址,但是如果省略,Node.js 会认为您的目标是localhost。现在您有了一个简单的服务器,它将监听您选择的端口。

正如您在清单 2-2 中创建的服务器中所看到的,您还可以深入了解服务器的当前配置。首先,您可以检索关于服务器正在监听的地址的信息。这个信息是通过调用server.address()方法获取的。这将返回一个显示服务器地址、家族和端口的对象(见清单 2-5 )。

清单 2-5 。server.address( )

{
  address: '127.0.0.1',
  family: 'IPv4',
  port: 8181
}

除了检索服务器地址,您还可以获得到您的服务器的连接数。这是通过在代码中调用getConnections() 方法来完成的。getConnections()函数接受一个回调函数,该函数应该接受两个参数:一个错误参数和一个计数参数。这将允许您在获取连接时检查错误,并获得到服务器的当前连接数。这显示在清单 2-2 中创建的服务器内的connectionListener回调中。

Node.js 的net模块中的服务器对象是一个event emitter,这是 Node.js 编程中常见的范式。event emitter提供了一种通用语言,对象可以用这种语言注册、删除和监听由系统生成或由开发人员定制的事件。服务器对象公开了几个事件,其中一些您已经见过了,比如连接和监听事件。connection 事件在每次新的套接字连接到服务器时发生,而 listening 事件,如您所见,是在服务器开始监听时发出的。作为net.Server对象基础的另外两个事件是 close 和 error。当服务器遇到错误时,将发出 error 事件。发出错误后,error 事件还会立即发出 close 事件。close 事件只是关闭服务器;但是,它会一直等到每个连接的套接字的连接结束。

2-2.创建到服务器的连接

问题

您需要创建一个到 Web 服务器的连接。

解决办法

为了建立到服务器的连接,您需要知道它监听的端口或 UNIX 路径。一旦了解了这一点,就可以通过 Node.js 创建一个连接。为此,您将再次使用 Node.js 本机net模块,该模块公开了一个createConnection方法 ,用于连接到一个远程(或本地)实例。

为了利用net模块通过 Node.js 连接到服务器,你必须再次通过一个 CommonJS require 设置到net模块的连接,如清单 2-6 所示。

清单 2-6 。导入网络模块进行连接

var net = require('net');

然后下一步是调用createConnection方法,传递要连接的端口或 UNIX 路径。或者,如果需要指定 IP 地址,也可以传递主机。现在我们可以创建一个记录控制台连接的 connectListener,如清单 2-7 所示。

清单 2-7 。创建到服务器的连接

var net = require('net');
// createConnection
var connection = net.createConnection({port: 8181, host:'127.0.0.1'},
// connectListener callback
    function() {
        console.log('connection successful');
});

它是如何工作的

在本节中,您创建了一个到 TCP 服务器的连接。这是用 Node.js 的net模块完成的。这包含了与connect()函数相同的createConnection函数。connect 方法首先检查您传递给它的参数。它将评估设置了哪些选项。

检查发送的参数是通过首先检查第一个参数是否是一个对象,然后如果它确实是一个对象就解析这个对象。如果第一个参数不是一个对象,它将被评估以查看是否是一个有效的管道名,在这种情况下,它将被设置为 UNIX path 选项。如果它不是管道的名称,它将默认为一个端口号。对参数的最后检查是对可选回调参数的检查,通过检查传递给connect()函数的最后一个参数是否是函数本身来评估。整个过程在一个名为normalizeConnectArgs的函数中运行,如清单 2-8 所示。

清单 2-8 。提取 createConnection 参数

function normalizeConnectArgs(args) {
  var options = {};

  if (typeof args[0] === 'object') {
    // connect(options, [cb])
    options = args[0];
  } else if (isPipeName(args[0])) {
    // connect(path, [cb]);
    options.path = args[0];
  } else {
    // connect(port, [host], [cb])
    options.port = args[0];
    if (typeof args[1] === 'string') {
      options.host = args[1];
    }
  }

  var cb = args[args.length - 1];
  return (typeof cb === 'function') ? [options, cb] : [options];
}

接下来,net模块创建一个新的 socket 对象,传递新规范化的连接参数。这个套接字在其原型上有一个名为connect的方法。

调用套接字上的这个 connect 方法,并向其传递规范化的参数。connect 方法将尝试创建一个新的套接字句柄,并连接到参数中指定的路径或端口和主机组合。如果没有为给定端口指定主机,则假定目标主机是localhost127.0.0.1。有趣的是,如果参数中提供了主机名或 IP 地址,Node.js 将需要dns模块并执行 DNS 查找来定位主机。如果查找没有错误地返回 null,这将再次默认为localhost,如清单 2-9 所示。

清单 2-9 。Socket.prototype.connect 的方法 解析路径、端口和主机

/* ... */
if (pipe) {
    connect(self, options.path);

  } else if (!options.host) {
    debug('connect: missing host');
    connect(self, '127.0.0.1', options.port, 4);

  } else {
    var host = options.host;
    debug('connect: find host ' + host);
    require('dns').lookup(host, function(err, ip, addressType) {
      // It's possible we were destroyed while looking this up.
      // XXX it would be great if we could cancel the promise returned by
      // the lookup.
      if (!self._connecting) return;

      if (err) {
        // net.createConnection() creates a net.Socket object and
        // immediately calls net.Socket.connect() on it (that's us).
        // There are no event listeners registered yet so defer the
        // error event to the next tick.
        process.nextTick(function() {
          self.emit('error', err);
          self._destroy();
        });
      } else {
        timers.active(self);

        addressType = addressType || 4;

        // node_net.cc handles null host names graciously but user land
        // expects remoteAddress to have a meaningful value
        ip = ip || (addressType === 4 ? '127.0.0.1' : '0:0:0:0:0:0:0:1');

        connect(self, ip, options.port, addressType, options.localAddress);
      }
    });
  }
/* ... */

从清单中可以看出,发现路径、端口或端口和主机的结果是调用函数connect()。这个函数只是将套接字句柄连接到路径或端口和主机。一旦连接请求被连接,就调用connectListener回调作为connect函数的代码,如清单 2-10 所示。

清单 2-10 。函数 connect()在 net 模块中实现

function connect(self, address, port, addressType, localAddress) {

  assert.ok(self._connecting);

  if (localAddress) {
    var r;
    if (addressType == 6) {
      r = self._handle.bind6(localAddress);
    } else {
      r = self._handle.bind(localAddress);
    }

    if (r) {
      self._destroy(errnoException(process._errno, 'bind'));
      return;
    }
  }

  var connectReq;
  if (addressType == 6) {
    connectReq = self._handle.connect6(address, port);
  } else if (addressType == 4) {
    connectReq = self._handle.connect(address, port);
  } else {
    connectReq = self._handle.connect(address, afterConnect);
  }

  if (connectReq !== null) {
    connectReq.oncomplete = afterConnect;
  } else {
    self._destroy(errnoException(process._errno, 'connect'));
  }
}

这是清单 2-8 中的函数,您在这里将“连接成功”记录到控制台。正如您将在 2-4 节中看到的,监听和连接客户端不仅仅是简单地将一个字符串记录到控制台,但是首先您将检查配置服务器的各种方式以及配置选项附带的默认设置。

2-3.配置服务器默认值

问题

您正在 Node.js 中创建一个服务器,并且需要控制该服务器的可访问缺省值。

解决办法

当您创建任何类型的 Web 服务器时,您经常会发现可能需要调整默认配置以满足您的特定需求。除了为 TCP 服务器设置主机和端口之外,您可能希望能够设置最大连接数,或者像在您的服务器中那样控制挂起连接的系统积压队列长度。许多这些设置在您的服务器上都有默认值。

很自然,服务器中您可以控制的最简单的部分之一就是服务器将要监听的端口和主机。这些是在服务器上调用listen()方法时设置的。listen 方法(如 2-1 节所见)也接受侦听器回调,但是第三个参数是 backlog 设置,可以选择放在这个回调之前,它限制服务器的连接队列长度。将这些缺省设置到位,您可以看到listen()函数在清单 2-11 中的样子。

清单 2-11 。设置 listen()默认值

server.listen(8181, '127.0.0.1', 12, function() {
        // listen on 127.0.0.1:8181
        // backlog queue capped at 12
        console.log('server is listening');
});

另一个需要考虑的默认选项是调用createServer()方法时设置的选项,它允许半开连接,默认为 false,但在方法中设置,如清单 2-12 所示。

清单 2-12 。allowalfopen:true

var server = net.createServer({ allowHalfOpen: true }, function(connectionListener) {
/* connection Listener stuffs */
});

在 Node.js 应用中,设置到服务器的最大连接数也非常有用。如果您希望对此加以限制,您必须显式设置该数字,因为它默认为未定义。这最好在connectionListene r回调中设置,如清单 2-13 所示。

清单 2-13 。设置到服务器的最大连接数

var server = net.createServer({ allowHalfOpen: true }, function(connectionListener) {
        console.log('connected');

        //get maxConnections - default undefined
        console.log(this.maxConnections);

        // set maxConnections to 4
        this.maxConnections = 4;

        // check set maxConnections is 4
        console.log(this.maxConnections);
});

它是如何工作的

通过对照默认设置检查服务器默认值,可以设置和覆盖服务器默认值;然后它们会被覆盖。向 Node.js 中的listen()方法传递 backlog 参数会发生什么?首先,传递给 backlog 参数的默认值是 511。传递值 511 是因为操作系统内核是如何确定积压工作大小的。

//使用 512 个条目的积压。我们将 511 传递给 listen()调用,因为

//内核确实:backlogsize = round up _ pow _ of _ two(backlogsize+1);

这将会给我们带来 512 个条目的积压。

知道这个很有趣。因为您在清单 2-11 中的server.listen() 示例中将 backlog 队列设置为上限为 12,所以您现在可以知道这将被计算为 16。这是因为您设置的值 12 递增 1,然后向上舍入到最接近的 2 的幂,即 16。需要注意的是,在清单 2-11 的示例server.listen中,您将主机地址的值设置为 127.0.0.1,也就是 IPv4。然而,Node.js 同样容易处理 IPv6 连接,因此您可以更改您的默认服务器监听以使用 IPv6,如清单 2-14 所示。

清单 2-14 。使用 IPv6 配置服务器

server.listen(8181, '::1', 12, function() {
    console.log(server.address());
});

随后,server.address()函数将记录新主机,并且该系列现在将是 IPv6 而不是 IPv4。

{ address: '::1', family: 'IPv6', port: 8181 }

允许半开连接是你在清单 2-12 、{ allowHalfOpen: true }中设置的选项。这将连接设置为允许对服务器连接进行更细粒度的控制。这将允许连接发送 TCP FIN 数据包,该数据包请求终止连接,但不会自动向连接发送响应 FIN 数据包。

这意味着您将保留一半的 TCP 连接,允许套接字保持可写但不可读。要正式关闭连接,必须通过调用。end()方法。

您还看到了如何通过 Node.js 和net模块的maxConnections设置来限制到服务器的最大连接数。默认情况下,这是未定义的,但是在清单 2-13 中,它被设置为一个较小的数字 4。这意味着您的连接数限制为 4,但是当您连接或试图连接到一个设置了最大连接数的服务器时会发生什么呢?你可以在清单 2-15 中看到 Node.js 源码对这个设置做了什么。

清单 2-15 。Node.js 处理 maxConnections 设置

if (self.maxConnections && self._connections >= self.maxConnections) {
    clientHandle.close();
    return;
}

这让您对为什么 maxConnections 默认为 undefined 有了更多的了解。这是因为如果没有设置它,Node.js 就没有必要为这部分代码费心。但是,如果设置了它,一个简单的检查将查看服务器上的当前连接数是否大于或等于 maxConnections 设置,并且它将关闭连接。如果你有一个 Node.js 客户端连接想要连接(你将在 2-4 节中读到更多),但是连接数超过了这个限制,你将看到这个连接的关闭事件被发出,你可以适当地处理它,如清单 2-16 所示。

清单 2-16 。处理连接句柄上的关闭事件

connection.on('close', function() {
    console.log('connection closed');
});

另一方面,如果你只是通过 Telnet (telnet ::1 8181)点击服务器端点,响应将是“连接被外来主机关闭”,如图 2-1 中的所示。

9781430260585_Fig02-01.jpg

图 2-1 。Telnet 连接关闭

2-4.创建客户端

问题

您希望使用 Node.js 创建一个连接到 Web 服务器的客户机。

解决办法

创建一个功能性 Node.js 客户机扩展了您在第 2-2 节中学到的概念。也就是说,客户机只是一个到服务器端点的连接。前面您已经看到了如何启动连接;在本节中,您将学习如何获取那个连接的套接字,并理解与之相关联的事件。

让我们假设我们将把我们的客户机连接到一个简单的 Node.js 服务器,类似于您在第 2-1 节中创建的服务器。但是,该服务器将从客户端接收消息,并向客户端写入消息。该消息将是一个简单的文本消息,显示当前到服务器的连接数。该服务器如清单 2-17 中的所示。

清单 2-17 。简单的 Node.js 服务器回显到客户端

var net = require('net');

var server = net.createServer(function(connectionListener) {
        //get connection count
        this.getConnections(function(err, count) {
                if (err) {
                        console.log('Error getting connections');
                } else {
                        // send out info for this socket
                        connectionListener.write('connections to server: ' + count + '\r\n');
                }
        });

        connectionListener.on('end', function() {
                console.log('disconnected');
        });

        //Make sure there is something happening
        connectionListener.write('heyo\r\n');

        connectionListener.on('data', function(data) {
                console.log('message for you sir: ' + data);
        });

        // Handle connection errors
        connectionListener.on('error', function(err) {
                console.log('server error: ' + err);
        });
});

server.on('error', function(err) {
        console.log('Server error: ' + err);
});

server.on('data', function(data) {
        console.log(data.toString());
});

server.listen(8181, function() {
        console.log('server is listening');
});

首先,您会看到,当使用 Node.js 中的net模块创建连接的客户端时,您需要注册可以通过 Node.js 事件发射器发出的底层事件。在您将创建的示例客户端中,这些事件被设置为监听dataenderror。这些事件接受回调,回调可用于处理通过这些事件传输的数据。这以 2-2 节中显示的服务器为例,并把它变成你在清单 2-18 中看到的样子。

清单 2-18 。带有套接字事件的客户端

var net = require('net');

// createConnection
var connection = net.createConnection({port: 8181, host:'127.0.0.1'},
// connectListener callback
        function() {
                console.log('connection successful');
                this.write('hello');
});

connection.on( 'data' , function(data) {
        console.log(data.toString());
});

connection.on('error', function(error) {
        console.log(error);
});

connection.on('end', function() {
        console.log('connection ended');
});

如您所见,在客户机上注册事件侦听器有许多选项。这些事件是确定服务器状态或处理来自服务器的响应缓冲区的网关。这些可以帮助您确定 Node.js 应用中联网客户端发送的状态和信息。

客户端(见清单 2-18 )中还有一种最简单的可以发送给服务器的通信形式:套接字上的write()方法。在这种情况下,套接字是在实例化连接时创建的。一旦连接建立,它只需向服务器发送一个字符串“hello”。这在客户端通过connectionListener's数据事件绑定来处理。

connectionListener.on('data', function(data) {
        console.log('message for you sir: ' + data);
});

如果一切运行正常,您将在控制台输出中看到客户机与您的服务器交互,如清单 2-19 和清单 2-20 所示。

清单 2-19 。命令行上的服务器交互

$ node server.js
server is listening
message for you sir: hello

清单 2-20 。客户端与服务器通信

$ node client.js
Connection successful
Heyo

它是如何工作的

当您研究这个客户端如何与您的服务器连接和通信时,您会再次看到我们已经使用 Node.js 自带的net模块创建了一个到服务器的连接。这个模块具有在 TCP 服务器和客户端之间顺利通信的能力。在你在清单 2-17 中创建的例子中,你创建了一个监听端口和主机的连接,如 2-2 节所述。一旦创建了这个连接,并将其设置为变量“client ”,就需要三个参数。因为客户端实际上是一个 TCP 套接字的表示,所以它们是公开的。

无论如何,套接字是在实现net.createConnection()方法时创建的。这意味着您现在可以访问在套接字之间传递的选项和事件。这可以通过查看这些套接字的 Node.js 源代码来演示。在 Node.js 中,net套接字是一个流的表示。这意味着为了理解当connection.end发生时正在执行的代码,你可以看到它实际上是socket.end方法的一个表示,如清单 2-21 所示。

清单 2-21 。Socket.end 方法 T7】

Socket.prototype.end = function(data, encoding) {
  stream.Duplex.prototype.end.call(this, data, encoding);
  this.writable = false;
  DTRACE_NET_STREAM_END(this);

  // just in case we're waiting for an EOF.
  if (this.readable && !this._readableState.endEmitted)
    this.read(0);
  return;
};

从清单 2-21 中可以看到,你可以访问实际上是一个流的套接字。“end”方法调用此流的 end,并立即将该流设置为不可写。当流的另一端发送 FIN 包时,会触发 end 事件,您可以在前面的小节中看到这一点。在那里,您检查了半开的套接字连接;然而,在这种情况下,套接字不再是可写的。然后是最后一轮检查,看看流中是否还有可读的实体,在它返回之前读取,最终确定套接字的“结束”。

在清单 2-19 和 2-20 中,您看到服务器是用命令node server.js启动的。这立即产生了.listen()回调,它将消息“服务器正在监听”打印到您的控制台。然后启动客户端(node client.js,并调用connectListener回调函数,在控制台中显示“连接成功”。这个连接还从服务器发起一个Socket.write(),从客户端发起一个Socket.write()。在下一节中,您将了解更多关于利用套接字进行通信的内容,但是现在您确实需要理解Socket.write的最终结果是每个套接字沿着套接字发送它的数据。这导致在服务器上产生来自客户机的“hello”消息,并通过服务器在客户机上产生“heyo”消息。

如果您检查数据事件(为客户端处理数据接收的事件),您会看到每次接收数据时都会发出该事件。当您监听这个事件时,您将能够看到从您的服务器传输的数据。Node.js 中的数据以缓冲区或字符串的形式传输。默认情况下它是作为缓冲区发出的,但是如果你设置了socket.setEncoding()函数,你会看到数据是作为一个字符串传输的。在这个解决方案中,您通过Socket.write()方法发送数据,该方法默认使用 UTF-8 编码发送数据。data 事件是在 Node.js 的 stream 模块中触发的。stream 模块是从 Node.js 的net模块中的socket.write()方法触发的,如清单 2-22 所示。

清单 2-22 。从 socket.write()触发 Streams 模块

if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk))
    throw new TypeError('invalid data');
  return stream.Duplex.prototype.write.apply(this, arguments);

一旦您将数据处理到流接口中,您就可以在模块中蜿蜒前进,直到找到可读流所在的位置。小溪。Readable 是一个有函数的可读流的实例,emitDataEvents . This is the “data” that will be read into the server that you send from your client. This lets an event listener, which is registered on the data event, actually go through the readable event on the stream, emitting the stream.read() 作为“数据”返回.on('data')。这部分源代码可以在[清单 2-23](#list23) 中查看。

`清单 2-23 。从流模块发出数据事件

stream.readable = true;
stream.pipe = Stream.prototype.pipe;
stream.on = stream.addListener = Stream.prototype.on;

stream.on('readable', function() {
  readable = true;

  var c;
  while (!paused && (null !== (c = stream.read())))
    stream.emit('data', c);

  if (c === null) {
    readable = false;
    stream._readableState.needReadable = true;
  }
});

这部分代码强调了将数据作为流传输的要点。您可以看到在emitDataEvents()方法 中,流监听它自己的可读事件。一旦可读事件注册,则调用 stream.read()事件,将数据传递给变量“c”,然后流发出数据事件,同时传递参数“c”

本节中为 Node.js 客户机创建的另一个事件侦听器是在 error 事件上注册的。当套接字遇到错误时,将发出此事件。一个很好的例子是,如果您的客户机连接到服务器,当连接到服务器失败时,您将得到一个错误。如果您关闭服务器,您将收到的错误是连接重置。这将是一个类似于清单 2-24 中的的对象。

清单 2-24 。错误:连接重置

{ [Error: read ECONNRESET] code: 'ECONNRESET', errno: 'ECONNRESET', syscall: 'read' }

现在,您应该能够在 Node.js 环境中构建一个联网的客户端了。通过套接字进行通信的过程将在第 2-5 节中详细介绍。

2-5.使用套接字在服务器之间进行通信

问题

您希望在 Node.js 中构建一个网络应用,并利用套接字在实例之间进行通信。

解决办法

套接字对于 Node.js net模块来说是本地的。这意味着如果您希望利用套接字,您需要在脚本中使用net模块。然后,您将通过调用Socket()构造函数来创建一个新的套接字实例。然后要连接一个套接字,你只需用socket.connect()方法创建一个连接,将套接字指向你想要连接的端口和主机(见清单 2-25 )。

清单 2-25 。创建套接字连接

var net = require('net');

var socket = new net.Socket();

socket.connect(/* port */ 8181, /*host*/ '127.0.0.1' /, *callback*/ );

假设可以在localhost的端口 8181 上建立连接,那么您现在就有了一个连接到该服务器的套接字。此时,除了通过这个套接字连接的流之外,什么也没有。任何传输的数据都将丢失。现在让我们仔细看看一个简单服务器的套接字连接,以便在彼此之间共享消息。为此,您可以创建一个简单的服务器(清单 2-26 ),它将监听套接字及其数据,并向套接字发回响应。

清单 2-26 。将与套接字通信的服务器

var net = require('net');

var server = net.createServer(connectionListener);

server.listen(8181, '127.0.0.1');

function connectionListener(conn) {
        console.log('new client connected');
        //greet the client
        conn.write('hello');

        // read what the client has to say and respond
        conn.on('readable', function() {
                var data = JSON.parse(this.read());
                if (data.name) {
                        this.write('hello ' + data.name);
                }
        });

        //handle errors
        conn.on('error', function(e) {
                console.log('' + e);
        });
}

这个服务器将监听一个连接,然后通过套接字流用“hello”问候这个新连接。它还将监听来自套接字的数据,在这种情况下,套接字应该是一个 JSON 对象。然后,您可以解析“可读”流中的数据,并返回包含解析数据的响应。

清单 2-27 中的套接字连接展示了如何创建这个套接字,它将从清单 2-26 中的连接到服务器,并在两者之间发送通信。

清单 2-27 。插座连接

var net = require('net');

var socket = new net.Socket(/* fd: null, type: null, allowHalfOpen: false */);

socket.connect(8181, '127.0.0.1' /*, connectListener  replaces on('connect') */);

socket.on('connect', function() {
        console.log('connected to: ' + this.remoteAddress);
        var obj = { name: 'Frodo', occupation: 'adventurer' };
        this.write(JSON.stringify(obj));
});

socket.on('error', function(error) {
        console.log('' + error);
        // Don't persist this socket if there is a connection error
        socket.destroy();
});

socket.on('data', function(data) {
        console.log('from server: ' + data);
});
socket.setEncoding('utf-8'); /* utf8, utf16le, ucs2, ascii, hex */

socket.setTimeout(2e3 /* milliseconds */ , function() {
        console.log('timeout completed');
        var obj = { name: 'timeout', message: 'I came from a timeout'};
        this.write(JSON.stringify(obj));
});

将服务器和客户端服务器放在一起首先运行服务器,以便您的套接字有一个端点可以连接到您能够成功地与套接字连接进行通信。发起的服务器控制台将看起来像清单 2-28 ,而客户端服务器输出将看起来像清单 2-29

清单 2-28 。服务器输出

$ node server.js
new client connected

清单 2-29 。连接的插座输出

$ node socket.js
Connected to: 127.0.0.1
From server: hellohello Frodo
Timeout completed
From server: hello timeout

它是如何工作的

如您所见,一个net.Socket连接是一个 Node.js 对象,表示一个 TCP 或 UNIX 套接字。在 Node.js 中,这意味着它实现了一个双工流接口。node 中的一个 duplex stream 表示两个event emitters,在 Node.js 中发布事件的对象,组成 duplex stream 的两个event emitters是可读流和可写流,你可以从清单 2-30 中的 Node.js duplex stream 源码中看到。

清单 2-30 。双工流调用可读 和可写流

function Duplex(options) {
  if (!(this instanceof Duplex))
    return new Duplex(options);

  Readable.call(this, options);
  Writable.call(this, options);

  if (options && options.readable === false)
    this.readable = false;

  if (options && options.writable === false)
    this.writable = false;

  this.allowHalfOpen = true;
  if (options && options.allowHalfOpen === false)
    this.allowHalfOpen = false;

  this.once('end', onend);
}

readable streams 接口将从流缓冲区接收数据,并在套接字上将它作为数据事件发出。另一方面,可写流将以写或结束事件的形式发出数据。这些一起构成了一个插座。socket 有一些有趣的属性和方法,您在清单 2-26 和 2-27 中使用了其中的一些来创建您的 socket 通信服务器。

在第一个服务器实例中,在connectionListener 回调中,传递了 conn 参数。因为一个net.Server对象实际上是一个将监听连接的套接字,所以这个 conn 参数表示您想要使用的套接字。这个服务器做的第一件事就是向连接发出问候。这发生在conn.write('hello');中,它是一种socket.write()方法。

socket.write()方法接受一个必需的参数、要写入的数据和两个可选参数。这些可选参数是 encoding,可用于设置套接字的编码类型。编码默认为 utf8,但其他有效值为 utf-8、utf16le、ucs2、ascii 和 hex。

接下来,在服务器的connectionListener中,套接字被绑定到可读事件。这个可读事件来自流模块。每当流发送准备读取的数据时,都会触发此事件。检索通过 readable 事件发送的数据的方法是调用read()事件来读取数据。在清单 2-26 的例子中,你期望数据是一个 JSON 字符串,然后你可以解析它来显示 JSON 对象。然后通过write()方法将另一条消息发送回连接。

服务器上的最终事件绑定是通过绑定到连接上的错误事件来处理错误。如果没有这一点,服务器将在连接发生错误时崩溃。这可能是一个被终止的连接,或者任何其他错误,但是不管是哪种类型的错误,没有什么比强大的错误处理功能更好的了。

现在看看你在清单 2-27 中做的套接字连接。这显示了我们沟通故事的另一面。它从一个新的net.Socket() 的实例化开始。在这个例子中,没有参数传递给构造函数。构造函数可以接受一个 options 对象,该对象的键为 fd、type 和 allowHalfOpen。

`fd 是文件描述符,或者套接字句柄应该是什么;这默认为 null。type 键也默认为空值,但是可以采用值 tcp4、tcp6 或 unix 来确定您希望实例化的套接字的类型。同样,正如您在前面几节中看到的,allowHalfOpen 选项可以设置为允许套接字在传输初始 FIN 包后保持打开。

为了连接套接字,您调用套接字上的connect()事件,并指定主机和端口。这将初始化 TCP 或 UNIX 套接字句柄,开始连接。host 参数是可选的,示例中省略的回调函数也是可选的。示例中的回调被替换了,因为 connect 函数上的回调函数与socket.on('connect', ...)事件侦听器相同,后者绑定到我们示例中的套接字,侦听要建立的连接。

connect事件回调中,您的解决方案做的第一件事是通过记录套接字的remoteAddress() 来获得一些关于连接的知识。在本章的下一节中,您将看到更多关于获取已连接服务器的信息。在获得这些信息之后,您创建一个包含一些信息的对象,使用JSON.stringify方法将它变成一个字符串,然后使用write()方法沿着套接字发送它。该对象必须编码为字符串;否则,写方法将失败,如清单 2-31 所示。

清单 2-31 。Node.js 网络模块中的 socket.write

Socket.prototype.write = function(chunk, encoding, cb) {
  if (typeof chunk !== 'string' &&
!Buffer.isBuffer(chunk))
    throw new TypeError('invalid data');
  return stream.Duplex.prototype.write.apply(this, arguments);
};

然后,套接字被绑定到error事件。这个事件将处理来自套接字的所有错误,但是这里值得注意的一点是,一旦错误被处理,通过提供给on('error')监听器的回调,socket.destroy();方法被调用。destroy 方法提供了一种有用且优雅的方式来防止任何进一步的 I/O 活动发生并关闭套接字。它通过关闭套接字句柄,在销毁过程中根据需要发出任何错误回调。最后,关闭句柄后会发出关闭事件,如清单 2-32 所示。

清单 2-32 。关闭 socket.destroy( ) 内的套接字

Socket.prototype._destroy = function(exception, cb) {
  debug('destroy');

  var self = this;

  function fireErrorCallbacks() {
    if (cb) cb(exception);
    if (exception && !self.errorEmitted) {
      process.nextTick(function() {
        self.emit('error', exception);
      });
      self.errorEmitted = true;
    }
  };

  if (this.destroyed) {
    debug('already destroyed, fire error callbacks');
    fireErrorCallbacks();
    return;
  }

  self._connecting = false;

  this.readable = this.writable = false;

  timers.unenroll(this);

  debug('close');
  if (this._handle) {
    if (this !== process.stderr)
      debug('close handle');
    var isException = exception ? true : false;
    this._handle.close(function() {
      debug('emit close');
      self.emit('close', isException);
    });
    this._handle.onread = noop;
    this._handle = null;
  }

  fireErrorCallbacks();
  this.destroyed = true;

  if (this.server) {
    COUNTER_NET_SERVER_CONNECTION_CLOSE(this);
    debug('has server');
    this.server._connections--;
    if (this.server._emitCloseIfDrained) {
      this.server._emitCloseIfDrained();
    }
  }
};

在套接字的错误处理程序之后,套接字被绑定到数据事件。该事件将从连接发送的可读流中产生数据,本质上是调用 stream.read()方法并将其作为数据事件发出。这为解析和处理从连接发送的信息提供了一个有用的地方。

正如您在上面看到的,对于套接字上的 write()方法,可以选择为通过套接字缓冲区发送的数据设置编码。这可以通过设置套接字上的 setEncoding( )参数为整个套接字进行配置。在上面的示例中,它被设置为 utf-8 字符串的默认值,但是可以被更改为任何有效的编码类型。将该设置更改为每个有效类型会导致不同的输出,如清单 2-33 所示。

清单 2-33 。编码的变化

# utf8
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout

# hex
connected to: 127.0.0.1
from server: 68656c6c6f
from server: 68656c6c6f2046726f646f
timeout completed
from server: 68656c6c6f2074696d656f7574

# ucs2
connected to: 127.0.0.1
from server: 
from server: 
timeout completed
from server:

# ascii
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout

# utf16le
connected to: 127.0.0.1
from server: hello
from server: hello Frodo
timeout completed
from server: hello timeout

最后,您看到了套接字可以通过使用 setTimeout 函数 来“等待”。setTimeout 接受一个参数和一个回调,该参数指示您选择等待的毫秒数。在示例应用中,这用于从套接字向连接发送消息,延迟两秒钟。为了使回调有效(如清单 2-34 所示),毫秒数必须大于零,并且是有限的,不能是数字(NaN)。如果是这种情况,Node.js 会将这个回调添加到计时器列表中,并在超时发生时发出超时事件。

清单 2-34 。socket.setTimeout

Socket.prototype.setTimeout = function(msecs, callback) {
  if (msecs > 0 && !isNaN(msecs) && isFinite(msecs)) {
    timers.enroll(this, msecs);
    timers.active(this);
    if (callback) {
      this.once('timeout', callback);
    }
  } else if (msecs === 0) {
    timers.unenroll(this);
    if (callback) {
      this.removeListener('timeout', callback);
    }
  }
};

网络上还有其他事件和参数。清单 2-27 中的套接字示例中没有包括的套接字。这些在下面的表 2-1 中进行了概述。

表 2-1 。套接字参数和事件

套接字参数或事件 描述
socket . end([数据],[编码]) 这个事件将 FIN 数据包发送到套接字的连接端,基本上关闭了一半的连接。如果设置了 allowHalfOpen,服务器仍然可以发送数据。您可以指定要发送的数据和编码,但这两个参数都是可选的。
socket.pause() 这正如您所期望的那样:它暂停了套接字上的数据发送。
socket.resume() 这将恢复套接字上的数据传输。
socket.setNoDelay([noDelay]) 这决定了 TCP 连接是否会在发送数据之前缓冲数据。这被称为“Nagle 算法”, noDelay 布尔参数默认为 true。
socket.setKeepAlive([enable]、[initialDelay]) 这将启用或禁用套接字的保持活动功能。这意味着在接收到最后一个数据包和初始延迟时间(默认为零)后,将会发送一个 keepalive 探测。启用布尔参数默认为 false。
socket.unref() 在 socket 上调用这个会检查 Node.js 事件系统,如果 socket 是这个系统中仅存的 socket,就允许它退出。
socket.ref() 一旦在套接字上设置了这个,如果它是唯一剩下的套接字,Node.js 中的事件系统将阻止程序退出。这与默认行为相反,默认行为会让程序退出,如果它是唯一剩下的套接字。
套接字.远程端口 这是套接字连接的端口。
套接字.本地地址 这是套接字源自的地址。
套接字.本地端口 这是套接字源自的端口。
socket . bytes loaded 这收集了从数据传输中读取的字节数。
socket . bytes loaded 这表示写入的字节数。

这些属性和事件将在本章的最后两节中详细介绍,在这两节中,您将发现如何检索有关连接的服务器的详细信息,以及如何在套接字本身中控制这些属性和详细信息。

2-6.正在检索有关已连接服务器的详细信息

问题

您希望能够在 Node.js 应用中获取有关连接的服务器和套接字的详细信息。

解决办法

要检索有关您连接的服务器的详细信息,您需要运用有关网络的知识。服务器和 net。您在前面章节中看到的插座模块。您可能有兴趣了解有关连接的许多细节,但是您可能感兴趣的是收集连接之间传输和接收的字节数。这是通过socket.bytesReadsocket.bytesWritten属性来处理的。由于各种原因,这些都是有价值的,但是许多人利用它来进行基准测试和记录应用的进度。清单 2-35 创建了一个带有循环连接的服务器,它记录了 Node.js 进程执行期间读写的字节总数。

清单 2-35 。计数字节

var net = require('net');

var PORT = 8181,
        totalRead = 0,
        totalWritten = 0,
        connectionCount = 0;

var server = net.Server(connectionListener);

function connectionListener(conn) {
        //tally the bytes on end
        conn.on('end', function() {
                totalRead += conn.bytesRead;
        });
}

server.listen(PORT);

//Connect a socket
var socket = net.createConnection(PORT);

socket.on('connect', function() {
        // plan on writing the data more than once
        connectionCount++;

        // My = 2 Bytes
        socket.write('My', function () {
                // Precious = 8 Bytes
                socket.end('Precious');
        });
});

// tally the bytes written on end
socket.on('end', function() {
        totalWritten += socket.bytesWritten;
});

socket.on('close', function() {
        // Each time we should get +=10 bytes Read and Written
        console.log('total read: ' + totalRead);
        console.log('total written: ' + totalWritten);
        // We're gonna do this a few times
        if (connectionCount < 5) {
                socket.connect(PORT);
        } else {
                server.close();
        }
});

现在,您可以访问在服务器和连接之间发送的字节数。这很好,但是现在您希望能够揭示服务器驻留在哪里以及套接字来自哪里的细节。为此,您可以使用套接字属性、remoteAddressremotePort ( 清单 2-36 )。您可以通过在connectionListener回调函数中添加一行代码和在socket.on('connect')事件中添加另一行代码,将这些代码添加到上面的示例中。

清单 2-36 。添加一些地址和端口嗅探器

console.log(socket.remoteAddress + ":" + socket.remotePort);

它是如何工作的

获得关于连接的服务器的信息实际上很容易。在创建 Node.js 应用时,这些告诉您已经发送或接收了多少字节的数据点非常有价值。Node.js 如何构建这些数据并呈现给net模块供你消费?如果您检查 net 模块源代码,您会发现当创建新的套接字句柄时,bytesRead 值总是被设置为零,正如您所预料的那样。该值随后增加缓冲区的长度,该长度在缓冲区句柄的 onread 函数中被读取(如清单 2-37 所示)。

清单 2-37 。onread 事件将字节增加 read 的长度

function onread(buffer, offset, length) {
  var handle = this;
  var self = handle.owner;
  assert(handle === self._handle, 'handle != self._handle');

  timers.active(self);

  var end = offset + length;
  debug('onread', process._errno, offset, length, end);

  if (buffer) {
    debug('got data');

    // read success.
    // In theory (and in practice) calling readStop right now
    // will prevent this from being called again until _read() gets
    // called again.

    // if we didn't get any bytes, that doesn't necessarily mean EOF.
    // wait for the next one.
    if (offset === end) {
      debug('not any data, keep waiting');
      return;
    }

    // if it's not enough data, we'll just call handle.readStart()
    // again right away.
    self.bytesRead += length;

    // Optimization: emit the original buffer with end points
    var ret = true;
    if (self.ondata) self.ondata(buffer, offset, end);
    else ret = self.push(buffer.slice(offset, end));

    if (handle.reading && !ret) {
      handle.reading = false;
      debug('readStop');
      var r = handle.readStop();
      if (r)
        self._destroy(errnoException(process._errno, 'read'));
    }

  } else if (process._errno == 'EOF') {
    debug('EOF');

    if (self._readableState.length === 0)
      self.readable = false;

    if (self.onend) self.once('end', self.onend);

    // push a null to signal the end of data.
    self.push(null);

    // internal end event so that we know that the actual socket
    // is no longer readable, and we can start the shutdown
    // procedure. No need to wait for all the data to be consumed.
    self.emit('_socketEnd');
  } else {
    debug('error', process._errno);
    // Error
    self._destroy(errnoException(process._errno, 'read'));
  }
}

获取 bytesWritten 值并不像通过传递给 onread 函数的 length 参数增加一个值那样简单。事实上,正如在清单 2-38 中可以看到的,bytesWritten 参数是通过读取缓冲区的块长度或实际字节长度本身来生成的。

清单 2-38 。写入的字节数

Socket.prototype.__defineGetter__('bytesWritten', function() {
  var bytes = this._bytesDispatched,
      state = this._writableState,
      data = this._pendingData,
      encoding = this._pendingEncoding;

  state.buffer.forEach(function(el) {
    if (Buffer.isBuffer(el.chunk))
      bytes += el.chunk.length;
    else
      bytes += Buffer.byteLength(el.chunk, el.encoding);
  });

  if (data) {
    if (Buffer.isBuffer(data))
      bytes += data.length;
    else
      bytes += Buffer.byteLength(data, encoding);
  }

  return bytes;
});

remoteAddress 和 remotePort 参数来自套接字句柄本身。这些代表了 Node.js 句柄的 getpeername 对象之上的一个抽象(清单 2-39 ),它包含一个地址和一个端口参数。这使得 Node.js 为 remotePort 和 remoteAddress 参数定义一个 getter 变得很简单。

清单 2-39 。getpeername 方法和 remoteAddress 以及 remotePort 属性

Socket.prototype._getpeername = function() {
  if (!this._handle || !this._handle.getpeername) {
    return {};
  }
  if (!this._peername) {
    this._peername = this._handle.getpeername();
    // getpeername() returns null on error
    if (this._peername === null) {
      return {};
    }
  }
  return this._peername;
};

Socket.prototype.__defineGetter__('remoteAddress', function() {
  return this._getpeername().address;
});

Socket.prototype.__defineGetter__('remotePort', function() {
  return this._getpeername().port;
});

您已经看到 Node.js 如何很好地定义了支持网络应用的服务器和套接字上的属性,使它们易于检索和使用。当您开发 Node.js 应用时,获得关于连接服务的这些细节可以提供非常需要的信息。``