最专业的八方代购网站源码!

资讯热点
如何编写HTTP反向代理服务器

发布时间:2022-2-14 分类: 行业资讯

如果您经常使用Node.js编写Web服务器程序,则必须熟悉使用Nginx作为反向代理服务。在生产环境中,我们经常需要将程序部署到Intranet上的多个服务器。在多核服务器上,为了充分利用所有CPU资源,我们需要启动多个服务进程,这些进程会侦听不同的端口。然后使用Nginx作为反向代理服务器来接收来自用户浏览器的请求,并将它们转发到后端的多个Web服务器。工作流程可能如下:

在Node.js上实现一个简单的HTTP代理仍然非常简单。本文中示例的核心代码只有60行。只需了解内置http模块的基本用法即可。请参阅下文了解详情。

接口设计及相关技术

使用http.createServer()创建的HTTP服务器,处理请求的函数格式一般是函数(req,res){}(以下简称requestHandler),它接收两个参数,即http.IncomingMessage和http.ServerResponse对象。我们可以使用这两个对象来获取所请求的所有信息并对其进行响应。

主流Node.js Web框架(如connect)的中间件通常有两种形式:

中间件不需要任何初始化参数,其导出结果是requestHandler

中间件需要初始化参数,导出的结果是中间件的初始化函数。执行初始化函数时,将传递options对象,并在执行后返回requestHandler。

为了使代码更加标准化,在本例中,我们将反向代理设计为中间件格式,并使用上面的第二个接口形式:

//生成中间件

Const处理程序=reverseProxy({

//用于设置目标服务器列表的初始化参数

服务器: ['127.0.0.1: 3001','127.0.0.1: 3002','127.0.0.1: 3003']

});

//可以直接在http模块中使用

Const server=http.createServer(handler);

//用作连接模块中的中间件

App.use(处理程序);

说明:

在上面的代码中,reverseProxy是反向代理服务器中间件的初始化函数,它接受一个对象参数,服务器是后端服务器地址列表,每个地址的格式为IP地址:端口

执行reverseProxy()后,它返回一个函数,如函数(req,res){},用于处理HTTP请求,可以用作http.createServer()的app.use()处理程序和连接中间件

收到客户端请求后,将在顺序循环中从服务器阵列获取服务器地址,并将请求委派给该地址的服务器。

在收到HTTP请求后,服务器首先需要向目标服务器发起新的HTTP请求以进行代理。您可以使用http.request()发送请求:

Const req=http.request(

{

主机名:'目标服务器地址',

端口:'80',

路径:'请求路径',

标题: {

'x-y-z':'请求标头'

}

},

功能(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对象是一个可读流,它通过数据事件接收数据,并指示在收到结束事件时已收到数据

res对象是可写流,它通过write()方法输出数据,end()方法结束输出

为了简化从可读流监听器数据中获取数据并使用Writable Stream的write()方法输出数据,您可以使用可读流的pipe()方法

以上仅提到了实现HTTP代理所需的关键技术。有关相关接口的详细文档,请访问:https://nodejs.org/api/http.htmlhttp_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){

断言(Array.isArray(options.servers),'options.servers必须是数组');

断言(options.servers.length> 0,'options.servers必须长于0');

//解析服务器地址,生成主机名和端口

Const服务器=options.servers.map(str=> {

Const s=str.split(':');

返回{hostname: s [0],端口: s [1] || '80'};

});

//获取后端服务器,顺序循环

设ti=0;

函数getTarget(){

Const t=servers [ti];

钛++;

如果(ti>=servers.length){

Ti=0;

}

返回t;

}

//生成一个监听器错误事件函数,当出现错误时响应500

函数bindError(req,res,id){

返回函数(错误){

Const msg=String(err.stack || err);

日志('[%s]有错误:%s',id,msg);

如果(!res.headersSent){

res.writeHead(500,{'content-type':'text/plain'});

}

Res.end(MSG);

};

}

返回函数代理(req,res){

//生成代理请求信息

Const target=getTarget();

Const info={

...目标,

方法: req.method,

路径: req.url,

标题: req.headers

};

Const id=`$ {req.method} $ {req.url}=> $ {target.hostname}: $ {target.port}`;

记录('[%s]代理请求',id);

//发送代理请求

Const req2=http.request(info,res2=> {

Res2.on('error',bindError(req,res,id));

记录('[%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模块进行基本参数检查。如果参数格式不符合要求,则抛出异常,确保可以在第一时间知道开发人员,而不是在运行期间出现各种不可预测的错误。 p>

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

bindError()函数用于侦听错误事件,防止整个程序崩溃,因为它没有捕获网络异常,并以统一的方式向客户端返回错误消息

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

Const http=require('http');

Const log=require('./log');

Const reverseProxy=require('./proxy');

//创建反向代理服务器

函数startProxyServer(port){

返回新的承诺((解决,拒绝)=> {

Const server=http.createServer(

反向代理({

服务器: ['127.0.0.1: 3001','127.0.0.1: 3002','127.0.0.1: 3003']

})

);

Server.listen(port,()=> {

日志('反向代理服务器已启动:%s',端口);

解析(服务器);

});

Server.on('错误',拒绝);

});

}

//创建演示服务器

函数startExampleServer(port){

返回新的承诺((解决,拒绝)=> {

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

Const chunks=[];

Req.on('data',chunk=> chunks.push(chunk));

Req.on('end',()=> {

Const buf=Buffer.concat(chunk);

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

});

});

Server.listen(port,()=> {

记录('服务器已启动:%s',端口);

解析(服务器);

});

Server.on('错误',拒绝);

});

}

(async function(){

等待startExampleServer(3001);

等待startExampleServer(3002);

等待startExampleServer(3003);

等待startProxyServer(3000);

})();

首先执行以下命令:

节点server.js

然后,您可以使用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

注意:如果您使用浏览器打开URL,您看到的结果的顺序可能会有所不同,因为浏览器会自动尝试请求/favicon,因此刷新页面实际上会发送两次请求。

单元测试

上面我们已经完成了一个基本的HTTP反向代理,并通过一个简单的方法验证它工作正常。但是,我们没有足够的测试,例如仅验证GET请求,而不验证POST请求或其他请求方法。手动进行更多测试很麻烦,很容易错过。因此,接下来我们必须为其添加自动化单元测试。

在本文中,我们使用在Node.js世界中广泛使用的mocha作为单元测试框架,并使用supertest来测试HTTP接口请求。由于supertest已经提供了一些基本的断言方法,因此我们不需要像chai或者应该这样的第三方断言库。

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

Npm安装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');

//创建反向代理服务器

函数startProxyServer(){

返回新的承诺((解决,拒绝)=> {

Const server=http.createServer(

反向代理({

服务器: ['127.0.0.1: 3001','127.0.0.1: 3002','127.0.0.1: 3003']

})

);

日志('反向代理服务器已启动');

解析(服务器);

});

}

//创建演示服务器

函数startExampleServer(port){

返回新的承诺((解决,拒绝)=> {

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

Const chunks=[];

Req.on('data',chunk=> chunks.push(chunk));

Req.on('end',()=> {

Const buf=Buffer.concat(chunk);

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

});

});

Server.listen(port,()=> {

记录('服务器已启动:%s',端口);

解析(服务器);

});

Server.on('错误',拒绝);

});

}

描述('测试反向代理',函数(){

让服务器;

设exampleServers=[];

//在测试开始之前启动服务器

之前(async function(){

exampleServers.push(等待startExampleServer(3001));

exampleServers.push(等待startExampleServer(3002));

exampleServers.push(等待startExampleServer(3003));

Server=await startProxyServer();

});

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

之后(async function(){

对于(exampleServers的const服务器){

Server.close();

}

});

它('顺序循环返回目标地址',异步函数(){

等待请求(服务器)

获得( '/你好')

.expect(200)

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

等待请求(服务器)

获得( '/你好')

.expect(200)

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

等待请求(服务器)

获得( '/你好')

.expect(200)

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

等待请求(服务器)

获得( '/你好')

.expect(200)

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

});

它('支持POST请求',异步函数(){

等待请求(服务器)

.POST( '/XYZ')

。发送({

一个: 123,

b: 456

})

.expect(200)

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

});

});

说明:

在单元测试开始之前,您需要使用before()注册回调函数,以便在开始执行测试用例时首先启动服务器。

类似地,在执行完所有测试用例以释放资源后,使用after()注册回调函数以关闭服务器(否则mocha进程将不会退出)

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

然后修改package.json文件的脚本部分:

{

'scripts': {

'test':'mocha test.js'

}

}

通过执行以下命令开始测试:

Npm测试

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

测试反向代理

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通过(45ms)

当然,上面的测试代码还不够,其余的都交给了读者。

界面改进

如果您想设计更通用的反向代理中间件,我们还可以通过提供生成http.ClientRequest的函数来动态修改代理请求:

反向代理({

服务器: ['127.0.0.1: 3001','127.0.0.1: 3002','127.0.0.1: 3003'],

请求:函数(req,info){

//info是默认生成的请求选项对象

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

Info.headers ['X-Request-Timestamp']=Date.now();

//返回一个http.ClientRequest对象

返回http.request(info);

}

});

然后在原始的http.request(info,(res2)=> {})部分,你可以听取响应事件:

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

Req2.on('response',res2=> {});

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

反向代理({

服务器: ['127.0.0.1: 3001','127.0.0.1: 3002','127.0.0.1: 3003'],

响应:函数(res,info){

//info是用于发送代理请求的请求选项对象

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

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

}

});

这里只有想法不同,具体的实现方法和代码将不再描述。

总结

本文重点介绍如何使用内置的http模块创建HTTP服务器,启动HTTP请求,以及如何测试HTTP接口的简要介绍。在实现HTTP请求代理的过程中,主要使用Stream对象的Pipe()方法。代码的关键部分只有几行。 Node.js中的许多程序都使用Stream的思想将数据视为流和管道,将一个流转换为另一个流。您可以在Node.js中看到Stream的重要性。

« 在设计中使用按钮| “按钮系列”的按钮位置与用户体验 | 5战略思考,打造具有持久竞争力的品牌 »