首頁 > web前端 > js教程 > 使用 Node.js 和 TypeScript 建立可擴充的反向代理伺服器,例如 Nginx

使用 Node.js 和 TypeScript 建立可擴充的反向代理伺服器,例如 Nginx

Susan Sarandon
發布: 2025-01-05 08:48:41
原創
617 人瀏覽過

靈感

在當今的微服務架構中,反向代理在管理傳入請求並將其路由到各種後端服務方面發揮著至關重要的作用。

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

反向代理位於應用程式的 Web 伺服器前面,攔截來自客戶端電腦的請求。這有許多好處,例如負載平衡、隱藏來源伺服器 IP 位址,從而提高安全性、快取、速率限制等。

在分散式微服務架構中,單一入口點是必要的。像 Nginx 這樣的反向代理伺服器可以在這種情況下提供協助。如果我們有多個伺服器實例在運行,管理和確保有效的請求路由就會變得很棘手。在這種情況下,像 Nginx 這樣的反向代理程式是一個完美的解決方案。我們可以將網域名稱指向 Nginx 伺服器的 IP 位址,Nginx 將根據配置將傳入請求路由到其中一個實例,同時處理每個實例處理的負載。

Nginx 怎麼這麼好?

我建議閱讀 Nginx 的這篇文章,它詳細解釋了 Nginx 如何以超強的可靠性和速度支持大規模的請求:Nginx 架構

簡單來說,Nginx有一個Master進程和一堆worker進程。它還具有快取載入器和快取管理器等輔助進程。主進程和工作進程完成所有繁重的工作。

  • 主程序:管理配置並產生子程序。
  • 快取載入器/管理器:用最少的資源處理快取載入和修剪。
  • 工作進程:管理連接、磁碟 I/O 和上游通信,非阻塞且獨立運作。

工作進程以非阻塞方式處理多個連接,從而減少上下文切換。它們是單線程的,獨立運行,並使用共享記憶體來共享快取和會話資料等資源。這種架構幫助 Nginx 減少上下文切換的次數,並比阻塞、多進程架構更快地提高速度。

從中得到啟發,我們將使用相同的主進程和工作進程概念,並將實現我們自己的事件驅動的反向代理伺服器,它將能夠處理每個工作進程數千個連接。

專案架構

我們的反向代理實作遵循以下關鍵設計原則:

  1. 設定驅動程式:所有代理行為都在YAML設定檔中定義,可以輕鬆修改路由規則。
  2. 類型安全性:TypeScript 和 Zod 架構確保配置有效性和運行時類型安全。
  3. 可擴充性:Node.js 叢集模組可以利用多個 CPU 核心以獲得更好的效能。
  4. 模組化:透過配置、伺服器邏輯和模式驗證的不同模組清晰地分離關注點。

專案結構

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration
登入後複製
登入後複製
登入後複製

關鍵零件

  1. config.yaml:定義伺服器的配置,包括連接埠、工作進程、上游伺服器、標頭和路由規則。
  2. config-schema.ts:使用 Zod 函式庫定義驗證模式,以確保設定結構正確。
  3. server-schema.ts:指定主進程和工作進程之間交換的訊息格式。
  4. config.ts:提供解析和驗證YAML設定檔的函數。
  5. server.ts:實作反向代理伺服器邏輯,包括叢集設定、HTTP 處理和請求轉送。
  6. index.ts:作為入口點,解析命令列選項並啟動伺服器。

配置管理

設定係統使用YAML。其工作原理如下:

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

登入後複製
登入後複製
登入後複製

依照規則評估傳入請求。反向代理根據路徑決定將請求轉送到哪個上游伺服器。

設定驗證(config-schema.ts)

我們使用 Zod 定義嚴格的設定驗證模式:

import { z } from "zod";

const upstreamSchema = z.object({
    id: z.string(),
    url: z.string(),
});

const headerSchema = z.object({
    key: z.string(),
    value: z.string(),
});

const ruleSchema = z.object({
    path: z.string(),
    upstreams: z.array(z.string()),
});

const serverSchema = z.object({
    listen: z.number(),
    workers: z.number().optional(),
    upstreams: z.array(upstreamSchema),
    headers: z.array(headerSchema).optional(),
    rules: z.array(ruleSchema),
});

export const rootConfigSchema = z.object({
    server: serverSchema,
});

export type ConfigSchemaType = z.infer<typeof rootConfigSchema>;
登入後複製
登入後複製

解析和驗證配置(config.ts)

config.ts 模組提供實用函數來解析和驗證設定檔。

import fs from "node:fs/promises";
import { parse } from "yaml";
import { rootConfigSchema } from "./config-schema";

export async function parseYAMLConfig(filepath: string) {
    const configFileContent = await fs.readFile(filepath, "utf8");
    const configParsed = parse(configFileContent);
    return JSON.stringify(configParsed);
}

export async function validateConfig(config: string) {
    const validatedConfig = await rootConfigSchema.parseAsync(
        JSON.parse(config)
    );
    return validatedConfig;
}
登入後複製
登入後複製

反向代理伺服器邏輯 (server.ts)

伺服器利用 Node.js 叢集模組來實現可擴充性,並利用 http 模組來處理請求。主進程將請求分發給工作進程,工作進程將請求轉發給上游伺服器。讓我們詳細探討一下 server.ts 文件,其中包含反向代理伺服器的核心邏輯。我們將分解每個元件並了解它們如何協同工作以創建可擴展的代理伺服器。

伺服器實作遵循使用 Node.js 的 cluster 模組的主從架構。這種設計使我們能夠:

  • 利用多個CPU核心
  • 同時處理請求
  • 保持高可用性
  • 隔離請求處理
  1. 主流程

    • 建立工作流程
    • 在工作人員之間分配傳入的請求
    • 管理工作池
    • 處理工作進程崩潰與重啟
  2. 工作進程:

    • 處理單一 HTTP 請求
    • 根據路由規則來匹配請求
    • 將請求轉送到上游伺服器
    • 處理回應並將其發送回客戶

主流程設定

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration
登入後複製
登入後複製
登入後複製

master程序建立一個worker池,並透過環境變數將配置傳遞給每個worker。這確保所有工作人員都可以存取相同的配置。

請求分發

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

登入後複製
登入後複製
登入後複製

master程序使用簡單的隨機分配策略將請求指派給worker。雖然不像循環演算法或最少連接演算法那麼複雜,但這種方法為大多數用例提供了良好的負載分配。請求分發邏輯:

  • 從池中隨機選出一名工人
  • 在員工之間創造平衡的工作負載
  • 處理工人可能無法使用的邊緣情況

工作行程請求邏輯

每個工作人員都會偵聽訊息,根據路由規則匹配請求,並將它們轉送到適當的上游伺服器。

import { z } from "zod";

const upstreamSchema = z.object({
    id: z.string(),
    url: z.string(),
});

const headerSchema = z.object({
    key: z.string(),
    value: z.string(),
});

const ruleSchema = z.object({
    path: z.string(),
    upstreams: z.array(z.string()),
});

const serverSchema = z.object({
    listen: z.number(),
    workers: z.number().optional(),
    upstreams: z.array(upstreamSchema),
    headers: z.array(headerSchema).optional(),
    rules: z.array(ruleSchema),
});

export const rootConfigSchema = z.object({
    server: serverSchema,
});

export type ConfigSchemaType = z.infer<typeof rootConfigSchema>;
登入後複製
登入後複製

主進程透過建構標準化訊息有效負載(包括所有必要的請求資訊)與工作進程進行通信,使用 Node.js IPC(進程間通信)並使用 Zod 模式驗證訊息結構。

工作人員負責實際的請求處理和代理。每位工人:

  • 從環境變數載入其配置
  • 使用 Zod 模式驗證設定
  • 維護自己的設定副本

工作人員透過以下方式選擇上游伺服器:

  • 從規則中找出適當的上游ID
  • 找到上游伺服器設定
  • 驗證上游伺服器是否存在

請求轉送機制:

  • 向上游伺服器建立新的 HTTP 請求
  • 串流回應資料
  • 聚合回應正文
  • 將回應傳回主程序

運行伺服器

要執行伺服器,請依照下列步驟操作:

  1. 建置專案:
import fs from "node:fs/promises";
import { parse } from "yaml";
import { rootConfigSchema } from "./config-schema";

export async function parseYAMLConfig(filepath: string) {
    const configFileContent = await fs.readFile(filepath, "utf8");
    const configParsed = parse(configFileContent);
    return JSON.stringify(configParsed);
}

export async function validateConfig(config: string) {
    const validatedConfig = await rootConfigSchema.parseAsync(
        JSON.parse(config)
    );
    return validatedConfig;
}
登入後複製
登入後複製
  1. 啟動伺服器:
if (cluster.isPrimary) {
    console.log("Master Process is up ?");
    for (let i = 0; i < workerCount; i++) {
        const w = cluster.fork({ config: JSON.stringify(config) });
        WORKER_POOL.push(w);
        console.log(Master Process: Worker Node spinned: ${i});
    }

    const server = http.createServer((req, res) => {
        const index = Math.floor(Math.random() * WORKER_POOL.length);
        const worker = WORKER_POOL.at(index);

        if (!worker) throw new Error("Worker not found.");

        const payload: WorkerMessageSchemaType = {
            requestType: "HTTP",
            headers: req.headers,
            body: null,
            url: ${req.url},
        };
        worker.send(JSON.stringify(payload));

        worker.once("message", async (workerReply: string) => {
            const reply = await workerMessageReplySchema.parseAsync(
                JSON.parse(workerReply)
            );

            if (reply.errorCode) {
                res.writeHead(parseInt(reply.errorCode));
                res.end(reply.error);
            } else {
                res.writeHead(200);
                res.end(reply.data);
            }
        });
    });

    server.listen(port, () => {
        console.log(Reverse Proxy listening on port: ${port});
    });
}
登入後複製
  1. 開發模式:
const server = http.createServer(function (req, res) {
    const index = Math.floor(Math.random() * WORKER_POOL.length);
    const worker = WORKER_POOL.at(index);

    const payload: WorkerMessageSchemaType = {
        requestType: "HTTP",
        headers: req.headers,
        body: null,
        url: ${req.url},
    };
    worker.send(JSON.stringify(payload));
});
登入後複製

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

在上面的截圖中,我們可以看到有 1 個主節點和 2 個工作進程正在運行。我們的反向代理伺服器正在偵聽連接埠 8080。
在 config.yaml 檔案中,我們描述了兩個上游伺服器,分別是:jsonplaceholder 和 dummy。如果我們希望所有到達我們伺服器的請求都路由到 jsonplaceholder,我們將規則設定為:

├── config.yaml           # Server configuration
├── src/
│   ├── config-schema.ts  # Configuration validation schemas
│   ├── config.ts         # Configuration parsing logic
│   ├── index.ts         # Application entry point
│   ├── server-schema.ts # Server message schemas
│   └── server.ts        # Core server implementation
└── tsconfig.json        # TypeScript configuration
登入後複製
登入後複製
登入後複製

類似地,如果我們希望對 /test 端點的請求應該路由到我們的虛擬上游伺服器,我們將規則設定為:

server:
    listen: 8080          # Port the server listens on.
    workers: 2            # Number of worker processes to handle requests.
    upstreams:            # Define upstream servers (backend targets).
        - id: jsonplaceholder
          url: jsonplaceholder.typicode.com
        - id: dummy
          url: dummyjson.com
    headers:              # Custom headers added to proxied requests.
        - key: x-forward-for
          value: $ip      # Adds the client IP to the forwarded request.
        - key: Authorization
          value: Bearer xyz  # Adds an authorization token to requests.
    rules:                # Define routing rules for incoming requests.
        - path: /test
          upstreams:
              - dummy     # Routes requests to "/test" to the "dummy" upstream.
        - path: /
          upstreams:
              - jsonplaceholder  # Routes all other requests to "jsonplaceholder".

登入後複製
登入後複製
登入後複製

讓我們來測試一下!

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

哇,太酷了!我們正在導航到 localhost:8080,但作為回應,我們可以看到我們收到了 jsonplaceholder.typicode.com 的主頁。最終用戶甚至不知道我們正在看到來自單獨伺服器的回應。這就是反向代理伺服器很重要的原因。如果我們有多個伺服器運行相同的程式碼,並且不想將所有連接埠公開給最終用戶,請使用反向代理作為抽象層。用戶將訪問反向代理伺服器,這是一個非常強大且快速的伺服器,它將確定將請求路由到哪個伺服器。

現在讓我們點擊 localhost:8080/todos 看看會發生什麼。

Building a Scalable Reverse Proxy Server like Nginx with Node.js and TypeScript

我們的請求再次反向代理到 jsonplaceholder 伺服器,並從解析的 URL 收到 JSON 回應:jsonplaceholder.typicode.com/todos。

通訊流程

讓我們可視化完整的請求流程:

客戶端發送請求→主程序
主流程→選定的工人
Worker → 上游伺服器
上游伺服器 → Worker
工人 → 主進程
主流程 → 客戶端

性能考慮因素

多進程架構提供了多種效能優勢:

  1. CPU 使用率:工作進程可以在不同的 CPU 核心上運行,並利用可用的硬體資源。
  2. 進程隔離:一個工作進程的崩潰不會影響其他工作進程,從而提高了可靠性。
  3. 負載分佈:請求的隨機分佈有助於防止任何單一工作人員不堪重負。

未來的改進

雖然功能正常,但目前的實作可以透過以下方式增強:

  1. 更好的負載平衡:實作更複雜的演算法,如循環或最少連接。
  2. 健康檢查:為上游伺服器增加定期健康檢查。
  3. 快取:實現回應快取以減少上游伺服器負載。
  4. 指標:增加普羅米修斯風格的指標進行監控。
  5. WebSocket 支援:擴充代理程式以處理 WebSocket 連線。
  6. HTTPS 支援:新增 SSL/TLS 終止功能。

總結

從頭開始建立反向代理伺服器一開始可能看起來很嚇人,但正如我們所探索的,這是一次有益的體驗。透過結合 Node.js 叢集、TypeScript 和基於 YAML 的設定管理,我們創建了一個受 Nginx 啟發的可擴展且高效的系統。

此實作仍有增強的空間 - 更好的負載平衡、快取或 WebSocket 支援只是一些需要探索的想法。但目前的設計為進一步實驗和擴展奠定了堅實的基礎。如果您已經按照要求進行操作,那麼您現在就可以更深入地研究反向代理,甚至可以開始建立適合您需求的自訂解決方案。

如果您想聯絡或查看我的更多作品,請查看我的 GitHub、LinkedIn。
該項目的存儲庫可以在這裡找到。

我很想聽聽您的想法、回饋或改進想法。感謝您的閱讀,祝您編碼愉快! ?

以上是使用 Node.js 和 TypeScript 建立可擴充的反向代理伺服器,例如 Nginx的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板