静态文件的反向代理

98 阅读2分钟

最近项目需要实现静态文件的反向代理,分别用node、go、nginx实现,在相同高请求量下,反向代理部分minio的接口及其他数据接口。测试结果是nginx的处理速度最快且性能最好,其次是用go的原生包

1、使用node的第三方包http-proxy-middleware反向代理

var express = require("express");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var compression = require("compression");
require("dotenv").config(); // 设置环境变量
const { createProxyMiddleware } = require('http-proxy-middleware');
const { bakGameConfig } = require("./tool/bakData/gameConfig");
const bakGameConfigStr = JSON.stringify(bakGameConfig);
const { bakRealization } = require("./tool/bakData/realization");
const bakRealizationStr = JSON.stringify(bakRealization);

// 时间格式化 yyyy-MM-dd hh:mm:ss 或者 yyyy-MM-dd
Date.prototype.format = function (fmt) {
  var o = {
    "M+": this.getMonth() + 1,                 //月份 
    "d+": this.getDate(),                    //日 
    "h+": this.getHours(),                   //小时 
    "m+": this.getMinutes(),                 //分 
    "s+": this.getSeconds(),                 //秒 
    "q+": Math.floor((this.getMonth() + 3) / 3), //季度 
    "S": this.getMilliseconds()             //毫秒 
  };
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
  }
  for (var k in o) {
    if (new RegExp("(" + k + ")").test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
    }
  }
  return fmt;
}

var app = express();

app.use(compression()); //开启gzip压缩
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

var indexRouter = require("./routes/index");

app.use("/", indexRouter);

const indexRexStr = "^\/$"; // /
const homeRexStr ="^\/(home|privacy|terms|gamebox|today)(\/)?$"; // /home
const versionRexStr = "^\/version\.json$"; // /version.json
const subchannelRexStr = "^\/[^\/\.]*$"; //main
const subchannelSencondRexStr = "^\/[^\/\.]*\/(home|privacy|terms|gamebox|today)(\/)?$"; //main/home
const gameThirdRexStr = "^\/game\/[^\/\.]*\/(play|play1|match|play)$"; // /game/:gameId/play
const subchannelGameRexStr = "^\/([^\/\.]*)\/game\/[^\/\.]*\/(play|play1|match)(\/)?$"; // /:sub_channel_id/game/:gameId/play/
const staticFileRexStr = "^\/static\/.*\.(jpg|ico|png|js|css|json)$"; // 静态资源

const specialRex = new RegExp(`(${indexRexStr})|(${versionRexStr})|(${subchannelRexStr})|(${subchannelSencondRexStr})|(${gameThirdRexStr})|(${subchannelGameRexStr}|${staticFileRexStr})`, "ig");
const homeRex = new RegExp(homeRexStr, "i");
const versionRex = new RegExp(versionRexStr, "i");
const subchannelRex = new RegExp(subchannelRexStr, "i");
const subchannelSencondRex = new RegExp(subchannelSencondRexStr, "i");
const gameThirdRex = new RegExp(gameThirdRexStr, "i");
const subchannelGameRex = new RegExp(subchannelGameRexStr, "i");
const staticFileRex = new RegExp(staticFileRexStr, "ig");

/**
 * 需要代理的请求,false返回404错误页面
 * @return {Boolean}
 */
const filter = (path, req) => {
  let url = removeQuery(path) || "/";
  const ifInclude = url.match(specialRex) && req.method === 'GET';
  return ifInclude;
};
const removeQuery = (path) => {
  let url = path;
  url = url.replace(/\?.*/ig, "");
  if (url && url?.lastIndexOf("/") === url.length - 1) {
    url = url.substring(0, url?.lastIndexOf("/"));
  }
  return url
}

const dealurl = (path, req) => {
  let url = removeQuery(path)
  let fileSuffix = ".html";
  let hostname = process.env.NODE_ENV === "development" ? process.env.HOSTNAME : req.hostname;
  let finalUrl = "";


  if (url.match(staticFileRex)) {
    url = url.substring(url?.indexOf("/") + 1, url?.length);
    finalUrl = `${url}`;
    //静态资源处理
  } else if (url.match(versionRex)) {
    // version.json
    finalUrl = `${hostname}${url}`;
  }
  else {
    //子渠道处理,默认main或者debug
    if (url.match(subchannelRex) && !url.match(homeRex) && !url.startsWith("/main") && !url.startsWith("/debug")) {
      url = "/main";
    }

    if (url.match(subchannelSencondRex) && !url.startsWith("/main/") && !url.startsWith("/debug/")) {
      const s = url.match(subchannelSencondRex);
      url = `/main/${s[1]}`;
    }

    if (url.match(subchannelGameRex) && !url.startsWith("/main/") && !url.startsWith("/debug/")) {
      const s = url.match(subchannelGameRex);
      url = `${s[0].replace(`/${s[1]}/`, "/main/")}`;
    }

    if (!url.startsWith("/main") && !url.startsWith("/debug")) {
      url = "/main" + url;
    }
    finalUrl = `${hostname}${url}${fileSuffix}`;
  }
  return finalUrl;
}

app.enable('trust proxy')
app.use(
  '/v1/api',
  createProxyMiddleware({
    target: process.env.API_BASE_URL,
    changeOrigin: true,
    pathRewrite: (path, req) => {
      // 其他默认替换掉 /v1/api
      return path.replace('/v1/api', '')
    },
    onProxyRes: function (proxyRes, req, res) {
      if (proxyRes.statusCode >= 400) {
        console.log(`响应代码${proxyRes.statusCode},访问地址:${req.originalUrl}`)
        res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
        const url = removeQuery(req.originalUrl);
        if (url.match(/\/realization$/ig)) {
          res.end(bakRealizationStr);
        } else if (url.match(/\/config$/ig)) {
          res.end(bakGameConfigStr);
        }

      }
    }
  })
);

app.use(
  '/*',
  createProxyMiddleware(
    filter,
    {
      target: `${process.env.WEB_API_BASE_URL}`,
      changeOrigin: true,
      pathRewrite: (path, req) => {

        let url = dealurl(path, req);
        console.log("最终的url:", url)
        return url
      },
      onProxyRes: function (proxyRes, req, res) {
        if (proxyRes.statusCode >= 400 && proxyRes.statusCode < 500) {
          console.log(`响应代码${proxyRes.statusCode},访问地址:${req.originalUrl}`)
          res.writeHead(404, { 'Content-Type': 'text/html' });
          res.end('<html lang="en" class="" style="font-size: 50px;"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel = "icon" type = "image/x-icon" href = "/images/favicon.ico" ></head><body><p style="color: #747474;font-size: 48px;font-family: Helvetica;font-weight: 400;">404</p></body></html>');
        } else if (proxyRes.statusCode >= 500) {
          console.log(`响应代码${proxyRes.statusCode},访问地址:${req.originalUrl}`)
          res.writeHead(500, { 'Content-Type': 'text/html' });
          res.end('<html lang="en" class="" style="font-size: 50px;"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel = "icon" type = "image/x-icon" href = "/images/favicon.ico" ></head><body><p style="color: #747474;font-size: 48px;font-family: Helvetica;font-weight: 400;">Service Not Avaliable</p><p style="color: #747474;font-size: 30px;font-family: Helvetica;margin-top: 20px;">You may want to visit <a style="color: #378ef5;" href="/">Minigame Center Home</a></p></body></html>');
        }
      }
    })
);



app.get("*", (req, res, next) => {
  res.status(400).send('<html lang="en" class="" style="font-size: 50px;"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel = "icon" type = "image/x-icon" href = "/images/favicon.ico" ></head><body><p style="color: #747474;font-size: 48px;font-family: Helvetica;font-weight: 400;">404</p></body></html>')
});//其他页面访问不到,默认显示404


//错误中间件,用于拦截错误,避免服务器挂了,(如果存在异步错误,需要调用next,或者使用util.wrapAsync封装)
app.use((err, req, res, next) => {
  console.error(`${new Date().format("yyyy-MM-dd hh:mm:ss")}服务器出现错误:`, err)
  res.status(500).send('<html lang="en" class="" style="font-size: 50px;"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><link rel = "icon" type = "image/x-icon" href = "/images/favicon.ico" ></head><body><p style="color: #747474;font-size: 48px;font-family: Helvetica;font-weight: 400;">Service Not Avaliable</p><p style="color: #747474;font-size: 30px;font-family: Helvetica;margin-top: 20px;">You may want to visit <a style="color: #378ef5;" href="/">Minigame Center Home</a></p></body></html>')
})

module.exports = app;

2、nginx反向代理


worker_processes  1;

events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       8022;
        server_name  localhost;


        location /static/ {
            proxy_pass http://minio.xxx.xxx/static/;         
        }     

        location /v1/api/channel/ {
            proxy_pass http://gms.xxx.xxx/v1/api/channel/;         
        }               


        location / {
            proxy_pass http://minio.xxx.xxx/fun.minigame.vip/;         
        }  

        rewrite ^/main(/)?$ /main.html last;
        rewrite ^/(home|privacy|terms|gamebox|today)(/)?$ /main/$1.html last;
        rewrite ^\/[^\/\.]*$ /main.html last;  
        rewrite ^\/[^\/\.]*\/(home|privacy|terms|gamebox|today)$ /main/$1.html last;
        rewrite ^\/game\/([^\/\.]*)\/(play|play1|match|play|play/|play1/|match/|play/)$ /main/game/$1/play.html last;
        rewrite ^\/([^\/\.]*)\/game\/([^\/\.]*)\/(play|play1|match)$ /main/game/$2/play.html last;

        proxy_intercept_errors on;                                           
        error_page   404  /40x.html;
        location = /40x.html {
            root   html;
        }
        # error_page   404   http://www.baidu.com;

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }


    }

}

3、go原生包net/http/httputil,实现反向代理

package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"regexp"
)

func main() {

	gmsURL, err := url.Parse("http://gms.xxx.xxx/")
	if err != nil {
		panic(err)
	}

	gmsProxy := httputil.NewSingleHostReverseProxy(gmsURL)
	http.HandleFunc("/v1/api/channel/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Println(r.URL.Path)
		r.Host = "gms.xxx.xxx"
		gmsProxy.ServeHTTP(w, r)
	})

	minioURL, err := url.Parse("http://minio.xxx.xxx")
	if err != nil {
		panic(err)
	}

	minioProxy := httputil.NewSingleHostReverseProxy(minioURL)
	// 设置 ErrorHandler 回调函数,拦截代理返回 404 错误
	minioProxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
		log.Printf("Proxy error: %v\n", err)
		rw.WriteHeader(http.StatusNotFound)
		rw.Write([]byte("404 Not Found"))
	}
	minioProxy.ModifyResponse = func(res *http.Response) error {
		if res.StatusCode == 404 {
			return errors.New("404 error")
		}
		return nil
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		rewrites := []struct {
			pattern string
			replace string
		}{
			{`^/(home|privacy|terms|gamebox|today)(/)?$`, `/fun.minigame.vip/main/$1.html`},
			{`^/[^/.]*(/)?$`, `/fun.minigame.vip/main.html`},
			{`^/[^/.]*/(home|privacy|terms|gamebox|today)(/)?$`, `/fun.minigame.vip/main/$1.html`},
			{`^/game/([^/.]*)/(play|play1|match|play)(/)?$`, `/fun.minigame.vip/main/game/$1/$2.html`},
			{`^/([^/.]*)/game/([^/.]*)/(play|play1|match)$`, `/fun.minigame.vip/main/game/$2/$3.html`},
		}
		for _, rw := range rewrites {
			if ok, _ := regexp.MatchString(rw.pattern, r.URL.Path); ok {
				r.URL.Path = regexp.MustCompile(rw.pattern).ReplaceAllString(r.URL.Path, rw.replace)
				break
			}
		}
		fmt.Println(r.URL.Path)
		r.Host = "minio.xxx.xxx"
		minioProxy.ServeHTTP(w, r)
	})
	fmt.Println("运行服务localhost:2002")
	log.Fatal(http.ListenAndServe(":2002", nil))
}