如果你经常使用 Node.js 编写 Web 服务端程序,一定对使用 Nginx 作为 反向代理 服务并不陌生。在生产环境中,我们往往需要将程序部署到内网多台服务器上,在一台多核服务器上,为了充分利用所有 CPU 资源,也需要启动多个服务进程,它们分别监听不同的端口。然后使用 Nginx 作为反向代理服务器,接收来自用户浏览器的请求并转发到后端的多台 Web 服务器上。大概工作流程如下图:
在 Node.js 上实现一个简单的 HTTP 代理程序还是非常简单的,本文章的例子的核心代码只有 60 多行,只要理解 内置 http 模块 的基本用法即可,具体请看下文。 接口设计与相关技术 使用 http.createServer() 创建的 HTTP 服务器,处理请求的函数格式一般为 function (req, res) {} (下文简称为 requestHandler ),其接收两个参数,分别为 http.IncomingMessage 和 http.ServerResponse 对象,我们可以通过这两个对象来取得请求的所有信息并对它进行响应。 主流的 Node.js Web 框架的中间件(比如 connect )一般都有两种形式: 中间件不需要任何初始化参数,则其导出结果为一个 requestHandler 中间件需要初始化参数,则其导出结果为中间件的初始化函数,执行该初始化函数时,传入一个 options 对象,执行后返回一个 requestHandler 为了使代码更规范,在本文例子中,我们将反向代理程序设计成一个中间件的格式,并使用以上第二种接口形式: // 生成中间件 const handler = reverseProxy({ // 初始化参数,用于设置目标服务器列表 servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"] }); // 可以直接在 http 模块中使用 const server = http.createServer(handler); // 作为中间件在 connect 模块中使用 app.use(handler); 说明: 上面的代码中, reverseProxy 是反向代理服务器中间件的初始化函数,它接受一个对象参数, servers 是后端服务器地址列表,每个地址为 IP 地址:端口 这样的格式 执行 reverseProxy() 后返回一个 function (req, res) {} 这样的函数,用于处理 HTTP 请求,可作为 http.createServer() 和 connect 中间件的 app.use() 的处理函数 当接收到客户端请求时,按顺序循环从 servers 数组中取出一个服务器地址,将请求代理到这个地址的服务器上 服务器在接收到 HTTP 请求后,首先需要发起一个新的 HTTP 请求到要代理的目标服务器,可以使用 http.request() 来发送请求: const req = http.request( { hostname: "目标服务器地址", port: "80", path: "请求路径", headers: { "x-y-z": "请求头" } }, function(res) { // res 为响应对象 console.log(res.statusCode); } ); // 如果有请求体需要发送,使用 write() 和 end() req.end(); 要将客户端的请求体( Body 部分,在 POST 、 PUT 这些请求时会有请求体)转发到另一个服务器上,可以使用 Stream 对象的 pipe() 方法,比如: // req 和 res 为客户端的请求和响应对象 // req2 和 res2 为服务器发起的代理请求和响应对象 // 将 req 收到的数据转发到 req2 req.pipe(req2); // 将 res2 收到的数据转发到 res res2.pipe(res); 说明: req 对象是一个 Readable Stream (可读流),通过 data 事件来接收数据,当收到 end事件时表示数据接收完毕 res 对象是一个 Writable Stream (可写流),通过 write() 方法来输出数据, end() 方法来结束输出 为了简化从 Readable Stream 监听 data 事件来获取数据并使用 Writable Stream 的 write() 方法来输出,可以使用 Readable Stream 的 pipe() 方法 以上只是提到了实现 HTTP 代理需要的关键技术,相关接口的详细文档可以参考这里: https://nodejs.org/api/http.html#http_http_request_options_callback 当然为了实现一个接口友好的程序,往往还需要很多 额外 的工作,具体请看下文。 简单版本 以下是实现一个简单 HTTP 反向代理服务器的各个文件和代码(没有任何第三方库依赖), 为了使代码更简洁,使用了一些最新的 ES 语法特性,需要使用 Node v8.x 最新版本来运行 : 文件 proxy.js : const http = require("http"); const assert = require("assert"); const log = require("./log"); /** 反向代理中间件 */ module.exports = function reverseProxy(options) { assert(Array.isArray(options.servers), "options.servers 必须是数组"); assert(options.servers.length > 0, "options.servers 的长度必须大于 0"); // 解析服务器地址,生成 hostname 和 port const servers = options.servers.map(str => { const s = str.split(":"); return { hostname: s[0], port: s[1] || "80" }; }); // 获取一个后端服务器,顺序循环 let ti = 0; function getTarget() { const t = servers[ti]; ti++; if (ti >= servers.length) { ti = 0; } return t; } // 生成监听 error 事件函数,出错时响应 500 function bindError(req, res, id) { return function(err) { const msg = String(err.stack || err); log("[%s] 发生错误: %s", id, msg); if (!res.headersSent) { res.writeHead(500, { "content-type": "text/plain" }); } res.end(msg); }; } return function proxy(req, res) { // 生成代理请求信息 const target = getTarget(); const info = { ...target, method: req.method, path: req.url, headers: req.headers }; const id = `${req.method} ${req.url} => ${target.hostname}:${target.port}`; log("[%s] 代理请求", id); // 发送代理请求 const req2 = http.request(info, res2 => { res2.on("error", bindError(req, res, id)); log("[%s] 响应: %s", id, res2.statusCode); res.writeHead(res2.statusCode, res2.headers); res2.pipe(res); }); req.pipe(req2); req2.on("error", bindError(req, res, id)); }; }; 文件 log.js : const util = require("util"); /** 打印日志 */ module.exports = function log(...args) { const time = new Date().toLocaleString(); console.log(time, util.format(...args)); }; 说明: (责任编辑:admin) |