Skip to content

Latest commit

 

History

History
1196 lines (875 loc) · 57.9 KB

File metadata and controls

1196 lines (875 loc) · 57.9 KB

四、构建 Web 服务器

Web 服务器是用 Node.js 构建的典型应用。这是由于 Node.js 的主要目标。Node.js 非常适合构建高度可伸缩的、事件驱动的、网络化的应用——web 服务器。

在本章中,你将学习和理解如何用 Node.js 构建一个 web 服务器。你将看到从简单的 web 服务器到在你的服务器上处理静态文件的主题。这些主题只是使 web 服务器正常工作的一部分。为了全面了解 web 服务器,因为它可以通过 Node.js 实现,您还将学习以下内容:

  • 使用 HTTPS 创建安全套接字层(SSL)服务器
  • 配置标题
  • 管理 HTTP 状态代码
  • 处理 HTTP 请求和响应
  • 使用 HTTP 事件管理您的 web 服务器

4-1.设置 HTTP 服务器

问题

您需要创建一个简单的 web 服务器来通过 HTTP 提供内容。

解决办法

在 Node.js 中,web 服务器通常使用 HTTP 模块来设置。这提供了一个与 HTTP 协议交互的层。

假设您正在编写一个 web 服务器,当您连接到 web 服务器时,它将向客户端发送一条状态消息。在这个解决方案中,清单 4-1 ,这已经被简化为简单地写响应‘hello ’,然后结束响应。

清单 4-1 。简单 HTTP Web 服务器

/**
* Setting up an HTTP server
*/

var http = require('http');

var server = http.createServer(function(req, res) {
        res.write('hello');
        res.end();
});

server.listen(8080);

它是如何工作的

这个 web 服务器过于简化,因此您可以研究 HTTP 模块如何创建服务器。在这个解决方案中,您自然会从需要 http 模块开始。这个模块公开了一个函数http.createServer,它是服务器实际创建的地方。http.createServer方法实例化一个新的服务器对象。服务器对象接受一个requestListener回调函数。这将把响应和请求参数发送给 web 服务器的回调。

新的 web 服务器是一个 HTTP 服务器,它是从你在《T4》第二章中看到的net.Server对象派生而来的。服务器还为事件、connectionrequestclientError提供事件监听器。

清单 4-2 。由 createServer 实例化的服务器源

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.addListener('request', requestListener);
  }

  // Similar option to this. Too lazy to write my own docs.
  //http://www.squid-cache.org/Doc/config/half_closed_clients/
  //http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;

  this.addListener('connection', connectionListener);

  this.addListener('clientError', function(err, conn) {
    conn.destroy(err);
  });

  this.timeout = 2 * 60 * 1000;
}
util.inherits(Server, net.Server);

Server.prototype.setTimeout = function(msecs, callback) {
  this.timeout = msecs;
  if (callback)
    this.on('timeout', callback);
};

exports.Server = Server;

您已经创建了您的 web 服务器。接下来,告诉服务器您想在哪里监听请求。这是通过server.listen完成的。server.listen函数接受一个端口以及一个可选的主机名、backlog 和一个回调。server.listen方法的回调函数将监听“listening’事件。提供主机名将告诉服务器您将在哪里监听给定端口的请求。

image server.listen还有另外两个签名。一种替代方法是只提供一个 UNIX 路径和一个回调。这将在路径上开始一个套接字服务器。另一种方法是提供一个句柄——一个套接字或一个服务器——它将成为新的服务器。

一旦您的服务器在监听,您就可以从服务器提供您的响应。在您提供给http.createServer方法的请求监听器回调中有两个参数。这些参数表示所提供的 HTTP 请求和 HTTP 响应。在该解决方案中,您希望创建一个 web 服务器来发送对“hello”连接的响应。这是通过流式传输一个res.write(‘hello’)函数来完成的。一旦response.end()函数被调用,这将在客户端呈现。

Response.write将响应体的块作为第一个参数发送。可选的第二个参数用于设置这个块的字符编码。您可能认为响应只需要一个response.write,但这种想法是不正确的。事实上,对于每个响应,您都需要调用response.end()函数。

4-2.使用 SSL 构建 HTTPS 服务器

问题

您创建了一个 web 服务器,但是您想通过使用 SSL 加密的连接通过 HTTPS 提供内容来增加额外的安全级别。

解决办法

为了构建一个 SSL 服务器,在开始之前,您需要准备好一些东西。首先,您的客户端和服务器必须执行传输层安全性(TLS)握手。为此,您需要生成一个证书和密钥来验证您的 HTTPS 会话。这些密钥在客户端和服务器之间交换。一旦交换了密钥,验证和确认会话的过程就开始了。一旦密钥被认为是有效的,会话就像普通的 HTTP 连接一样通过 HTTPS 继续进行,只是增加了一层安全性。

从那里,您可以使用 Node.js 中的 HTTPS 模块。该模块的行为类似于 HTTP 模块,但是连接是通过 TLS/SSL 加密的。然后通过 Node.js 创建一个 HTTPS 服务器,如清单 4-3 所示。

清单 4-3 。HTTPS 服务器

/**
* HTTPS server
*/
var https = require('https');
var fs = require('fs');

var options = {
  key: fs.readFileSync('privatekey.pem'),
  cert: fs.readFileSync('certificate.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.write("https!\n");
  res.end();
}).listen(8080);

它是如何工作的

创建 HTTPS 连接从 TLS/SSL 开始。该协议确保客户端和服务器之间的安全通信。发生这种情况是因为客户端和服务器之间存在握手,在握手过程中,服务器向客户端公开其证书和公钥。然后,当客户端发送响应时,用服务器的公钥对响应进行加密,并进行验证。如果所有数据都被评估为有效,则会话将在 HTTPS 上继续。

但是如何获得这些证书和密钥呢?在 Node.js 中,SSL/TLS 实现利用了 OpenSSL。OpenSSL 是 SSL/TLS 的开源实现。这是一个能让你轻松实现密钥和证书的协议。为了生成这样一个密钥,你需要打开你的终端并输入如清单 4-4 所示的命令。

清单 4-4 。创建 TLS/SSL 密钥和证书

$ openssl genrsa -out privatekey.pem 1024
$ openssl req -new -key privatekey.pem -out certrequest.csr
$ openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem

在 Windows 上这略有不同,因为默认情况下 Windows 不包含 OpenSSL 实现。您应该首先从http://openssl.org/related/binaries.html下载一个二进制发行版。默认情况下,这将安装到您计算机上的 C:\OpenSSL-Win32。在那里,您可以打开 PowerShell 并从 C:\OpenSSL-Win32\bin 目录运行以下内容。

PS C:\OpenSSL-Win32\bin> .\openssl.exe genrsa –out privatekey.pem 1024
PS C:\OpenSSL-Win32\bin> .\openssl.exe req –new –key .\privatekey.pem –out certrequest.csr
PS C:\OpenSSL-Win32\bin> .\openssl.exe x509 –req –in .\certrequest.csr –signkey .\privatekey.pem –out certificate.pem

一旦创建了证书和密钥,现在就可以创建安全的服务器了。这从https.createServer方法开始。这个函数类似于http.createServer方法,除了创建安全连接。这是通过一个选项对象完成的。本例中使用的选项为创建tls.ServerT3 设置证书和密钥。你会在第 6 章中看到更多关于 SSL 和 TLS 的细节。为了实际读取密钥和证书文件的值,你使用文件系统读取它们,如第 3 章中所讨论的。一旦这些被读取,您就可以创建您的服务器。

清单 4-5 。HTTPS 服务器继承了 tls。计算机 Web 服务器

function Server(opts, requestListener) {
  if (!(this instanceof Server)) return new Server(opts, requestListener);

  if (process.features.tls_npn && !opts.NPNProtocols) {
    opts.NPNProtocols = ['http/1.1', 'http/1.0'];
  }

  tls.Server.call(this, opts, http._connectionListener);

  this.httpAllowHalfOpen = false;

  if (requestListener) {
    this.addListener('request', requestListener);
  }

  this.addListener('clientError', function(err, conn) {
    conn.destroy(err);
  });

  this.timeout = 2 * 60 * 1000;
}
inherits(Server, tls.Server);

一旦创建了服务器,您应该能够通过 SSL 连接访问它。要测试这一点,只需旋转服务器地址,您应该会看到响应“https!”写入您的控制台。另一方面,如果您不尝试访问服务器的 HTTPS 版本,您将无法从服务器获得预期的结果。

清单 4-6 。使用 cURL 查看您的安全连接

$ curl –khttps://localhost:8080 # works
https!

$ curl http://localhost:8080 # nope
curl: (52) Empty response from the server

4-3.在您的服务器上处理请求

问题

你有一个 HTTP 或 HTTPS 服务器。该服务器需要处理传入的请求。

解决办法

当您构建 web 服务器时,您需要处理请求。请求的形式多种多样,包含的内容很快就会变成大量的数据。在处理请求时,您需要能够有效地筛选传入的数据,以便处理头、方法和 URL 参数。

在此解决方案中,您将创建一个处理请求的 web 服务器。它可能看起来与您熟悉的许多 web 服务器相似。该服务器将处理请求头,并按照您认为合适的方式处理它们。例如,如果请求标头包含“不要跟踪”指令,则不发送跟踪 cookie。

在正确处理了头之后,您可能想要解析请求 URL。这将通过处理传入路径来帮助您处理 404 和一般应用路由。除了路径之外,您还可能对随请求一起发送的查询字符串参数感兴趣。

最后,您将需要检查启动请求的请求方法。这就是 HTTP 方法,在你创建任何应用,或者一个具象状态转移(REST)应用 编程接口(API)的时候都会很有用。

清单 4-7 。处理请求

/**
* Processing Requests
*/
var http = require('http'),
                url = require('url');

var server = http.createServer(function(req, res) {
        //Handle headers
        if (req.headers.dnt == 1) {
                console.log('Do Not Track');
        }

        //Parse the URL
        var url_parsed = url.parse(req.url, true);

            //What type of request is this
    if (req.method === 'GET') {
        handleGetRequest(res, url_parsed);
    } else if (['POST', 'PUT', 'DELETE'].indexOf(req.method) > -1) {
        handleApiRequest(res, url_parsed, req.method);
    } else {
        res.end('Method not supported');
    }

});

handleGetRequest = function(res, url_parsed) {
    console.log('search: ' + url_parsed.search);
    console.log('query: ' + JSON.stringify(url_parsed.query));
    console.log('pathname: ' + url_parsed.pathname);
    console.log('path: ' + url_parsed.path);
    console.log('href: ' + url_parsed.href);
    res.end('get\n');
};

handleApiRequest = function(res, url_parsed, method) {
    if (url_parsed.path !== '/api') {
        res.statusCode = 404;
        res.end('404\n');
    }
    res.end(method);
};

server.listen(8080);

它是如何工作的

该解决方案中的 web 服务器是为处理请求而构建的。它通过检查服务器收到的请求周围的细节来做到这一点。这个请求实际上是一个名为http.IncomingMessage的对象。

http.IncomingMessage 继承了可读流接口。在此基础上,它构建了一些对 HTTP 消息有用的对象,如清单 4-8 所示。

清单 4-8 。http。传入消息

function IncomingMessage(socket) {
  Stream.Readable.call(this);

  this.socket = socket;
  this.connection = socket;

  this.httpVersion = null;
  this.complete = false;
  this.headers = {};
  this.trailers = {};

  this.readable = true;

  this._pendings = [];
  this._pendingIndex = 0;

  // request (server) only
  this.url = '';
  this.method = null;

  // response (client) only
  this.statusCode = null;
  this.client = this.socket;

  this._consuming = false;

  this._dumped = false;
}
util.inherits(IncomingMessage, Stream.Readable);

正如您从源代码中看到的,http.IncomingMessage带来了几个对您的解决方案很重要的对象或设置。首先,它带来了标题。请求头是直接反映随请求一起发送的键值对的对象。当我试图从我的 web 浏览器向这个服务器发送一个请求时,标题看起来如清单 4-9 所示。

清单 4-9 。典型的请求头

{ host: 'localhost:8080',
  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:20.0) Gecko/20100101 Firefox/20.0',
  accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'accept-language': 'en-us,en;q=0.5',
  'accept-encoding': 'gzip, deflate',
  dnt: '1',
  connection: 'keep-alive' }

其次,在您的 web 服务器中,您需要处理request.url。这包含通过请求 URL 发送的所有信息。最简单的解析方法是利用 URL 模块。您可以告诉 URL 模块解析包含查询字符串的 request.url。

请求的第三部分对您的服务器有价值的是request.method . request.method将为您提供开始请求的 HTTP 方法。在该解决方案中,web 服务器被设置为模仿 web API。在这种情况下,API 方法的路由不完全由 URL 的路径决定,还由request.method决定。这些方法只是 HTTP 方法的字符串名称。您的解决方案以两种不同的方式处理这些不同的方法。首先,您服务一个 HTTP GET 请求;使用它将记录请求的一些细节,并响应该方法确实是一个 GET。第二,用其他方法模拟 API 路由方案。这些由一个单独的函数处理,该函数将检查以确保您请求的不仅是正确的方法,还有路径。正如你在解决方案中看到的,这些都是以这样的方式处理的,你可以卷曲每种类型来查看不同的结果,如你在清单 4-10 中看到的。

清单 4-10 。不同结果的卷曲

$ curl -X PUT http://localhost:8080/api
put
$ curl -X PUT http://localhost:8080/apis
404
$ curl -X DELETE http://localhost:8080/api
delete
$ curl -X TRACE http://localhost:8080/api
Method not supported

通过理解 Node.js 中伴随着http.request的信息,您能够利用它来构建您的 web 服务器来处理这些请求。接下来,您将看到如何从您的服务器发送响应。

4-4.从您的服务器发送响应

问题

您已经有了 web 服务器,但是现在您需要能够以响应的形式从服务器发送信息。

解决办法

服务器响应是作为请求事件的一部分发出的 Node.js EventEmitter对象。在这个解决方案中,您将利用响应对象直接将内容写入请求者。首先,您想要发送一个 HTML 文档。您可以通过创建一个响应并直接发送 HTML 内容来做到这一点。

清单 4-11 。HTML 的响应.写入

/**
* Sending a response from your server
*/

var http = require('http');

var server = http.createServer(function(req, res) {

        res.setHeader('Content-Type', 'text/html');
        res.writeHead(200, 'woot');
        res.write('<!doctype html>');
        res.write('<html>');
        res.write('<head><meta charset="utf-8"></head>');
        res.write('<body>');
        res.write('<h2>Hello World</h2>');
        res.write('</body></html>');
        res.end();
});

server.listen(8080);

现在,您可以在回复中直接提供 HTML 内容。您可能需要能够发送其他类型的内容,以便为您的应用提供可靠的解决方案。在这种情况下,您选择发送一个 JavaScript Object Notation (JSON)编码的对象,以便客户机可以从您的服务器检索信息。这在实现上是相似的,只是有一些小的变化,您将在它的工作原理一节中看到细节。

清单 4-12 。A JSON 服务器负责人

var http = require('http');

var server = http.createServer(function(req, res) {

        res.setHeader('Content-Type', 'application/json');
        res.writeHead(200, 'json content');
        res.write('{ "wizard": "mithrandir" }');
        res.end();
});

server.listen(8080);

它是如何工作的

您现在通过使用serverResponse对象从您的 web 服务器提供内容。在本解决方案中使用的这个对象带有一些有价值的功能。requestListener回调函数中的第一行是response.setHeader.``setHeader函数顾名思义就是这样做的;它设置响应的标头。这些被设置在一个名称和值对中,res.setHeader(‘Name’, ‘Value’);.

在解决方案中,您设置 Content-Type 头来定义随请求一起发送的内容的类型。您还可以设置 cookies、自定义标头参数或来自服务器的请求标头附带的任何内容。

本解决方案中使用的另一种设置响应头的方法是response.writeHead方法。这种方法不会将您的头创建限制为单个名称和值对。此方法最多需要三个参数。第一个参数是必需的,它为响应设置 HTTP 状态代码。然后,您可以选择设置与状态代码描述相对应的自定义描述或原因短语。这可以是您希望的任何原因短语,与 HTTP 标准描述不同。

清单 4-13 。Node.js 中的 HTTP 原因短语覆盖

if (typeof arguments[1] == 'string') {
    reasonPhrase = arguments[1];
    headerIndex = 2;
  } else {
    reasonPhrase = STATUS_CODES[statusCode] || 'unknown';
    headerIndex = 1;
  }

第三个参数实际上是一个 header 对象,它将接受名称和值对,而不仅仅是单个的名称-值对,作为一个完整的对象。为了重构上面 JSON serverResponse的解决方案,您可以简单地调用一次response.writeHead来获得相同的结果。

清单 4-14 。在 response.writeHead 调用中组合 HTTP 状态代码、原因短语和标头

res.writeHead(200, ‘json content’, {
        ‘Content-Type’: ‘application/json’});

然后,使用response.write向客户机发送响应的主体。这个函数将接受一个表示响应体块的字符串。response.write的第二个参数是设置响应的编码,默认为 utf8。response.write不要求您已经通过前面提到的方法设置了标题。如果没有显式设置这个头,那么response.write方法将隐式定义一个状态码为 200 的头。然后,write 方法确保标头已经发送。如果标头尚未发送,则它们将与数据的初始写入一起发送到客户端。

清单 4-15 。如果头还没有发送,就和第一个块一起发送

if (!this._headerSent) {
  if (typeof data === 'string') {
    data = this._header + data;
  } else {
    this.output.unshift(this._header);
    this.outputEncodings.unshift('ascii');
  }
  this._headerSent = true;
}
return this._writeRaw(data, encoding);

现在,您已经看到并执行了从 web 服务器发送响应的方法。这些选项只是 HTTP 响应的一部分。可用物品的完整列表如表 4-1 所示。

表 4-1 。HTTP serverResponse 方法

方法 描述
response.addTrailers(标题) 向响应中添加 HTTP 尾随标头(标头,但在消息的末尾)。仅当分块编码用于响应时,才会发出尾部;如果不是(例如,如果请求是 HTTP/1.0),它们将被无声地丢弃。
response . end([数据],[编码]) 向服务器发出信号,表明所有响应标头和正文都已发送;服务器应该认为消息是完整的。必须对每个响应调用方法 response.end(),
response.getHeader(名称) 读出一个已经排队但没有发送到客户端的头。请注意,该名称不区分大小写。这只能在头被隐式刷新之前调用。
response.headersSent 布尔值(只读)。如果发送了头,则为真,否则为假。
response.removeHeader(名称) 移除排队等待隐式发送的标头。
响应.发送日期 如果为真,将自动生成日期标题,如果标题中没有日期标题,则在响应中发送。默认为真。
response.setHeader(名称,值) 为隐式标头设置单个标头值。如果该标题已经存在于待发送标题中,其值将被替换。如果需要发送多个同名的头,请在这里使用字符串数组。
response.setTimeout(毫秒,回调) 将套接字的超时值设置为毫秒。如果提供了回调,那么它将被添加为响应对象上的“超时”事件的侦听器。
response.statusCode 代码 当使用隐式标头(不显式调用 response.writeHead()时),此属性控制标头刷新时将发送到客户端的状态代码。
response.write(块,[编码]) 发送一大块响应正文。可以多次调用此方法来提供身体的连续部分。
writeContinue() 向客户端发送 HTTP/1.1 100 Continue 消息,指示应该发送请求正文。
writeHead(状态代码,[原因短语],[标题]) 向请求发送响应标头。

4-5.处理标题和状态代码

问题

在构建 Node.js web 应用时,您需要能够正确地传递和处理头信息和 HTTP 状态代码。

解决办法

在为这个解决方案创建的场景中,您可以想象这样一种情况,您需要为您的 web 应用提供特定类型的文件。当您正在构建一个希望发布到托管 web 应用商店或市场(如 Chrome 或 Firefox OS 应用)的 web 应用时,可能会出现这种情况。

在这种情况下,您可以提供一个应用清单文件。这通常是 JSON 文件的形式,它设置应用的细节,以便使它可以安装在托管平台上。这需要特定的头类型,以便主机平台将该文件识别为清单。因此,在这个解决方案中,您将操作标题来适当地表示内容类型,并为您的应用处理正确的状态代码。

清单 4-16 。处理标题和状态代码

/**
* Headers and status codes
*/
var http = require('http');
                url = require('url');

var server = http.createServer(function(req, res) {

        if (req.headers) {
                console.log('request headers', req.headers);
        }

        var parsedUrl = url.parse(req.url);
        if (parsedUrl.path === '/manifest.webapp' && req.method === ‘GET’) {
                // serving an application manifest file type
                res.writeHead(200, { 'Content-Type' : 'application/x-web-app-manifest+json' });
                res.write('{ "name" : "App" }');
                res.write( '"description": "My elevator pitch goes here",');
                res.write('"launch_path": "/",');
                res.write('"icons": {');
                res.write('"128": "/img/icon-128.png" },');
                res.write('"developer": {');
                res.write(' "name": "Your name or organization",');
                res.write(' "url": "http://your-homepage-here.org" },');
                res.write('"default_locale": "en" }');
                res.end();
        } else if (parsedUrl.path !== '/') {
                res.statusCode = 404;
                res.end(http.STATUS_CODES[res.statusCode]);
        } else {
                res.writeHead(200, { 'Content-Type': 'text/html'});
                res.end('<h2>normalContent</h2>');
        }

});

server.listen(8080);

它是如何工作的

该解决方案旨在做两件事。首先,它被设计为从应用的根提供静态 HTML,url path = '/'。第二,它被设计成服务于 webapp.manifest 文件,或者您将编写来打包您的应用以在应用市场上托管的内容。为了正确地做到这一点,您需要控制标题和状态代码。

状态代码很重要,因为它们提供了有关您对客户端的响应状态的信息。状态代码属于五个类别中的一个,这五个类别由每个以 100 开始的整数块分隔。100 范围内的状态代码是信息代码;200 范围内的代码是代表成功的代码;300 个范围代码表示重定向。对于客户端错误,错误由 400 范围内的状态代码表示,对于服务器错误,错误由 500 范围内的状态代码表示。

在此解决方案中,您的应用被设计为仅提供来自 web 应用根的内容,或者清单文件本身。服务器请求的其他路径将导致 404 状态代码。此状态代码是一个客户端错误,指示找不到路径。

清单 4-17 。设置 404 未找到状态码

if (parsedUrl.path !== '/') {
        res.statusCode = 404;
        res.end(http.STATUS_CODES[res.statusCode]);
}

这个响应是用通过response.end方法传递的数据编写的。这利用了http.STATUS_CODES对象,该对象将为传递的response.statusCode找到相应的状态代码原因描述。

您的目标 URL 都将返回 200 或“OK”状态代码。第一个是 web 应用的根。除了状态代码,您还希望将这个根目录作为 HTML 文档提供。这不仅由您提供的内容控制,也由标题控制。

当从任何类型的 web 服务器提供内容时,控制头是很重要的,因为头指示客户端如何处理内容,或者一旦内容被处理后如何处理。这方面的例子有内容类型头,指示请求如何提供内容;Cache-Control 头,它告诉客户端如何处理内容的缓存;和指示请求长度的 Content-Length。这些只是可以在 Node.js 的请求中设置的三个标准和非标准头名称。

在此解决方案中,当您发送应用清单文件时,您发送了一个自定义的非标准头:{ ' Content-Type ':' application/x-we b-app-manifest+JSON ' }。这个头表明内容属于应用清单类型,应该是一个 JSON 文件。如果您在应用的根目录下,响应会提供一个“text/html”的内容类型头,您可能会认为这是一个 html 文档。当然,您可以根据需要向这些响应添加任何额外的头,但是知道某些响应的内容类型(比如清单文件)需要精确是很重要的。

4-6.创建 HTTP 客户端

问题

您希望创建一个 Node.js 应用作为 HTTP 客户端。

解决办法

从 Node.js 应用中创建 HTTP 客户机就像创建 HTTP 服务器一样简单。在此解决方案中,您将从为客户端设置选项开始。这些选项告诉您的应用将请求发送到哪里,以及通过什么方式获取请求。您这样做是为了能够与在 4-3 节中为您的应用创建的 REST API 进行通信。这将解析一组参数,确定发送给 API 的方法和路径,然后处理http.request

清单 4-18 。HTTP 客户端

/*
* Creating an HTTP client
*/

var http = require('http'),
                args = process.argv.slice(2);

//Set defaults
var clientOptions = {
        host: 'localhost',
        // hostname:'nodejs.org',
        port: '8080',
        path: '/',
        method: 'GET'
};

args.forEach(function(arg) {
        switch(arg) {
                case 'GET':
                        clientOptions.method = 'GET';
                        break;
                case 'SUBMIT':
                case 'POST':
                        clientOptions.method = 'POST';
                        clientOptions.path = '/api';
                        break;
                case 'UPDATE':
                case 'PUT':
                        clientOptions.path = '/api';
                        clientOptions.method = 'PUT';
                        break;
                case 'REMOVE':
                case 'DELETE':
                        clientOptions.method = 'DELETE';
                        clientOptions.path = '/api';
                        break;
                default:
                        clientOptions.method = 'GET';
                        clientOptions.path = '/';
        }

        var clientReq = http.request(clientOptions, function(res) {
                console.log('status code', res.statusCode);
                switch(res.statusCode) {
                        case 200:
                                res.setEncoding('utf8');
                                res.on('data', function(data) {
                                        console.log('data', data);
                                });
                                break;
                        case 404:
                                console.log('404 error');
                                break;
                }
        });

        clientReq.on('error', function(error) {
                throw error;
        });

        clientReq.end();
});

它是如何工作的

这通过利用 Node.js HTTP 模块来实现。该模块提供了一个界面,可以方便地创建一个客户端请求,http.request。在这个解决方案中,您首先利用process.argv去除启动您的应用的任何相关命令行参数。在本例中,您只需实例化传递您希望提供的 HTTP 方法的应用,应用将遍历这些方法,为每个方法创建一个请求。

$ node 4-6-1.js GET POST PUT DELETE NOTHING

如果您的目标是在第 4-3 节中创建的服务器,您可以看到与下面类似的结果,显示您成功地访问了客户端请求的 API 端点。

status code 200
data get

status code 200
data get

status code 200
data post

status code 200
data put

status code 200
data delete

以上介绍了实现的工作原理,但现在您将看到 Node.js 如何处理一个http.requesthttp.request有两个参数,一个选项对象和一个接收响应的回调函数。

当您调用http.request时,您初始化了一个ClientRequest对象。ClientRequest对象继承自 Node.js OutgoingMessage对象。ClientRequest对象有一整套缺省值,这些缺省值是根据传递给 options 参数的内容进行处理的。当您浏览ClientResponse对象时,您将看到这些默认设置正在被配置。

表 4-2。客户端请求对象选项

[计]选项 功能
代理人 控制代理行为。当使用代理时,请求将默认为 Connection: keep-alive。
作家(author 的简写) 基本认证(即“用户:密码”)。
头球 包含请求标头的对象。
宿主 向其发出请求的服务器的域名或 IP 地址(默认为“localhost”)。
主机名 为了支持 url.parse(),主机名优于主机。
本地地址 要为网络连接绑定的本地接口。
方法 指定 HTTP 请求方法的字符串(默认为 GET)。
小路 请求路径(默认为“/”)。应包含查询字符串(如果有)。
港口 远程服务器的端口(默认为 80)。
套接字路径 Unix 域套接字(使用 host:port 或 socketPath 之一)。

只有当响应返回时,传递给http.request函数的回调函数才会从ClientRequest对象中调用。您还为 error 事件设置了一个事件侦听器,以便捕获请求过程中可能发生的任何错误。一旦返回了响应,您就可以处理该响应。在这个例子中,您检查statusCodes并相应地记录。您将在下一节看到更多关于处理响应的内容。

需要注意的是,为了让clientResponse工作,你必须调用request.end()函数。不管通过请求体发送的数据量有多少,这都是必要的,因为您必须表示请求的结束。

4-7.处理客户端响应

问题

您已经创建了一个 HTTP 客户端;您现在需要理解如何处理客户端响应。

解决办法

正确处理您在 HTTP 客户机上收到的响应非常重要。您需要响应诸如状态代码或应用所依赖的特定标题之类的东西。

对于这个解决方案,您可以想象一个场景,其中您的 HTTP 客户端需要查找由服务器设置的自定义头,x-ample,如果设置为适当的值,它将提醒客户端执行一个特殊的操作。然后,您将检查状态代码,以确保在执行您的操作之前有一个良好的响应。

清单 4-19 。处理响应

/**
* Processing client responses
*/

var http = require('http');

var clientOptions = {
        host: 'localhost',
        port: '8080',
        path: '/',
        method: 'GET'
};

var clientReq = http.request(clientOptions, function(res) {
        //Handle custom header for something special
        if (res.headers['x-ample'] === 'trigger') {
                console.log('x-ample header trigger');

                //work with status codes
                switch(res.statusCode) {
                        case 200:
                                res.setEncoding('utf8'); // unless you can read buffer chunks
                                res.on('data', function(data) {
                                        console.log('data', data);
                                });
                                break;
                        case 404:
                                console.log('404 error');
                                break;
                        default:
                                console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
                                break;
                }
        } else {
                console.log('required header not present');
        }
});

clientReq.on('error', function(error) {
        throw error;
});

clientReq.setHeader('Cache-Control', 'no-cache');
clientReq.end();

它是如何工作的

http.request函数上的回调函数是“response”事件的事件处理程序。这个事件监听器是从服务器响应接收数据的唯一方式。如果您省略了“响应”侦听器或回调,您的客户端请求将永远不会从服务器接收任何数据。

一旦您使用适当的监听器为'response'事件设置了客户端请求,您就能够从响应中获取数据。响应是一个可读的流,所以您可以通过为'data'事件添加一个侦听器,或者在流变成“readable”时调用response.read()来处理数据

在本例中,您避免直接从响应中读取数据,直到您检查了响应中的某个值。其中一个值是检查从响应发送的头。因为响应是包含 headers 对象的可读流,所以只需检查想要解析的头;将其值与应用中所需的值进行比较。

清单 4-20 。响应标题

if (res.headers['x-ample'] === 'trigger') {
        console.log('x-ample header trigger');
        /* . . . */
}

然后,您继续处理响应。在这个解决方案中,下一步是检查响应状态代码。如果状态代码不是 200 OK,您将无法从响应中读取数据。当然,如果一切正常,您将阅读响应正文。

清单 4-21 。响应状态代码

switch(res.statusCode) {
       case 200:
               res.setEncoding('utf8');
               res.on('data', function(data) {
                       console.log('data', data);
               });
               break;
       case 404:
               console.log('404 error');
               break;
       default:
               console.log(res.statusCode + ': ' + http.STATUS_CODES[res.statusCode]);
               break;
}

读取响应分两步完成。首先,为了使响应可读,将编码设置为 UTF-8。

清单 4-22 。响应默认编码

data <Buffer 67 65 74 0a>

清单 4-23 。响应 UTF-8 编码

data get

通过有策略地检查返回到 HTTP 请求回调的响应对象,可以处理特定于 Node.js 解决方案的各种参数和任务。

4-8.处理客户端请求

问题

您已经创建了一个 HTTP 客户端,并学习了如何处理来自它的响应。现在,您需要更详细地控制您的客户端请求。

解决办法

首先构建一个 HTTP GET 请求。GET 请求可以有两种形式。首先,如果您不需要控制自定义头或何时发送request.end()事件,您可以通过使用http.get() .使用 Node.js 快速实现 HTTP GET 请求

清单 4-24 。使用 http.get()

var http = require('http');

var getReq = http.get('http://localhost:8080', function(res) {
  console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});

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

或者,如果您需要能够控制您的头的某些方面,但是您仍然只需要处理一个 HTTP GET 请求,您将希望使用完整的http.request方法来代替。

清单 4-25 。HTTP GET 使用 http.request

var http = require('http');

var clientOptions = {
  host: 'localhost',
  port: '8080',
  path: '/',
  method: 'GET',
  headers: { 'Connection': 'keep-alive',
             'Content-Length': 0 }
};

var clientReq = http.request(clientOptions, function(res) {
  console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});

clientReq.on('continue', function(res) {
        console.log('continue event due to 100-continue');
});

clientReq.on('error', function(error) {
  throw error;
});

clientReq.end();

在构建 Node.js 应用时,您可能会遇到需要处理数据上传的情况。在 Node.js 中,你可以用 POST 方法处理这个http.request。这个请求然后用request.write函数写上传。

清单 4-26 。用 HTTP POST 上传

var http = require('http');

var opt = {
        host : 'localhost',
        port : 8080,
        path : '/upload',
        method : 'POST'
};

var upload = http.request(opt, function(res) {
        console.log('status code', res.statusCode, ': ', http.STATUS_CODES[res.statusCode]);
});

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

upload.write('my upload stuff');
upload.end();

它是如何工作的

通过客户机请求检索内容的第一种解决方案是使用http.get()。HTTP GET 是对http.request的抽象。事实上,http.get()函数调用默认的http.request,允许设置所有的默认选项;然后它立即调用request.end()方法并完成请求。

清单 4-27 。Node.js http 模块,获取方法

exports.get = function(options, cb) {
  var req = exports.request(options, cb);
  req.end();
  return req;
};

接下来,通过将方法选项设置为“GET”的http.request方法检索内容。这是一个标准的 GET 请求,但是您也传递了两个特定的头。值设置为“keep-alive”的连接头将告诉 Node.js 保持到服务器的连接打开,直到下一个请求。本解决方案中的另一个标题集是Content-Length标题。这个头一旦设置,将阻止 Node.js 使用默认的分块编码。在http.request选项中,有另外两个值得注意的头文件没有在这个解决方案中使用。

其中一个标题是Expect标题。设置这个头将立即发送请求头,以便考虑潜在的Expect: 100-continue头,我们将在 4-9 节处理事件时看到更多细节。

最后一个值得注意的头是当你发送一个授权头时。当配置http.request的设置时,该标题将取代利用auth选项的需要。

处理 HTTP 客户端请求的解决方案的最后一部分是演示如何处理文件上传请求。要做到这一点,必须做几件事。首先,如您所料,不要使用 HTTP GET 方法。相反,将上传的方法选项设置为 HTTP POST。然后通过http.request的 write 方法发送数据来处理上传。

现在,您已经看到了如何在 web 服务器上使用 HTTP 客户端请求来处理请求。接下来,您将看到在您的 web 服务器上发出和使用的各种事件。

4-9.响应事件

问题

您已经在 Node.js 中构建了一个 web 服务器,现在您需要处理并正确响应在您的服务器上发出或侦听的事件。

解决办法

为了恰当地描述这个解决方案,您需要理解事件的两个方面。为此,您将构建一个 HTTP web 服务器和一个 HTTP 客户端。

服务器(见清单 4-28 )是为处理不同方法的请求和事件而构建的。首先,您的 web 服务器将监听传入的请求。在出现这些请求时,您会希望用一个明文响应来欢迎请求者。

其次,您将希望通过监听连接事件来监控到该服务器的连接。这将增加与您的服务器建立的连接总数。

您希望您的服务器也能处理一些特殊事件。其中一个事件是监听发送了'Expect: 100-continue'头的传入请求的事件。这适用于希望在实际发送请求正文之前确定您的服务器是否能够接收消息的客户端连接。在这种情况下,您需要监听的事件是'checkContinue'事件。您还需要允许使用“Request: Upgrade”报头,以便通过监听服务器上的“upgrade”事件来升级请求。然后可以发送 upgrade 头,将传输升级到 TLS,或者在本例中,升级到 WebSockets。

清单 4-28 。Web 服务器事件

/**
* Responding to events
*/

var http = require('http'),
                server = http.createServer(),
                connections = 0;

// request event
server.on('request', function(req, res) {
        console.log('request');//, req);
        res.writeHead(200, { 'Content-Type': 'text/plain'});
        res.end('heyo');
});

server.on('connection', function(socket) {
        connections++;
        console.log('connection count: ', connections);
});

server.on('checkContinue', function(req, res) {
        console.log('checkContinue');
        res.writeContinue();
});

server.on('upgrade', function(req, socket, head) {
        console.log('upgrade');
        socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
               'Upgrade: WebSocket\r\n' +
               'Connection: Upgrade\r\n' +
               'Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n' +
               'Sec-WebSocket-Protocol: chat\r\n' +
               '\r\n');
        socket.pipe(socket);
});

server.listen(8080);

为了正确体验这些事件,您需要有两组连接到此服务器的客户端。您将构建的第一个客户端,清单 4-22 ,您需要在其中提供必要的事件,以便提供 Expect 头,Expect: 100-continue,并正确地响应从服务器发出的 continue 事件。

清单 4-29 。客户端事件用于处理 Expect: 100-continue

/*
* client events
*/

var http = require('http');

var clientOptions = {
        host: 'localhost',
        // hostname:'nodejs.org',
        port: '8080',
        path: '/',
        method: 'GET',
        headers: { 'Expect': '100-continue' }
};

var clientReq = http.request(clientOptions, function(res) {
        console.log('status code', res.statusCode);
        switch(res.statusCode) {
                case 200:
                        res.setEncoding('utf8'); // unless you can read buffer chunks
                        res.on('data', function(data) {
                                console.log('data', data);
                        });
                        break;
                case 404:
                        console.log('404 error');
                        break;
        }
});

clientReq.on('continue', function() {
        console.log('client continue');
});

clientReq.on('error', function(error) {
        throw error;
});

clientReq.end();

您将创建第二个客户机来演示您的 web 服务器发出和使用的事件,它将创建一个客户机来处理对 WebSocket 服务器的升级。这发生在你在清单 4-30 中创建的客户端中,它在设计上类似于清单 4-29 。但是,它处理不同的事件以提供不同的实现。

清单 4-30 。升级客户端

/*
* client events
*/

var http = require('http');

var clientOptions = {
        host: 'localhost',
        // hostname:'nodejs.org',
        port: '8080',
        path: '/',
        method: 'GET',
        headers: { 'Connection': 'Upgrade',
        'Upgrade': 'websocket',
        'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
        'Origin' :'localhost',
        'Sec-WebSocket-Protocol': 'chat',
        'Sec-WebSocket-Version': 13 }
};

var clientReq = http.request(clientOptions, function(res) {
        console.log('status code', res.statusCode);
        switch(res.statusCode) {
                case 200:
                        res.setEncoding('utf8'); // unless you can read buffer chunks
                        res.on('data', function(data) {
                                console.log('data', data);
                        });
                        break;
                case 404:
                        console.log('404 error');
                        break;
        }
});

clientReq.on('upgrade', function(res, socket, head) {
        console.log('client upgrade');
});

clientReq.on('error', function(error) {
        throw error;
});

clientReq.end();

它是如何工作的

事件是构建成功的 Node.js 应用的关键部分。就这一点而言,在 Node.js 中构建一个成功的 web 服务器必须包含对客户机和服务器之间发出的事件的正确处理。在本节的解决方案中,您创建了一个同时解决多个问题的 web 服务器。

该服务器开始监听请求事件。每次有对服务器的请求时,都会发出此请求事件。一旦接收到请求,就向请求者发送一个响应,在本例中是一个简单的问候。对请求事件的回调同时提供请求和响应对象;其实这个事件和直接给http.createServer函数添加回调是一样的。

您的下一个侦听器用于通过侦听何时发出连接事件来跟踪与服务器的连接。每次连接到服务器时都会发生这种情况。每当有一个连接时,当您递增计数器时,就会发生这种情况。connection 事件在回调函数中发送连接的 socket 对象,如果您愿意,允许您访问net.Socket

在下一个事件监听器中处理'Expect: 100-continue'头。该侦听器被绑定到“checkContinue”事件。仅当请求发送 expect 标头时,才会发出此事件。如果您没有监听此事件,服务器将自己发送适当的继续响应。

清单 4-31 。当 Expect 头存在时,Node.js 发出 checkContinue

if (req.headers.expect !== undefined &&
        (req.httpVersionMajor == 1 && req.httpVersionMinor == 1) &&
        continueExpression.test(req.headers['expect'])) {
      res._expect_continue = true;
      if (EventEmitter.listenerCount(self, 'checkContinue') > 0) {
        self.emit('checkContinue', req, res);
      } else {
        res.writeContinue();
        self.emit('request', req, res);
      }
}

如果您正在适当地处理这个事件,您需要向请求表明允许继续发送请求的主体。这是通过调用response.writeContinue()函数来完成的。这个函数向请求者写入适当的 HTTP 100 响应。

清单 4-32 。Response 继续发送 HTTP 100 继续响应

ServerResponse.prototype.writeContinue = function() {
  this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii');
  this._sent100 = true;
};

这个 continue 事件只有在你发送适当的头时才起作用,就像在来自清单 4-22 : headers: { ‘expect’ : ‘100-continue’ }的客户端请求中一样。然后监听来自客户机请求应用的 connect 事件,表示何时调用了response.writeContinue()函数并发送了 HTTP 100 响应。

最后,您的服务器被设置为处理 WebSocket 协议的升级。这个协议是通过一个握手过程启动的,这个握手过程由客户端请求和 web 服务器发出的事件处理。当客户端发送升级报头时,该过程开始:报头:{ 'Connection': 'Upgrade', 'Upgrade': 'websocket'}。除了这些头字段之外,还会发送一个 WebSocket 密钥,服务器将利用该密钥来验证请求握手是否已收到。当这个头存在时,将发出一个'upgrade’'事件,您将在您的服务器上监听这个升级事件。

服务器上的“upgrade”事件有一个回调函数,它有三个参数:请求、套接字和头。为了完成请求 WebSocket 握手,您必须发送适当的 HTTP 响应。在这种情况下,这是具有相同升级和连接头的 HTTP 101 Web Socket 协议握手。还发回了 websocket-accept 头,这是对收到来自请求的密钥的验证。一旦发送了头,您的客户机就可以接收升级事件并完成 WebSocket 升级握手。

此次升级活动还引入了。套接字流上的管道方法。流是构建许多 Node.js 应用不可或缺的一部分。这是一种以简洁和统一的方式管理流的输入和输出的方法。这可以通过获取可读的源流并将其通过管道传输到可写的目标流来实现。这导致目标流的返回。在这个升级事件回调中,你写socket.pipe(socket);。这需要您刚才调用 socket.write() 的源(或套接字)来添加 WebSocket 升级头。然后,它通过管道把它输出到代表目标流的.pipe(socket)

4-10.通过文件系统提供静态页面

问题

您正在构建一个 web 服务器。直接从 Node.js 代码提供 HTML 是不可维护的,也是不可取的。您需要能够从驻留在文件系统本身的文件中提供内容。

解决办法

要构建提供内容的 web 服务器,您需要利用 HTTP 模块和文件系统模块,它们是 Node.js 核心的一部分。您将构建您的服务器来处理服务器上的错误,然后您可以用正确的状态代码进行响应。您还将确保随您提供的文件一起发送适当的响应头。这意味着您需要注意所提供内容的 mime 类型。为此,使用一个简单的 URL 结构来了解应用中的哪些路由将从 web 服务器请求哪些类型的文件。

清单 4-33 。静态文件 Web 服务器

/**
* serving static HTML with the file system
*/

var http = require('http'),
    fs = require('fs'),
    path = require('path');

//Content types map
var contentTypes = {
    '.htm'  : 'text/html',
    '.html' : 'text/html',
    '.js'   : 'text/javascript',
    '.json' : 'application/json',
    '.css'  : 'text/css'
};

var server = http.createServer(function(req, res) {

    var fileStream = fs.createReadStream(req.url.split('/')[1]);

    fileStream.on('error', function(error) {
        if (error.code === 'ENOENT') {
            res.statusCode = 404;
            res.end(http.STATUS_CODES[404]);
        } else {
            res.statusCode = 500;
            res.end(http.STATUS_CODES[500]);
        }
    });
    //Get the extension
    var extension = path.extname(req.url);

    //read the extension against the content type map - default to plain text
    var contentType = contentTypes[extension] || 'text/plain';

    // add the content type header
    res.writeHead(200, { 'Content-Type' : contentType });

    // pipe the stream to the response stream
    fileStream.pipe(res);

});

server.listen(8080);

现在您有了一个 web 服务器,可以从文件系统中提供静态文件。为了测试这个功能,您还需要构建两个测试文件。一个是基本 HTML 文件。第二个是 JSON 文件。这个文件表示一个假想的 API 的响应,您可以构建这个 API 来从您的应用中访问它。这些文件显示在清单 4-34清单 4-35 中。

清单 4-34 。要提供的基本 HTML 文件

<!doctype html>
<html>
<head>
<title>Static HTML</title>
</head>
<body>
        <h2>Node.js Recipes</h2>
        <p> Tasty </p>
        <button id='getJSON'>Get JSON file</button>
        <script type='text/javascript'>
                // bind to click
                var btn = document.getElementById('getJSON');
                btn.addEventListener('click', getJSONContent, false);
                // Send a request to the server for the JSON file
                function getJSONContent() {
                        var xhr = new XMLHttpRequest();
                        xhr.onload = jsonRetrieved;
                        xhr.open('GET', '/4-10-1.json', true);
                        xhr.send();
                }
                // Log to the console
                function jsonRetrieved() {
                        console.log(this.responseText);
                }
        </script>
</body>
</html>

清单 4-35 。要提供的示例 JSON 文件

{
    'Test': 'if',
    'this':'sends'
}

它是如何工作的

让我们调查一下您的全功能 web 服务器是如何工作的。首先,您为这个服务器使用 HTTP 模块。您还可以用文件系统和 URL 模块来扩充这些模块。这将允许您从 web 服务器的文件系统中获取和读取文件,并且 URL 模块允许解析 URL,以便正确地路由您的内容。

现在,您通过调用http.createServer创建一个 web 服务器,并让该服务器监听您指定的端口。这个解决方案的实质在于requestListener回调。在这个回调中,您可以处理传入的请求和传出的响应。

当您收到一个请求时,您的服务器做的第一件事就是使用 fs.createReadStream 将传入的请求 URL 读入一个流。这将允许您创建适当的错误响应代码发送到客户端。在您的情况下,如果错误代码是 ENOENT(没有这样的文件或目录),您将发送 404 not found,对于其他错误,您将返回到一般的 500 服务器错误。

然后解析来自请求 URL 的扩展。这是通过使用 Node.js 路径模块完成的,该模块有一个方法“extname ”,它将返回给定路径的扩展名。然后将它用于您创建的内容类型对象,以将给定的扩展映射到您希望从服务器提供的适当内容类型。一旦将扩展映射到内容类型,就可以将内容类型头写入响应。接下来是通过管道将文件流传送到响应。

接下来,您将研究构建到 web 服务器中的模拟 JSON API。这条路线在网址/*.json上。这表示可能调用数据库来检索信息,但是在我们的例子中,它检索的是一个 JSON 文件,该文件将在头中带有“Content-Type: application/json”。

现在,您可以为 web 服务器提供任何类型的内容。您可以通过运行您的服务器,然后导航到各种 URL 来测试这一点。如果您导航到//localhost:8080/4-10-1.html,您将看到一个 html 页面。这个页面有一个按钮,您可以按下它向 JSON API 提交一个 XMLHttpRequest,将内容记录到控制台。当然,您可以直接导航到/4-10-1.json 路径,在那里您也将收到 json。测试一个 404,你可以简单地尝试卷曲http://localhost:8080/404,你会收到预期的 404:

> GET /404 HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8r zlib/1.2.5
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Date: Sat, 18 May 2013 19:17:55 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked