烟台SEO,seo+互联网思维,移动互联网思维,SEO思维爱好者。泥鳅SEO博客专注网站建设,网站优化,关注烟台SEO个人博客为你解答更多,网络营销推广问题。
  • 首页
  • 如何编写一个 HTTP 反向代理服务器

作者:泥鳅2017-12-13 9:25分类: 标签: 手机版

  如果你经常使用 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));

  };

  说明:

  log.js 文件实现了一个用于打印日志的函数 log() ,它可以支持 console.log() 一样的用法,并且自动在输出前面加上当前的日期和时间,方便我们浏览日志

  reverseProxy() 函数入口使用 assert 模块来进行基本的参数检查,如果参数格式不符合要求即抛出异常,保证可以第一时间让开发者知道,而不是在运行期间发生各种 不可预测 的错误

  getTarget() 函数用于循环返回一个目标服务器地址

  bindError() 函数用于监听 error 事件,避免整个程序因为没有捕捉网络异常而崩溃,同时可以统一返回出错信息给客户端

  为了测试我们的代码运行的效果,我编写了一个简单的程序,文件 server.js :

  const http = require("http");

  const log = require("./log");

  const reverseProxy = require("./proxy");

  // 创建反向代理服务器

  function startProxyServer(port) {

  return new Promise((resolve, reject) => {

  const server = http.createServer(

  reverseProxy({

  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"]

  })

  );

  server.listen(port, () => {

  log("反向代理服务器已启动: %s", port);

  resolve(server);

  });

  server.on("error", reject);

  });

  }

  // 创建演示服务器

  function startExampleServer(port) {

  return new Promise((resolve, reject) => {

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

  const chunks = [];

  req.on("data", chunk => chunks.push(chunk));

  req.on("end", () => {

  const buf = Buffer.concat(chunks);

  res.end(`${port}: ${req.method} ${req.url} ${buf.toString()}`.trim());

  });

  });

  server.listen(port, () => {

  log("服务器已启动: %s", port);

  resolve(server);

  });

  server.on("error", reject);

  });

  }

  (async function() {

  await startExampleServer(3001);

  await startExampleServer(3002);

  await startExampleServer(3003);

  await startProxyServer(3000);

  })();

  执行以下命令启动:

  node server.js

  然后可以通过 curl 命令来查看返回的结果:

  curl http://127.0.0.1:3000/hello/world

  连续执行多次该命令,如无意外输出结果应该是这样的(输出内容端口部分按照顺序循环):

  3001: GET /hello/world

  3002: GET /hello/world

  3003: GET /hello/world

  3001: GET /hello/world

  3002: GET /hello/world

  3003: GET /hello/world

  注意:如果使用浏览器来打开该网址,看到的结果顺序可能是不一样的,因为浏览器会自动尝试请求 /favicon ,这样刷新一次页面实际上是发送了两次请求。

  单元测试

  上文我们已经完成了一个基本的 HTTP 反向代理程序,也通过简单的方法验证了它是能正常工作的。但是,我们并没有足够的测试,比如只验证了 GET 请求,并没有验证 POST 请求或者其他的请求方法。而且通过手工去做更多的测试也比较麻烦,很容易遗漏。所以,接下来我们要给它加上自动化的单元测试。

  在本文中我们选用在 Node.js 界应用广泛的 mocha 作为单元测试框架,搭配使用 supertest 来进行 HTTP 接口请求的测试。由于 supertest 已经自带了一些基本的断言方法,我们暂时不需要chai 或者 should 这样的第三方断言库。

  首先执行 npm init 初始化一个 package.json 文件,并执行以下命令安装 mocha 和 supertest :

  npm install mocha supertest --save-dev

  然后新建文件 test.js :

  const http = require("http");

  const log = require("./log");

  const reverseProxy = require("./proxy");

  const { expect } = require("chai");

  const request = require("supertest");

  // 创建反向代理服务器

  function startProxyServer() {

  return new Promise((resolve, reject) => {

  const server = http.createServer(

  reverseProxy({

  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"]

  })

  );

  log("反向代理服务器已启动");

  resolve(server);

  });

  }

  // 创建演示服务器

  function startExampleServer(port) {

  return new Promise((resolve, reject) => {

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

  const chunks = [];

  req.on("data", chunk => chunks.push(chunk));

  req.on("end", () => {

  const buf = Buffer.concat(chunks);

  res.end(`${port}: ${req.method} ${req.url} ${buf.toString()}`.trim());

  });

  });

  server.listen(port, () => {

  log("服务器已启动: %s", port);

  resolve(server);

  });

  server.on("error", reject);

  });

  }

  describe("测试反向代理", function() {

  let server;

  let exampleServers = [];

  // 测试开始前先启动服务器

  before(async function() {

  exampleServers.push(await startExampleServer(3001));

  exampleServers.push(await startExampleServer(3002));

  exampleServers.push(await startExampleServer(3003));

  server = await startProxyServer();

  });

  // 测试结束后关闭服务器

  after(async function() {

  for (const server of exampleServers) {

  server.close();

  }

  });

  it("顺序循环返回目标地址", async function() {

  await request(server)

  .get("/hello")

  .expect(200)

  .expect(`3001: GET /hello`);

  await request(server)

  .get("/hello")

  .expect(200)

  .expect(`3002: GET /hello`);

  await request(server)

  .get("/hello")

  .expect(200)

  .expect(`3003: GET /hello`);

  await request(server)

  .get("/hello")

  .expect(200)

  .expect(`3001: GET /hello`);

  });

  it("支持 POST 请求", async function() {

  await request(server)

  .post("/xyz")

  .send({

  a: 123,

  b: 456

  })

  .expect(200)

  .expect(`3002: POST /xyz {"a":123,"b":456}`);

  });

  });

  说明:

  在单元测试开始前,需要通过 before() 来注册回调函数,以便在开始执行测试用例时先把服务器启动起来

  同理,通过 after() 注册回调函数,以便在执行完所有测试用例后把服务器关闭以释放资源(否则 mocha 进程不会退出)

  使用 supertest 发送请求时,代理服务器不需要监听端口,只需要将 server 实例作为调用参数即可

  接着修改 package.json 文件的 scripts 部分:

  {

  "scripts": {

  "test": "mocha test.js"

  }

  }

  执行以下命令开始测试:

  npm test

  如果一切正常,我们应该会看到这样的输出结果,其中 passing 这样的提示表示我们的测试完全通过了:

  测试反向代理

  2017-12-12 18:28:15 服务器已启动: 3001

  2017-12-12 18:28:15 服务器已启动: 3002

  2017-12-12 18:28:15 服务器已启动: 3003

  2017-12-12 18:28:15 反向代理服务器已启动

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 代理请求

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 响应: 200

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3002] 代理请求

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3002] 响应: 200

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3003] 代理请求

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3003] 响应: 200

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 代理请求

  2017-12-12 18:28:15 [GET /hello => 127.0.0.1:3001] 响应: 200

  ✓ 顺序循环返回目标地址

  2017-12-12 18:28:15 [POST /xyz => 127.0.0.1:3002] 代理请求

  2017-12-12 18:28:15 [POST /xyz => 127.0.0.1:3002] 响应: 200

  ✓ 支持 POST 请求

  2 passing (45ms)

  当然以上的测试代码还远远不够,剩下的就交给读者们来实现了。

  接口改进

  如果要设计成一个比较通用的反向代理中间件,我们还可以通过提供一个生成 http.ClientRequest 的函数来实现在代理时动态修改请求:

  reverseProxy({

  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"],

  request: function(req, info) {

  // info 是默认生成的 request options 对象

  // 我们可以动态增加请求头,比如当前请求时间戳

  info.headers["X-Request-Timestamp"] = Date.now();

  // 返回 http.ClientRequest 对象

  return http.request(info);

  }

  });

  然后在原来的 http.request(info, (res2) => {}) 部分可以改为监听 response 事件:

  const req2 = http.request(options.request(info));

  req2.on("response", res2 => {});

  同理,我们也可以通过提供一个函数来修改部分的响应内容:

  reverseProxy({

  servers: ["127.0.0.1:3001", "127.0.0.1:3002", "127.0.0.1:3003"],

  response: function(res, info) {

  // info 是发送代理请求时所用的 request options 对象

  // 我们可以动态设置一些响应头,比如实际代理的模板服务器地址

  res.setHeader("X-Backend-Server", `${info.hostname}:${info.port}`);

  }

  });

  此处只发散一下思路,具体实现方法和代码就不再赘述了。

  总结

  本文主要介绍了如何使用内置的 http 模块来创建一个 HTTP 服务器,以及发起一个 HTTP 请求,并简单介绍了如何对 HTTP 接口进行测试。在实现 HTTP 请求代理的过程中,主要是运用了Stream 对象的 pipe() 方法,关键部分代码只有区区几行。Node.js 中的很多程序都运用了 Stream 这样的思想,将数据当做一个流,使用 pipe 将一个流转换成另一个流,可以看出 Stream在 Node.js 的重要性。

项目推荐

小程序生成平台 模块自由组合

微信小程序开发 海量模板无需代码

epower企服引擎 助力快速创业

一行代码让你拥有智能客服

温馨提示如有转载或引用以上内容之必要,敬请将本文链接作为出处标注,谢谢合作!

已有 0/82 人参与

发表评论:

欢迎使用手机扫描访问本站,还可以关注微信哦~