TL;DR: 上一篇文章我们介绍了关于计算机网络代理的定义,代理的类型 以及其对应的实现原理。这篇文章将通过手写代码的方式,用100行代码实现一个同时支持HTTP普通代理,隧道代理,以及TCP代理的代理软件。同时介绍中业务代码如何使用HTTP代理。
100行代码实现一个多功能的代理服务
实现HTTP普通代理
《HTTP权威指南》图描述了普通代理的基本原理:
看样子我们在中间实现一个API服务,满足左手交右手的功能即可。那中间的这个API服务要怎么知道你最终要去的目标服务器是哪里呢?所以这里在客户端跟代理服务器之间要有一个约定,客户端要把目标地址放到url里面,当代理服务器收到请求时,从url解析出目标服务器的地址,再向目标服务器发起请求,把返回的内容再发送给客户端,整个过程就完成了。
下面的20行左右的代码就可以在NodeJS里面实现了这个 普通代理。
function request(srcReq, srcRes) {
const url = new URL(srcReq.url);
const options = {
hostname: url.hostname,
port: url.port || 80,
path: url.pathname,
method: srcReq.method,
headers: srcReq.headers
}
// timestamp clientAddress requestMethod URI result/statusCodes responseTime
const startAt = process.hrtime();
let logLine = `${
Date.now()} ${
requestIP.getClientIp(srcReq)} ${
srcReq.method} ${
srcReq.url}`
const destRequest = http.request(options, destResponse => {
srcRes.writeHead(destResponse.statusCode, destResponse.headers);
destResponse.pipe(srcRes)
logLine += ` ${
destResponse.statusCode} ${
_.get(destResponse.headers,"content-type",'-')} ${
_getTotalTime(startAt)}`
access.log(logLine);
}).on('error', e => {
srcRes.end();
logLine += ` ERROR ${
_getTotalTime(startAt)}`;
error.log(logLine + '\r\n' + e)
})
srcReq.pipe(destRequest);
}
http.createServer().on('request', request);
代码的详细介绍如下:
- 代理服务收到请求后,从url解析出目标服务器的地址,同时将客户端请求的 path, method 以及 headers都搬过去 ,代码 2-9行
- 代理服务向目标服务器发起一个另一个HTTP请求,代码 13-22 行
- 把客户端请求的内容,用管道的方式 发送给目标服务器,代码23行
- 把目标服务器返回的内容,同样用管道的方式 发送回给客户端,代码15行
- 通过Nodejs的 http模块,创建一个服务器。从这里也可以看出来,这个代理服务器是在HTTP 层上面的
实现HTTP隧道代理
关于HTTP普通代理的问题可以参考上一篇文章。HTTP 隧道代理的工作原理如下图:
通过HTTP CONNECT方法,做客户端跟代理中间建立起一个隧道,然后代理服务器跟目标服务器发起一个SSL的TCP连接,最后代理将与目标服务器建立TCP连接,与客户端之间建立的TCP连接对接起来,从而实现了这个隧道代理的过程。
下面这另外的20行代码也实现了这一过程:
function connect(srcReq, srcSocket) {
const url = new URL(`http://${
srcReq.url}`);
const startAt = process.hrtime();
let logLine = `${
Date.now()} CONNECT ${
url.hostname}:${
url.port}`
const destSocket = net.connect(parseInt(url.port), url.hostname, () => {
srcSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
destSocket.pipe(srcSocket);
logLine += ` ${
_getTotalTime(startAt)}