Nodejs之实现代理

前言

我们上线一个网站,往往需要代理来完成需求,不然的话就只能使用 IP 加端口的方式来访问应用,目前基本上都会使用 Nginx 来完成反向代理,那么我们开发 Node 应用一定需要 Nginx 吗?肯定不是,我们完全可以通过 Node 来做,但是大多是不建议你用 Node 去处理,因为 Nginx 功能更强大,比如负载均衡,本文主要是帮你了解如何通过 Node 实现代理

使用http-proxy

最简单的做法是拿已经写好的包直接使用

1
$ cnpm i http-proxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const proxy = require("http-proxy").createProxyServer({});

server = require("http").createServer(function(req, res) {
const host = req.headers.host;
switch (host) {
case "your domain":
proxy.web(req, res, { target: "http://localhost:8000" });
break;
default:
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.end("Welcome to my server!");
}
});

console.log("listening on port 80");

server.listen(80);

上面👆代码监听 80 端口(如果你的服务器目前使用来 Nginx,暂用80 端口的话,需要先暂停 Nginx 服务),然后我们通过访问域名(前提是域名做好了解析),然后使用 proxy.web方法反向代理到当前服务下的 8000 端口,到此一个简单的服务完成了

原生实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const http = require("http");
const url = require("url");

function request(req, res) {
const reqUrl = url.parse(req.url);
const target = url.parse("http://localhost:3000");
const options = {
hostname: target.hostname,
port: target.port,
path: reqUrl.path,
method: req.method,
headers: req.headers
};

const proxyReq = http.request(options);

proxyReq.on("response", proxyRes => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res);
});

req.pipe(proxyReq);
}

http
.createServer()
.on("request", request)
.listen(8003);

是不是很简单?通过访问 8003 端口,我们将请求转发到 3000 端口,可以复制当前代码尝试一下,前提是 3000 端口可以正常访问。当访问 8003 端口的时候,内部重新请求我们需要代理的地址,然后通过 pipe 返回转发后的数据

http-proxy源码实现原理

源码地址
执行 proxy.web

1
2
3
4
5
6
7
function ProxyServer(options) {
...

this.web = this.proxyRequest = createRightProxy('web')(options);

...
}

源码地址

内部关键代码执行了一下这段,passes 是一个数组方法,包含deleteLengthtimeoutXHeadersstream,关键点在 stream,其他基本是辅助作用,XHeaders 功能是设置 x-forwarded-* 这种 header,不过前提是 option 配置了 xfwd 才行,timeout 是设置超时时间的,deleteLength 只有请求方法是 OPTIONSDELETE 才会执行

1
2
3
4
5
6
...
for(var i=0; i < passes.length; i++) {
if(passes[i](req, res, requestOptions, head, this, cbl)) { // passes can return a truthy value to halt the loop
break;
}
}

源码地址
stream 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
module.exports = {
deleteLength: ....,
timeout: ...,
XHeaders: ...,
stream: function stream(req, res, options, _, server, clb) {

// And we begin!
server.emit('start', req, res, options.target || options.forward);

var agents = options.followRedirects ? followRedirects : nativeAgents;
var http = agents.http;
var https = agents.https;

if(options.forward) {
// If forward enable, so just pipe the request
var forwardReq = (options.forward.protocol === 'https:' ? https : http).request(
common.setupOutgoing(options.ssl || {}, options, req, 'forward')
);

// error handler (e.g. ECONNRESET, ECONNREFUSED)
// Handle errors on incoming request as well as it makes sense to
var forwardError = createErrorHandler(forwardReq, options.forward);
req.on('error', forwardError);
forwardReq.on('error', forwardError);

(options.buffer || req).pipe(forwardReq);
if(!options.target) { return res.end(); }
}

// Request initalization
var proxyReq = (options.target.protocol === 'https:' ? https : http).request(
common.setupOutgoing(options.ssl || {}, options, req)
);

// Enable developers to modify the proxyReq before headers are sent
proxyReq.on('socket', function(socket) {
if(server) { server.emit('proxyReq', proxyReq, req, res, options); }
});

// allow outgoing socket to timeout so that we could
// show an error page at the initial request
if(options.proxyTimeout) {
proxyReq.setTimeout(options.proxyTimeout, function() {
proxyReq.abort();
});
}

// Ensure we abort proxy if request is aborted
req.on('aborted', function () {
proxyReq.abort();
});

// handle errors in proxy and incoming request, just like for forward proxy
var proxyError = createErrorHandler(proxyReq, options.target);
req.on('error', proxyError);
proxyReq.on('error', proxyError);

function createErrorHandler(proxyReq, url) {
return function proxyError(err) {
if (req.socket.destroyed && err.code === 'ECONNRESET') {
server.emit('econnreset', err, req, res, url);
return proxyReq.abort();
}

if (clb) {
clb(err, req, res, url);
} else {
server.emit('error', err, req, res, url);
}
}
}

(options.buffer || req).pipe(proxyReq);

proxyReq.on('response', function(proxyRes) {
if(server) { server.emit('proxyRes', proxyRes, req, res); }

if(!res.headersSent && !options.selfHandleResponse) {
for(var i=0; i < web_o.length; i++) {
if(web_o[i](req, res, proxyRes, options)) { break; }
}
}

if (!res.finished) {
// Allow us to listen when the proxy has completed
proxyRes.on('end', function () {
if (server) server.emit('end', req, res, proxyRes);
});
// We pipe to the response unless its expected to be handled by the user
if (!options.selfHandleResponse) proxyRes.pipe(res);
} else {
if (server) server.emit('end', req, res, proxyRes);
}
});
}
}

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Request initalization
var proxyReq = (options.target.protocol === 'https:' ? https : http).request(
common.setupOutgoing(options.ssl || {}, options, req)
);
(options.buffer || req).pipe(proxyReq);
proxyReq.on('response', function(proxyRes) {
if(!res.headersSent && !options.selfHandleResponse) {
for(var i=0; i < web_o.length; i++) {
if(web_o[i](req, res, proxyRes, options)) { break; }
}
}
if (!res.finished) {
// We pipe to the response unless its expected to be handled by the user
if (!options.selfHandleResponse) proxyRes.pipe(res);
}
});

实现大致和我们之前写得差不多,但是他考虑得更多,支持 https,错误处理也做得很好,已经很成熟了

结尾

至此就基本讲完了,本文意在实操,理论讲解涉及极少,在写一个功能的时候,还是要多了解一下内部的原理知识,能帮助你更好的理解,如果错误还望指正。

本文地址 Nodejs之实现代理

坚持原创技术分享,您的支持将鼓励我继续创作!