node-http-proxy 源碼學(xué)習(xí)

什么是代理?

下面給出一幅圖說說我的簡單理解:


如上圖所示,代理服務(wù)器擋在了服務(wù)器的面前。對于用戶來說,是不知道這層代理的存在的,因為它是針對服務(wù)器的,所以稱之為反向代理。如果是用戶在客戶端主動設(shè)置代理,則為正向代理。

正向代理針對客戶端,反向代理針對服務(wù)端

那么在服務(wù)器前面加設(shè)一層代理服務(wù)器有什么用吶?

如圖所示,服務(wù)器A監(jiān)聽 192.168.100.1628001 端口(假設(shè)主要存放的是 /api 開頭的后臺接口);服務(wù)器B監(jiān)聽 192.168.100.1628002 端口(假設(shè)主要存放的都是 / 開頭的靜態(tài)文件)。
那么我們在客戶端訪問 192.168.100.162:8001/api/xxx 就可以獲得接口的返回數(shù)據(jù);訪問 192.168.100.162:8002/${static_file_path} 就可以獲取到對應(yīng)的靜態(tài)文件。

這樣就實現(xiàn)了頁面和接口的分離,但是真正使用的時候,我們還是需要將它們統(tǒng)一起來。
那么如何將他們統(tǒng)一起來吶?這就需要代理服務(wù)器來幫我們做些工作了。

假設(shè)使用下面這個配置代理服務(wù)器(代理服務(wù)器監(jiān)聽 8003 端口)。客戶端訪問192.168.100.162:8003/api/* 的時候代理服務(wù)器就幫我們訪問192.168.100.162:8001/api/*;訪問 192.168.100.162:8003/* 的時候代理服務(wù)器就幫我們訪問 192.168.100.162:8002/* 。這樣的話,就解決了上面我們遇到的問題。

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

stream.pipe 學(xué)習(xí)

第三個關(guān)于代理的例子可以學(xué)習(xí)下,因為在 node-http-proxy 源碼里面有用到

最小的 http 代理服務(wù)實現(xiàn):

#!/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 源碼學(xué)習(xí)

好了,大致了解了什么是代理后,我們開始著手學(xué)習(xí) node-http-proxy 源碼吧。

其實 node-http-proxy 就是根據(jù)我們輸入的配置來幫我們實現(xiàn)請求的代發(fā)送。

我自己將源碼抽離出來一個最基本的內(nèi)容方便學(xué)習(xí),其他內(nèi)容需要深入了解可以查看node-http-proxy學(xué)習(xí)。

項目目錄如下(和源碼一樣):


項目目錄

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;
        // 解析可選參數(shù) opts
        if (args[cntr] !== res) {
            // 覆蓋 request options
            Object.assign(requestOptions, args[cntr]);
        }

        ['target'].forEach((e) => {
            // 如果 target 設(shè)置為字符串格式,則將其解析為對象
            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();

         // 這個方法就是我們這個項目的核心了(對客戶端的請求進(jìn)行響應(yīng)處理)
        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 實例
        // 所以調(diào)用該方法的時候,this 會指向該 http.Server 實例,所以我們這里需要設(shè)置下 this
        const self = this;
        const closure = function(req, res) {
            self.web(req, res);
        };

        // 創(chuàng)建代理服務(wù)器并監(jiān)聽對應(yīng)端口
        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,不存在則使用默認(rèn)值
    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) => {
            // 用戶可以監(jiān)聽 proxyReq 事件
            if(server) { server.emit('proxyReq', proxyReq, req, res, options) };
        });

        // 錯誤處理相關(guān) ----
        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);
            }
        }
        // -----

        // 這里還蠻關(guān)鍵的,將 req 請求流通過管道的形式傳給 proxyReq 請求
        req.pipe(proxyReq);

        proxyReq.on('response', (proxyRes) => {
            // 用戶可以監(jiān)聽 proxyRes 事件
            if(server) { server.emit('proxyRes', proxyReq, req, res, options); }
            
            // 對響應(yīng)進(jìn)行處理
            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 響應(yīng)流通過管道的形式傳給 res 響應(yīng)
                proxyRes.pipe(res);
            } else {
                if (server) server.emit('end', req, res, proxyRes);
            }
        });
    }
};

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

// web-outcoming 的方法功能:處理響應(yīng)請求
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;
        }
      }
};

例子

好了,源碼的內(nèi)容基本上就是上面的樣子,實現(xiàn)了 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({});

// 設(shè)置代理請求 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" // 設(shè)置的 headers 添加成功
}

后續(xù)學(xué)習(xí)

http-proxy-middleware
webpack - devserver - proxy

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容