发布时间: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的重要性。