我正在尝试学习Go,并发现一个不错的小项目将是放在Web服务器前面的A/B测试代理。我几乎不知道Go实质上提供了开箱即用的反向代理,因此设置很容易。我已经到了代理流量的地步,但实际上,我无法实现实际的功能,因为无论我在哪里访问响应,我都无法访问分配的A/B测试变体:

  • handleFunc中,我为请求分配了每个测试的变体,因此上游服务器也可以意识到这一点,并在后端的实现中使用if。
  • 我正在设置所有测试和变体的cookie,包括在代理到上游的请求以及返回给客户端的响应上。
  • 由查找/替换对组成的测试将在响应从上游服务器返回后在响应主体上进行突变。
  • 我正在尝试使用modifyResponsehttputil.ReverseProxy函数进行响应突变。

  • 问题是我不知道如何在不更改上游服务器的情况下共享handleFuncmodifyResponse之间分配的变体。我希望能够共享此上下文(基本上是某种程度上的map[string]string

    代码示例:

    这是我的代码的简化版本,基本上我的问题是,modifyRequest如何知道handleFunc中发生的随机分配?

    package main
    
    import (
        config2 "ab-proxy/config"
        "bytes"
        "fmt"
        "io/ioutil"
        "net/http"
        "net/http/httputil"
        "net/url"
        "strconv"
        "strings"
    )
    
    var config config2.ProxyConfig
    var reverseProxy *httputil.ReverseProxy
    var tests config2.Tests
    
    func overwriteCookie(req *http.Request, cookie *http.Cookie) {
        // omitted for brevity, will replace a cookie header, instead of adding a second value
    }
    
    func parseRequestCookiesToAssignedTests(req *http.Request) map[string]string {
        // omitted for brevity, builds a map where the key is the identifier of the test, the value the assigned variant
    }
    
    func renderCookieForAssignedTests(assignedTests map[string]string) string {
        // omitted for brevity, builds a cookie string
    }
    
    func main () {
        var err error
    
        if  config, err = config2.LoadConfig(); err != nil {
            fmt.Println(err)
    
            return
        }
    
        if tests, err = config2.LoadTests(); err != nil {
            fmt.Println(err)
    
            return
        }
    
        upstreamUrl, _ := url.Parse("0.0.0.0:80")
    
        reverseProxy = httputil.NewSingleHostReverseProxy(upstreamUrl)
        reverseProxy.ModifyResponse = modifyResponse
    
        http.HandleFunc("/", handleRequest)
    
        if err := http.ListenAndServe("0.0.0.0:80", nil); err != nil {
            fmt.Println("Could not start proxy")
        }
    }
    
    func handleRequest(res http.ResponseWriter, req *http.Request) {
        assigned := parseRequestCookiesToAssignedTests(req)
    
        newCookies := make(map[string]string)
    
        for _, test := range tests.Entries {
            val, ok := assigned[test.Identifier]
    
            if ok {
                newCookies[test.Identifier] = val
            } else {
                newCookies[test.Identifier] = "not-assigned-yet" // this will be replaced by random variation assignment
            }
        }
    
        testCookie := http.Cookie{Name: config.Cookie.Name, Value: renderCookieForAssignedTests(newCookies)}
    
        // Add cookie to request to be sent to upstream
        overwriteCookie(req, &testCookie)
    
        // Add cookie to response to be returned to client
        http.SetCookie(res, &testCookie)
    
        reverseProxy.ServeHTTP(res, req)
    }
    
    func modifyResponse (response *http.Response) error {
        body, err := ioutil.ReadAll(response.Body)
    
        if err != nil {
            return  err
        }
    
        err = response.Body.Close()
    
        if err != nil {
            return err
        }
    
        response.Body = ioutil.NopCloser(bytes.NewReader(body))
        response.ContentLength = int64(len(body))
        response.Header.Set("Content-Length", strconv.Itoa(len(body)))
    
        return nil
    }
    

    最佳答案

    使用标准的 context.Context 。这可以通过*http.Request在您的处理程序中访问。也可以通过*http.ResponsemodifyResponse参数访问该请求。

    在您的处理程序中:

    ctx := req.Context()
    // Set values, deadlines, etc.
    req = req.WithContext(ctx)
    reverseProxy.ServeHTTP(res, req)
    

    然后在modifyResponse中:
    ctx := response.Request.Context()
    // fetch values, check for cancellation, etc
    

    09-30 14:26