使用 Node.js 和 TypeScript 构建可扩展的反向代理服务器,例如 Nginx
灵感
在当今的微服务架构中,反向代理在管理传入请求并将其路由到各种后端服务方面发挥着至关重要的作用。
反向代理位于应用程序的 Web 服务器前面,拦截来自客户端计算机的请求。这有很多好处,例如负载平衡、隐藏源服务器 IP 地址,从而提高安全性、缓存、速率限制等。
在分布式微服务架构中,单个入口点是必要的。像 Nginx 这样的反向代理服务器可以在这种情况下提供帮助。如果我们有多个服务器实例在运行,管理和确保有效的请求路由就会变得很棘手。在这种情况下,像 Nginx 这样的反向代理是一个完美的解决方案。我们可以将域名指向 Nginx 服务器的 IP 地址,Nginx 将根据配置将传入请求路由到其中一个实例,同时处理每个实例处理的负载。
Nginx 怎么这么好呢?
我建议阅读 Nginx 的这篇文章,它详细解释了 Nginx 如何以超强的可靠性和速度支持大规模的请求:Nginx 架构
简单来说,Nginx有一个Master进程和一堆worker进程。它还具有缓存加载器和缓存管理器等辅助进程。主进程和工作进程完成所有繁重的工作。
- 主进程:管理配置并生成子进程。
- 缓存加载器/管理器:用最少的资源处理缓存加载和修剪。
- 工作进程:管理连接、磁盘 I/O 和上游通信,非阻塞且独立运行。
工作进程以非阻塞方式处理多个连接,从而减少上下文切换。它们是单线程的,独立运行,并使用共享内存来共享缓存和会话数据等资源。这种架构帮助 Nginx 减少上下文切换的次数,并比阻塞、多进程架构更快地提高速度。
从中得到启发,我们将使用相同的主进程和工作进程概念,并将实现我们自己的事件驱动的反向代理服务器,它将能够处理每个工作进程数千个连接。
项目架构
我们的反向代理实现遵循以下关键设计原则:
- 配置驱动:所有代理行为都在YAML配置文件中定义,可以轻松修改路由规则。
- 类型安全:TypeScript 和 Zod 架构确保配置有效性和运行时类型安全。
- 可扩展性:Node.js 集群模块可以利用多个 CPU 核心以获得更好的性能。
- 模块化:通过配置、服务器逻辑和模式验证的不同模块清晰地分离关注点。
项目结构
├── 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
关键部件
- config.yaml:定义服务器的配置,包括端口、工作进程、上游服务器、标头和路由规则。
- config-schema.ts:使用 Zod 库定义验证模式,以确保配置结构正确。
- server-schema.ts:指定主进程和工作进程之间交换的消息格式。
- config.ts:提供解析和验证YAML配置文件的函数。
- server.ts:实现反向代理服务器逻辑,包括集群设置、HTTP 处理和请求转发。
- 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核心
- 同时处理请求
- 保持高可用性
- 隔离请求处理
-
主流程:
- 创建工作进程
- 在工作人员之间分配传入的请求
- 管理工作池
- 处理工作进程崩溃和重启
-
工作进程:
- 处理单个 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 请求
- 流式传输响应数据
- 聚合响应正文
- 将响应发送回主进程
运行服务器
要运行服务器,请按照以下步骤操作:
- 构建项目:
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; }
- 启动服务器:
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}); }); }
- 开发模式:
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)); });
在上面的截图中,我们可以看到有 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".
让我们测试一下!
哇,太酷了!我们正在导航到 localhost:8080,但作为响应,我们可以看到我们收到了 jsonplaceholder.typicode.com 的主页。最终用户甚至不知道我们正在看到来自单独服务器的响应。这就是反向代理服务器很重要的原因。如果我们有多个服务器运行相同的代码,并且不想将所有端口公开给最终用户,请使用反向代理作为抽象层。用户将访问反向代理服务器,这是一个非常强大且快速的服务器,它将确定将请求路由到哪个服务器。
现在让我们点击 localhost:8080/todos 看看会发生什么。
我们的请求再次反向代理到 jsonplaceholder 服务器,并从解析的 URL 收到 JSON 响应:jsonplaceholder.typicode.com/todos。
通讯流程
让我们可视化完整的请求流程:
客户端发送请求→主进程
主流程→选定的工人
Worker → 上游服务器
上游服务器 → Worker
工人 → 主进程
主流程 → 客户端
性能考虑因素
多进程架构提供了多种性能优势:
- CPU 利用率:工作进程可以在不同的 CPU 内核上运行,利用可用的硬件资源。
- 进程隔离:一个工作进程的崩溃不会影响其他工作进程,从而提高了可靠性。
- 负载分布:请求的随机分布有助于防止任何单个工作人员不堪重负。
未来的改进
虽然功能正常,但当前的实现可以通过以下方式增强:
- 更好的负载平衡:实现更复杂的算法,如循环或最少连接。
- 健康检查:为上游服务器添加定期健康检查。
- 缓存:实现响应缓存以减少上游服务器负载。
- 指标:添加普罗米修斯风格的指标进行监控。
- WebSocket 支持:扩展代理以处理 WebSocket 连接。
- HTTPS 支持:添加 SSL/TLS 终止功能。
总结
从头开始构建反向代理服务器一开始可能看起来很吓人,但正如我们所探索的,这是一次有益的体验。通过结合 Node.js 集群、TypeScript 和基于 YAML 的配置管理,我们创建了一个受 Nginx 启发的可扩展且高效的系统。
此实现仍有增强的空间 - 更好的负载平衡、缓存或 WebSocket 支持只是一些需要探索的想法。但当前的设计为进一步实验和扩展奠定了坚实的基础。如果您已经按照要求进行操作,那么您现在就可以更深入地研究反向代理,甚至可以开始构建适合您需求的自定义解决方案。
如果您想联系或查看我的更多作品,请查看我的 GitHub、LinkedIn。
该项目的存储库可以在这里找到。
我很想听听您的想法、反馈或改进想法。感谢您的阅读,祝您编码愉快! ?
以上是使用 Node.js 和 TypeScript 构建可扩展的反向代理服务器,例如 Nginx的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

本文讨论了在浏览器中优化JavaScript性能的策略,重点是减少执行时间并最大程度地减少对页面负载速度的影响。

本文讨论了使用浏览器开发人员工具的有效JavaScript调试,专注于设置断点,使用控制台和分析性能。

本文说明了如何使用源地图通过将其映射回原始代码来调试JAVASCRIPT。它讨论了启用源地图,设置断点以及使用Chrome DevTools和WebPack之类的工具。

本文探讨了Java收藏框架的有效使用。 它强调根据数据结构,性能需求和线程安全选择适当的收集(列表,设置,地图,队列)。 通过高效优化收集用法

掌握了入门级TypeScript教程后,您应该能够在支持TypeScript的IDE中编写自己的代码,并将其编译成JavaScript。本教程将深入探讨TypeScript中各种数据类型。 JavaScript拥有七种数据类型:Null、Undefined、Boolean、Number、String、Symbol(ES6引入)和Object。TypeScript在此基础上定义了更多类型,本教程将详细介绍所有这些类型。 Null数据类型 与JavaScript一样,TypeScript中的null

本教程将介绍如何使用 Chart.js 创建饼图、环形图和气泡图。此前,我们已学习了 Chart.js 的四种图表类型:折线图和条形图(教程二),以及雷达图和极地区域图(教程三)。 创建饼图和环形图 饼图和环形图非常适合展示某个整体被划分为不同部分的比例。例如,可以使用饼图展示野生动物园中雄狮、雌狮和幼狮的百分比,或不同候选人在选举中获得的投票百分比。 饼图仅适用于比较单个参数或数据集。需要注意的是,饼图无法绘制值为零的实体,因为饼图中扇形的角度取决于数据点的数值大小。这意味着任何占比为零的实体
