30

Nodejs之实现代理

 4 years ago
source link: http://www.wclimb.site/2020/04/04/node-proxy/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

使用http-proxy

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

$ cnpm i http-proxy
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);

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

原生实现

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

function ProxyServer(options) {
  ...

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

  ...
}

源码地址

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

...
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 方法

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);
      }
    });
  }
}

关键代码

// 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 ,错误处理也做得很好,已经很成熟了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK