Skip to content

Golang HTTP反向代理实现与媒体信息缓存

简介

朋友提供了一个需求,由于互联网一些媒体资源出现了网络抖动,导致在某些时刻出现了媒体信息无法加载,由于加载的是动态图床,数量也比较大,在寻找了大量的现有解决方法之后,最终想到使用代理缓存的方式。

试用nginx

最开始想到的就是使用nginx进行解决,但由于存在多个域名,而手里只有一个ip,也不想写lua脚本,网络抖动时会返回302重定向;故放弃nginx的方案。

编程解决

经过一段时间的思想抗争,最终还是选择使用编程解决,思索再三,使用Golang解决,原因有几点,一是Golang支持多系统无障碍运行,二是Golang网络默认官方就带有反向代理的包,三是Golang实现很快。

整理思路

首先创建http服务->然后拦截所有请求->接收到请求判断是否存在缓存->存在则直接发返回缓存数据->不存在则使用代理请求服务,然后在返回结果。

主要功能

  1. 实现http反向代理
  2. 对响应数据进行区别处理
    1. 如果为image则缓存response
    2. 如果为其他类型则跳过

入口函数

func main() {
    //创建日志文件
    file, err := mkFile("httpProxy.log")
    if err != nil {
        log.Fatalln("fail to create httpProxy.log file!")
    }
    logger = log.New(file, "", log.Llongfile)
    logger.SetFlags(log.LstdFlags)
    // 初始化代理计数
    proxyCount=0
    flag.IntVar(&port, "P", 80, "端口号,默认为80")
    flag.StringVar(&host, "H", "0.0.0.0", "主机地址,默认为0.0.0.0")
    flag.StringVar(&internetIP, "I", "localhost", "主机公网地址,默认为")
    flag.Parse()
    // 获取本地IP
    localIPs=getLocalIps()
    localIPs=append(localIPs,internetIP)
    // 创建反向代理缓存
    domain=make(map[string]*httputil.ReverseProxy)
    // 创建http 服务
    http.HandleFunc("/", proxyRequestHandler)
    hostStr:=host+":"+strconv.Itoa(port)
    logger.Println("启动host",hostStr)
    // 启动http 服务
    logger.Fatal(http.ListenAndServe(hostStr, nil))
}

创建反向代理


// NewProxy 创建反向代理
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
    url, err := url.Parse(targetHost)
    if err != nil {
        return nil, err
    }
    proxy := httputil.NewSingleHostReverseProxy(url)
    var DefaultTransport http.RoundTripper = &http.Transport{
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        ForceAttemptHTTP2:     true,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
        MaxIdleConnsPerHost: 20,
    }
    proxy.Transport=DefaultTransport

    originalDirector := proxy.Director
    proxy.Director = func(req *http.Request) {
        originalDirector(req)
        modifyRequest(req)
    }

    proxy.ModifyResponse = modifyResponse()
    proxy.ErrorHandler = errorHandler()
    return proxy, nil
}

修改http响应数据


// responseModify 修改http响应数据
func responseModify(r *http.Response) error  {
    //获取返回请求的内容类型
    typeStr:=r.Header.Get("Content-Type")
    //如果为图片
    if strings.Contains(typeStr, "image"){
        if r.StatusCode != 200 {
            logger.Fatal("下载文件失败,http状态码为:",r.StatusCode,"响应内容为:",r.Body)
            return nil
        }
        //缓存文件
        querys:=r.Request.URL.Query()
        args:=querys.Get("x")+querys.Get("y")+querys.Get("z")+querys.Get("ak")+querys.Get("styles")
        js:=matchFile(md5V(args))
        f, err := os.Create(js)

        if err==nil{
            res,err:=httputil.DumpResponse(r,true)
            if err==nil{
                f.Write(res)
                if err1 := f.Close(); err == nil {
                    err = err1
                }
            }
        }
    }
    //累计代理次数
    proxyCount++
    logger.Println("代理请求次数:",proxyCount)
    if proxyCount>1000000{
        proxyCount=0
        logger.Println("代理超过:1000000 重置为0")
    }
    //logger.Println("完成请求:",r.Request.RequestURI)
    return nil
}

返回缓存


// executeCache 执行缓存
func executeCache(f string,w http.ResponseWriter, r *http.Request){
    file,_:=os.Open(f)
    buff,err2:=ioutil.ReadAll(file)
    if err2==nil{
        temr := bufio.NewReader(bytes.NewReader(buff))
        resp, err3 := http.ReadResponse(temr, nil)
        if err3 == nil{
            for k,vv:=range resp.Header{
                for _,v:=range vv{
                    w.Header().Add(k,v)
                }
            }
            bufio.NewReader(resp.Body).WriteTo(w)
            cacheCount++
            logger.Println("缓存击中一次",f,"缓存击中共计次数:",cacheCount)
            if cacheCount>1000000{
                cacheCount=0
                logger.Println("缓存击中超过:1000000 重置为0")
            }
        }else{
            w.WriteHeader(400)
            w.Write([]byte(err3.Error()))
        }
    }else{
        // 使用缓存
        executeProxy(w,r)
    }
}

完整代码

package main

import (
    "bufio"
    "bytes"
    "crypto/md5"
    "crypto/sha1"
    "encoding/hex"
    "errors"
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "net/http/httputil"
    "net/url"
    "os"
    "path"
    "strconv"
    "strings"
    "time"
)
var logger    *log.Logger
var domain map[string]*httputil.ReverseProxy
var h = sha1.New()
var port int
var host string
var internetIP string
var proxyCount int64
var cacheCount int64
var localIPs []string

// NewProxy 创建反向代理
func NewProxy(targetHost string) (*httputil.ReverseProxy, error) {
    url, err := url.Parse(targetHost)
    if err != nil {
        return nil, err
    }
    proxy := httputil.NewSingleHostReverseProxy(url)
    var DefaultTransport http.RoundTripper = &http.Transport{
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        ForceAttemptHTTP2:     true,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
        MaxIdleConnsPerHost: 20,
    }
    proxy.Transport=DefaultTransport

    originalDirector := proxy.Director
    proxy.Director = func(req *http.Request) {
        originalDirector(req)
        modifyRequest(req)
    }

    proxy.ModifyResponse = modifyResponse()
    proxy.ErrorHandler = errorHandler()
    return proxy, nil
}

// modifyRequest 修改请求
func modifyRequest(req *http.Request) {
    req.Header.Set("X-Proxy", "Maple-Reverse-Proxy")
}

// errorHandler 错误处理器
func errorHandler() func(http.ResponseWriter, *http.Request, error) {
    return func(w http.ResponseWriter, req *http.Request, err error) {
        fmt.Printf("Got error while modifying response: %v \n", err)
        w.WriteHeader(500)
        w.Write([]byte("proxy error"))
        return
    }
}

// mkdir 创建文件夹
func mkdir(dir string) {
    exist, err := PathExists(dir)
    if err != nil {
        fmt.Println(err.Error())
    } else {
        if exist {
            //fmt.Println(dir + "文件夹已存在!")
        } else {
            // 文件夹名称,权限
            err := os.Mkdir(dir, os.ModePerm)
            if err != nil {
                fmt.Println("文件夹创建失败", err.Error())
            } else {
                fmt.Println("创建文件夹", dir)
            }
        }
    }
}

// PathExists 判断文件夹是否存在
func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        return true, nil
    }
    if os.IsNotExist(err) {
        return false, nil
    }
    return false, err
}

// responseModify 修改http响应数据
func responseModify(r *http.Response) error  {
    //获取返回请求的内容类型
    typeStr:=r.Header.Get("Content-Type")
    //如果为图片
    if strings.Contains(typeStr, "image"){
        if r.StatusCode != 200 {
            logger.Fatal("下载文件失败,http状态码为:",r.StatusCode,"响应内容为:",r.Body)
            return nil
        }
        //缓存文件
        querys:=r.Request.URL.Query()
        args:=querys.Get("x")+querys.Get("y")+querys.Get("z")+querys.Get("ak")+querys.Get("styles")
        js:=matchFile(md5V(args))
        f, err := os.Create(js)

        if err==nil{
            res,err:=httputil.DumpResponse(r,true)
            if err==nil{
                f.Write(res)
                if err1 := f.Close(); err == nil {
                    err = err1
                }
            }
        }
    }
    //累计代理次数
    proxyCount++
    logger.Println("代理请求次数:",proxyCount)
    if proxyCount>1000000{
        proxyCount=0
        logger.Println("代理超过:1000000 重置为0")
    }
    //logger.Println("完成请求:",r.Request.RequestURI)
    return nil
}

// modifyResponse 修改http响应数据
func modifyResponse() func(*http.Response) error {
    return func(r *http.Response)error {
        return responseModify(r)
    }
}

// matchFile 匹配缓存文件夹
func matchFile(bs string) string{
    dir:=bs[0:2]
    mkdir(path.Join("cache-basedir",dir))
    js := path.Join("cache-basedir",dir,bs)
    return js
}

// md5V 生产M5D值
func md5V(str string) string  {
    h := md5.New()
    h.Write([]byte(str))
    return hex.EncodeToString(h.Sum(nil))
}

// isExist 判断文件是否否存在
func isExist(path string)(bool){
    _, err := os.Stat(path)
    if err != nil{
        if os.IsExist(err){
            return true
        }
        if os.IsNotExist(err){
            return false
        }
        fmt.Println(err)
        return false
    }
    return true
}

// getLocalIps 获取本地IP
func getLocalIps()[]string{
    addrs, err := net.InterfaceAddrs()

    if err != nil {
        fmt.Println(err)
        logger.Println("无法获取本机ip信息",err)
        //os.Exit(1)
    }
    var ips []string
    for _, address := range addrs {
        // 检查ip地址判断是否回环地址
        if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
            if ipnet.IP.To4() != nil {
                ips=append(ips,ipnet.IP.String())
            }
        }
    }
    return ips
}

// checkCacheFile 检查是否存在缓存文件
func checkCacheFile(r *http.Request) (string,error){
    // 判断是否存在
    querys:=r.URL.Query()
    args:=querys.Get("x")+querys.Get("y")+querys.Get("z")+querys.Get("ak")+querys.Get("styles")
    js:=matchFile(md5V(args))
    //检查缓存文件
    if isExist(js){
        f,err:=os.Open(js)
        defer f.Close()
        fi,_:=f.Stat()
        // 文件过小不缓存
        if err==nil &&fi.Size() > 10240{
            // 缓存文件
            return js,nil
        }
    }
    return js,errors.New("未找到缓存")
}

// proxyRequestHandler 具体处理请求
func proxyRequestHandler(w http.ResponseWriter, r *http.Request) {
    host:=r.Host
    requestLocal:=false
    //检查是否本地IP
    for _,localIP:=range localIPs{
        if host==localIP{
            requestLocal=true
            break
        }
    }
    // 处理本地IP情况
    if requestLocal {
        w.WriteHeader(400)
        w.Write([]byte("错误的请求,请检查"))
        return
    }
    //检查缓存文件是否存在
    f,err:=checkCacheFile(r)
    if err!=nil{
        // 反向代理
        executeProxy(w,r)
    }else{
        //使用缓存
        executeCache(f,w,r)
    }
}

// executeProxy 执行代理
func executeProxy(w http.ResponseWriter, r *http.Request){
    //判断请求的协议
    scheme := "http://"
    if r.TLS != nil {
        scheme = "https://"
    }
    // 从代理缓存池中 抓取请求代理
    serverProxy := domain[host]
    //如果未获取到
    if serverProxy==nil{
        //构建反向代理
        proxy,err:=NewProxy(scheme+host)
        //如果创建成功则放入缓存 TODO 多线程情况
        if err==nil{
            domain[host]=proxy
            serverProxy=proxy
        }else{
            w.WriteHeader(400)
            w.Write([]byte("错误的请求,请检查"))
            return
        }
    }
    serverProxy.ServeHTTP(w, r)
}

// executeCache 执行缓存
func executeCache(f string,w http.ResponseWriter, r *http.Request){
    file,_:=os.Open(f)
    buff,err2:=ioutil.ReadAll(file)
    if err2==nil{
        temr := bufio.NewReader(bytes.NewReader(buff))
        resp, err3 := http.ReadResponse(temr, nil)
        if err3 == nil{
            for k,vv:=range resp.Header{
                for _,v:=range vv{
                    w.Header().Add(k,v)
                }
            }
            bufio.NewReader(resp.Body).WriteTo(w)
            cacheCount++
            logger.Println("缓存击中一次",f,"缓存击中共计次数:",cacheCount)
            if cacheCount>1000000{
                cacheCount=0
                logger.Println("缓存击中超过:1000000 重置为0")
            }
        }else{
            w.WriteHeader(400)
            w.Write([]byte(err3.Error()))
        }
    }else{
        // 使用缓存
        executeProxy(w,r)
    }
}

// mkFile 创建文件
func mkFile(file string) (*os.File, error) {
    exist, err := PathExists(file)
    if err != nil {
        fmt.Println(err.Error())
        return nil, err
    } else {
        if exist {
            //fmt.Println(dir + "文件夹已存在!")
            return os.OpenFile(file, os.O_RDWR|os.O_APPEND, 0600)
        } else {
            // 文件夹名称,权限
            file, err := os.Create(file)
            if err != nil {
                logger.Println("文件夹创建失败", err.Error())
            }
            return file, nil
        }
    }
    return nil, nil
}

func main() {
    //创建日志文件
    file, err := mkFile("httpProxy.log")
    if err != nil {
        log.Fatalln("fail to create budget.log file!")
    }
    logger = log.New(file, "", log.Llongfile)
    logger.SetFlags(log.LstdFlags)
    // 初始化代理计数
    proxyCount=0
    flag.IntVar(&port, "P", 80, "端口号,默认为80")
    flag.StringVar(&host, "H", "0.0.0.0", "主机地址,默认为0.0.0.0")
    flag.StringVar(&internetIP, "I", "localhost", "主机公网地址,默认为")
    flag.Parse()
    // 获取本地IP
    localIPs=getLocalIps()
    localIPs=append(localIPs,internetIP)
    // 创建反向代理缓存
    domain=make(map[string]*httputil.ReverseProxy)
    // 创建http 服务
    http.HandleFunc("/", proxyRequestHandler)
    hostStr:=host+":"+strconv.Itoa(port)
    logger.Println("启动host",hostStr)
    // 启动http 服务
    logger.Fatal(http.ListenAndServe(hostStr, nil))
}
发表评论

电子邮件地址不会被公开。 必填项已用*标注