美文网首页
node-http-proxy 源码学习

node-http-proxy 源码学习

作者: 小黄人get徐先生 | 来源:发表于2020-01-05 00:50 被阅读0次

    什么是代理?

    下面给出一幅图说说我的简单理解:


    如上图所示,代理服务器挡在了服务器的面前。对于用户来说,是不知道这层代理的存在的,因为它是针对服务器的,所以称之为反向代理。如果是用户在客户端主动设置代理,则为正向代理

    正向代理针对客户端,反向代理针对服务端

    那么在服务器前面加设一层代理服务器有什么用呐?

    如图所示,服务器A监听 192.168.100.1628001 端口(假设主要存放的是 /api 开头的后台接口);服务器B监听 192.168.100.1628002 端口(假设主要存放的都是 / 开头的静态文件)。
    那么我们在客户端访问 192.168.100.162:8001/api/xxx 就可以获得接口的返回数据;访问 192.168.100.162:8002/${static_file_path} 就可以获取到对应的静态文件。

    这样就实现了页面和接口的分离,但是真正使用的时候,我们还是需要将它们统一起来。
    那么如何将他们统一起来呐?这就需要代理服务器来帮我们做些工作了。

    假设使用下面这个配置代理服务器(代理服务器监听 8003 端口)。客户端访问192.168.100.162:8003/api/* 的时候代理服务器就帮我们访问192.168.100.162:8001/api/*;访问 192.168.100.162:8003/* 的时候代理服务器就帮我们访问 192.168.100.162:8002/* 。这样的话,就解决了上面我们遇到的问题。

    {
      {
        'path': '/api/',
        'target': '192.168.100.162',
        'port‘: 8001
      },
      {
        'path': '/',
        'target': '192.168.100.162',
        'port‘: 8002
      }
    }
    

    stream.pipe 学习

    第三个关于代理的例子可以学习下,因为在 node-http-proxy 源码里面有用到

    最小的 http 代理服务实现:

    #!/usr/bin/env node
    
    const http = require('http');
    
    http.createServer(function(request, response) {
        const proxyRequest = http.request({
            hostname: 'localhost',
            port: 9001,
            path: request.url,
            method: 'GET',
            headers: request.headers
        });
        proxyRequest.on('response', function (proxyResponse) {
            proxyResponse.pipe(response);
        });
        request.pipe(proxyRequest);
    }).listen(8001);
    
    http.createServer(function (req, res) {
        res.writeHead(200, { 'Content-Type': 'text/plain' });
        res.write('request successfully proxied to port 9001!' + '\n' + JSON.stringify(req.headers, true, 2));
        res.end();
    }).listen(9001);
    

    node-http-proxy 源码学习

    好了,大致了解了什么是代理后,我们开始着手学习 node-http-proxy 源码吧。

    其实 node-http-proxy 就是根据我们输入的配置来帮我们实现请求的代发送。

    我自己将源码抽离出来一个最基本的内容方便学习,其他内容需要深入了解可以查看node-http-proxy学习。

    项目目录如下(和源码一样):


    项目目录

    src/http-proxy.js

    const ProxyServer = require('./http-proxy/index.js').Server;
    
    function createProxyServer(options) {
        return new ProxyServer(options);
    }
    
    ProxyServer.createProxyServer = createProxyServer;
    ProxyServer.createServer      = createProxyServer;
    ProxyServer.createProxy       = createProxyServer;
    
    module.exports = ProxyServer;
    

    src/http-proxy/index.js

    const httpProxy = module.exports;
    const parse_url = require('url').parse;
    const http = require("http");
    const https = require('https');
    const EventEmitter = require('events');
    const web = require('./passes/web-incoming');
    
    function createRightProxy(options) {
    
        return function(req, res, /*, [opts] */) {
    
            // 下面的 this 指向 ProxyServer 实例
    
            const passes = this.webPasses;
            const args = Array.prototype.slice.call(arguments);
            const cntr = args.length - 1;
    
            const requestOptions = options;
            // 解析可选参数 opts
            if (args[cntr] !== res) {
                // 覆盖 request options
                Object.assign(requestOptions, args[cntr]);
            }
    
            ['target'].forEach((e) => {
                // 如果 target 设置为字符串格式,则将其解析为对象
                if (typeof requestOptions[e] === 'string') {
                    requestOptions[e] = parse_url(requestOptions[e]);
                }
            });
    
            if (!requestOptions.target) {
                return this.emit('error', new Error('Must provide a proper URL as target'));
            }
    
            for(let i=0; i < passes.length; i++) {
                if(passes[i](req, res, requestOptions, this)) {
                    break;
                }
            }
        }
    }
    
    class ProxyServer extends EventEmitter {
    
        constructor(options) {
            super();
    
             // 这个方法就是我们这个项目的核心了(对客户端的请求进行响应处理)
            this.web = createRightProxy(options);
            this.options = options || {};
            
            this.webPasses = Object.keys(web).map(function(pass) {
                return web[pass];
            });
    
            this.on('error', this.onError);
        }
    
        onError(err) {
            throw err;
        }
    
        listen(port) {
            // 这个闭包传递给 http.createServer 方法,该方法返回 http.Server 实例
            // 所以调用该方法的时候,this 会指向该 http.Server 实例,所以我们这里需要设置下 this
            const self = this;
            const closure = function(req, res) {
                self.web(req, res);
            };
    
            // 创建代理服务器并监听对应端口
            this._server  = this.options.ssl ?
            https.createServer(this.options.ssl, closure) :
            http.createServer(closure);
            this._server.listen(port);
            return this;
        }
    }
    
    httpProxy.Server = ProxyServer;
    

    src/http-proxy/common.js

    const common = exports;
    const url = require('url');
    
    const isSSL = /^https|wss/;
    
    common.setupOutgoing = (outgoing, options, req) => {
    
        // 使用 target 的 port,不存在则使用默认值
        outgoing.port = options['target'].port || (isSSL.test(options['target'].protocol)? 443 : 80);
    
        // 获取 host 和 hostname
        ['host', 'hostname'].forEach(
            function(e) { outgoing[e] = options['target'][e]; }
        );
    
        // 方法
        outgoing.method = options.method || req.method;
    
        // 处理 headers
        outgoing.headers = Object.assign({}, req.headers);
        if (options.headers) {
            Object.assign(outgoing.headers, options.headers);
        }
    
        // path
        outgoing.path = url.parse(req.url).path || '';
    
        return outgoing;
    
    }
    

    src/http-proxy/passes/web-incoming.js

    const httpNative = require('http');
    const httpsNative = require('https');
    const common = require('../common');
    const webOutgoing = require('./web-outgoing');
    
    const web_o = Object.keys(webOutgoing).map(function(pass) {
        return webOutgoing[pass];
    });
    
    const nativeAgents = {
        http: httpNative,
        https: httpsNative
    };
    
    // web-incoming 的方法功能:处理加工请求。
    module.exports = {
        stream: (req, res, options, server) => {
    
            server.emit('start', req, res, options.target);
    
            const { http, https } = nativeAgents;
    
            const proxyReq = (options.target.protocol === 'https:'? https : http).request(
                common.setupOutgoing(options.ssl || {}, options, req)
            );
    
            proxyReq.on('socket', (socket) => {
                // 用户可以监听 proxyReq 事件
                if(server) { server.emit('proxyReq', proxyReq, req, res, options) };
            });
    
            // 错误处理相关 ----
            req.on('aborted', () => {
                proxyReq.abort();
            });
            const 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();
                    }
                    server.emit('error', err, req, res, url);
                }
            }
            // -----
    
            // 这里还蛮关键的,将 req 请求流通过管道的形式传给 proxyReq 请求
            req.pipe(proxyReq);
    
            proxyReq.on('response', (proxyRes) => {
                // 用户可以监听 proxyRes 事件
                if(server) { server.emit('proxyRes', proxyReq, req, res, options); }
                
                // 对响应进行处理
                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) {
                    proxyRes.on('end', function () {
                        if (server) server.emit('end', req, res, proxyRes);
                    });
                    // 将 proxyRes 响应流通过管道的形式传给 res 响应
                    proxyRes.pipe(res);
                } else {
                    if (server) server.emit('end', req, res, proxyRes);
                }
            });
        }
    };
    

    src/http-proxy/passes/web-outgoing.js

    // web-outcoming 的方法功能:处理响应请求
    module.exports = {
        writeStatusCode: function writeStatusCode(req, res, proxyRes) {
            // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers])
            if(proxyRes.statusMessage) {
              res.statusCode = proxyRes.statusCode;
              res.statusMessage = proxyRes.statusMessage;
            } else {
              res.statusCode = proxyRes.statusCode;
            }
          }
    };
    

    例子

    好了,源码的内容基本上就是上面的样子,实现了 node-http-proxy 最核心基本的功能。下面让我们写个例子测一测:
    examples/test.js

    const httpProxy = require('../src/http-proxy');
    const http = require('http');
    const fs = require('fs');
    
    httpProxy.createServer({
        target:'http://localhost:8001'
    }).listen(8003);
      
    //
    // Target Http Server
    //
    http.createServer(function (req, res) {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2));
      res.end();
    }).listen(8001);
    
    console.log('proxy server start at 8003');
    console.log('server start at 8001');
    

    命令行输入 node examples/test.js 启动脚本。然后打开页面访问 localhost:8003 ,页面成功返回如下:


    成功!!!
    其他使用方法
    • 自定义请求逻辑,如下:
    const httpProxy = require('../src/http-proxy');
    const http = require('http');
    
    const proxy = httpProxy.createProxyServer({});
    
    const proxyServer = http.createServer((req, res) => {
        // 在代理请求之前自定义请求逻辑
        proxy.web(req, res, { target:'http://localhost:8001' });
    });
    
    proxyServer.listen(8003);
    
    // Target Http Server
    //
    http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2));
    res.end();
    }).listen(8001);
    
    console.log('proxy server start at 8003');
    console.log('server start at 8001');
    
    • 代理请求 headers 重写
    const httpProxy = require('../src/http-proxy');
    const http = require('http');
    
    const proxy = httpProxy.createProxyServer({});
    
    // 设置代理请求 headers
    proxy.on('proxyReq', function(proxyReq, req, res, options) {
        proxyReq.setHeader('X-Special-Proxy-Header', 'foobar');
    });
    
    const proxyServer = http.createServer((req, res) => {
        // 在代理请求之前自定义请求逻辑
        proxy.web(req, res, { target:'http://localhost:8001' });
    });
    
    proxyServer.listen(8003);
    
    // Target Http Server
    //
    http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write('request successfully proxied to: ' + req.url + '\n' + JSON.stringify(req.headers, true, 2));
    res.end();
    }).listen(8001);
    
    console.log('proxy server start at 8003');
    console.log('server start at 8001');
    

    请求返回如下:

    request successfully proxied to: /
    {
      "host": "localhost:8003",
      "connection": "keep-alive",
      "cache-control": "max-age=0",
      "upgrade-insecure-requests": "1",
      "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
      "sec-fetch-user": "?1",
      "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
      "sec-fetch-site": "cross-site",
      "sec-fetch-mode": "navigate",
      "accept-encoding": "gzip, deflate, br",
      "accept-language": "zh-CN,zh;q=0.9",
      "cookie": "_ga=GA1.1.1152336717.1566564547",
      "x-special-proxy-header": "foobar" // 设置的 headers 添加成功
    }
    

    后续学习

    http-proxy-middleware
    webpack - devserver - proxy

    相关文章

      网友评论

          本文标题:node-http-proxy 源码学习

          本文链接:https://www.haomeiwen.com/subject/kxqqactx.html