最近项目需要实现静态文件的反向代理,分别用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))
}