使用Golang实现Nginx代理功能
由于业务需要实现对多个web应用做同域二级目录代理,用NGINX的又感觉太重了,而且不好做配置页面,用golang来实现代理功能
- 支持正则表达式匹配机制
- 支持多应用多级目录代理。
- 支持应用子路由代理
- 支持webapi代理
- 支持websocket代理
- 支持禁用缓存设置
- 支持本地文件服务
- 支持http、https混合使用
- 支持/dir/app 重定向为 /dir/app/
- 支持简单的路由热度升级
定义处理器选项并初始化默认数据记录
package main
const (
/* 本地文件系统 */
LocalFileSystem HandlerType = 10000
/* 远程Http服务 */
RemoteHttpServer HandlerType = 20000
)
type HandlerOptions struct {
Id int32
/* 处理器名称 */
Name string
/* 处理器类型 */
Type HandlerType
/* 处理路径 */
Path string
/* 禁用缓存 */
DisableCache bool
/* 目标服务器,或本地文件目录 */
Target string
/* 子处理器列表 */
SubHandlers []*HandlerOptions
/* 是否启用 */
Enabled bool
}
var handlers = []*HandlerOptions{
{
Id: 1,
Name: "My First App",
Path: "^/app1",
Type: RemoteHttpServer,
Target: "https://192.168.1.20:8000/",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{
{
Id: 11,
Name: "API接口",
Path: "^/app1/api",
Type: RemoteHttpServer,
Target: "http://192.168.1.30:8100/api",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{},
},
{
Id: 12,
Name: "静态文件服务器",
Path: "^/app1/static/",
Type: RemoteHttpServer,
Target: "https://192.168.1.70:8200/static/",
Enabled: true,
DisableCache: false,
SubHandlers: []*HandlerOptions{},
},
{
Id: 13,
Name: "其他系统接口",
Path: "^/app1/others/api/",
Type: RemoteHttpServer,
Target: "http://192.168.1.50:8300/api/",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{},
},
},
},
{
Id: 20,
Name: "Local File App",
Path: "/test",
Type: LocalFileSystem,
Target: "/root/workspace/test/static/www",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{
{
Id: 21,
Name: "api",
Path: "^/test/api/",
Type: RemoteHttpServer,
Target: "https://192.168.1.100:8000/api/",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{},
},
{
Id: 22,
Name: "静态文件",
Path: "^/test/static/",
Type: LocalFileSystem,
Target: "/root/workspace/test/static",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{},
},
{
Id: 23,
Name: "websocket相关",
Path: "^/test/ws",
Type: RemoteHttpServer,
Target: "https://192.168.1.100:8000/ws",
Enabled: true,
DisableCache: true,
SubHandlers: []*HandlerOptions{},
},
},
}
处理器节点的实现
通过root节点的Options方法更新全部处理器节点。
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"sync"
)
type ProxyHandlerNode struct {
// 节点选项
options *HandlerOptions
// 节点处理器列表
nodes []*ProxyHandlerNode
// 匹配正则
regxExpression *regexp.Regexp
// 匹配计数器, 用于热度排序
Popularity int
// 目标服务器URL
TargetURL *url.URL
// 目标服务器路径(这段路径需要放到请求Path前面)
TargetPath string
// 操作锁
locker sync.Mutex
// 本地文件系统
fileSystem *http.Handler
}
// 配置节点选项
func (node *ProxyHandlerNode) Options(options []*HandlerOptions) {
node.locker.Lock()
defer node.locker.Unlock()
node.nodes = make([]*ProxyHandlerNode, 0)
for i := 0; i < len(options); i++ {
opt := options[i]
if !opt.Enabled {
continue
}
newNode := ProxyHandlerNode{}
newNode.Popularity = 0
newNode.options = opt
var err error
newNode.regxExpression, err = regexp.Compile(opt.Path)
if err != nil {
fmt.Printf("Regexp Compile Error:%v\n", err)
continue
}
newNode.TargetURL, err = url.Parse(opt.Target)
if err != nil {
fmt.Printf("URL Parse Error:%v\n", err)
continue
}
if newNode.options.Type == LocalFileSystem {
var fs = http.FileServer(LocalFile(newNode.options.Target, false))
newNode.fileSystem = &fs
}
newNode.TargetPath = newNode.TargetURL.Path
newNode.TargetURL.Path = ""
newNode.Options(opt.SubHandlers)
node.nodes = append(node.nodes, &newNode)
}
}
/*
* 随着匹配次数热度爬升
*/
func popularityUp(nodes []*ProxyHandlerNode, node *ProxyHandlerNode, index int) {
node.Popularity++
if index > 0 {
if node.Popularity > nodes[index-1].Popularity {
nodes[index], nodes[index-1] = nodes[index-1], nodes[index]
}
}
}
/*
* 匹配请求URL的处理器
*/
func (node *ProxyHandlerNode) MatchNode(url string) *ProxyHandlerNode {
node.locker.Lock()
defer node.locker.Unlock()
for i := 0; i < len(node.nodes); i++ {
var cnode = node.nodes[i]
if cnode.regxExpression.Match([]byte(url)) {
popularityUp(node.nodes, cnode, i)
var ret = cnode.MatchNode(url)
if ret != nil {
return ret
}
return cnode
}
}
return nil
}
/*
* 代理请求
*/
func (node *ProxyHandlerNode) ProxyRequest(req *http.Request, res http.ResponseWriter) {
// 替换url
var requestPath = node.regxExpression.ReplaceAllString(req.URL.Path, "")
// http重定向
if len(requestPath) == 0 && len(req.URL.RawQuery) == 0 {
// 把 /dir/app 重定向为 /dir/app/
if !strings.HasSuffix(req.URL.Path, "/") {
http.Redirect(res, req, req.URL.Path+"/", http.StatusTemporaryRedirect)
return
}
}
// 节点是本地文件系统
if node.fileSystem != nil {
req.URL.Path = requestPath
(*node.fileSystem).ServeHTTP(res, req)
return
}
// 修复路径
req.URL.Path = node.TargetPath + requestPath
// 代理
proxy := httputil.NewSingleHostReverseProxy(node.TargetURL)
proxy.ModifyResponse = func(r *http.Response) error {
if node.options.DisableCache {
r.Header.Set("Cache-Control", "no-store,no-cache")
r.Header.Set("Pragma", "no-store,no-cache")
r.Header.Set("Expires", "0")
r.Header.Del("Last-Modified")
}
r.Header.Del("Server")
return nil
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
fmt.Printf("Serve Error %v {%v}\n", r.URL, err)
}
if node.options.DisableCache {
req.Header.Set("Cache-Control", "no-store,no-cache")
req.Header.Set("Pragma", "no-store,no-cache")
req.Header.Del("if-modified-since")
req.Header.Del("if-none-match")
}
proxy.ServeHTTP(res, req)
}
本地文件系统服务
package main
import (
"net/http"
"os"
"path"
"strings"
"github.com/gin-gonic/gin"
)
const INDEX = "index.html"
type ServeFileSystem interface {
http.FileSystem
Exists(prefix string, path string) bool
}
type localFileSystem struct {
http.FileSystem
root string
indexes bool
}
func LocalFile(root string, indexes bool) *localFileSystem {
return &localFileSystem{
FileSystem: gin.Dir(root, indexes),
root: root,
indexes: indexes,
}
}
func (l *localFileSystem) Exists(prefix string, filepath string) bool {
if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
name := path.Join(l.root, p)
stats, err := os.Stat(name)
if err != nil {
return false
}
if stats.IsDir() {
if !l.indexes {
index := path.Join(name, INDEX)
_, err := os.Stat(index)
if err != nil {
return false
}
}
}
return true
}
return false
}
食用方式
package main
import (
"crypto/tls"
"fmt"
"log"
"net/http"
)
var rootHandler = &ProxyHandlerNode{}
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
//
var node = rootHandler.MatchNode(req.URL.Path)
if node != nil {
node.ProxyRequest(req, res)
return
}
fmt.Printf("Not Handle Request %v\n", req.RequestURI)
res.WriteHeader(http.StatusBadGateway)
res.Write([]byte("Proxy Error: No suitable forwarding processor matched."))
}
func main() {
// 初始化处理器
rootHandler.Options(handlers)
// 跳过tsl证书验证
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
http.HandleFunc("/", handleRequestAndRedirect)
// http
if err := http.ListenAndServe(":8888", nil); err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律