在 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 的重要性。

更多關于云服務器域名注冊,虛擬主機的問題,請訪問三五互聯官網:m.shinetop.cn

贊(0)
聲明:本網站發布的內容(圖片、視頻和文字)以原創、轉載和分享網絡內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。郵箱:3140448839@qq.com。本站原創內容未經允許不得轉載,或轉載時需注明出處:三五互聯知識庫 » HTTP反向代理服務器該怎么編寫

登錄

找回密碼

注冊

主站蜘蛛池模板: 色悠悠在线观看入口一区| 岛国一区二区三区高清视频| 河源市| 成人国产精品一区二区网站公司| 国产一区二区三区黄色片| 欧美午夜小视频| 色悠悠久久精品综合视频 | 小污女小欲女导航| 亚洲一区在线观看青青蜜臀| 久久午夜无码鲁丝片直播午夜精品| 国产一级av在线播放| 日韩中文字幕亚洲精品| 国产精品自拍午夜福利| 亚洲AV日韩精品久久久久| 日本高清日本在线免费| 成人午夜福利视频一区二区| 狂野欧美性猛交免费视频| 久久精品国产亚洲av高| 国产午夜三级一区二区三| 美日韩av一区二区三区| 2020年最新国产精品正在播放| 女性高爱潮视频| 色综合天天综合天天综| 亚洲永久精品一区二区三区 | 亚洲不卡一区三区三区四| 亚洲综合无码一区二区| 亚洲免费视频一区二区三区| 高级会所人妻互换94部分| 麻豆蜜桃伦理一区二区三区| 色爱综合另类图片av| 99久久er热在这里只有精品99| 青青草无码免费一二三区| 两个人的视频www免费| 国产桃色在线成免费视频| 少妇爆乳无码专区| 国产一区二区三区自拍视频| 日韩中文字幕av有码| 一本之道高清乱码少妇| 国产在线视频精品视频| ww污污污网站在线看com| 99国产午夜福利在线观看|