在本文中,我们将会介绍在 Gin HTTP 框架中,如何能够简单快速的实现 headers 的透传方案。

原理分析

对于任何一个微服务而言,想要实现 headers 的透传的话,也是主要分三步:

  • 接收 HTTP 请求时,从请求中读取中 headers 信息。
  • 将 headers 在程序内部进行保存和传播。
  • 发送 HTTP 请求时,将内部传播的 headers 在客户端请求加入并发送出去。

此外,为了避免在各个请求接收和请求发送中,都需要进行相关的改动,我们在程序实现中应该按照 AOP 的思想,一次改动全场生效。

请求读取

首先,在 Gin 框架中,支持通过 middleware 的中间件方式在接收 http 请求处理的前后增加自定义逻辑。

因此,我们可以通过自定义 middleware 来读取请求头,并自身维护一个 Context 上下游对象,其中记录着对应的请求头信息。

  1. func Middleware() gin.HandlerFunc {
  2. return handler
  3. }
  4. func handler(c *gin.Context) {
  5. headersWithFirst := make(map[string]string, len(c.Request.Header))
  6. for k, v := range c.Request.Header {
  7. if len(v) > 0 {
  8. headersWithFirst[k] = v[0]
  9. }
  10. }
  11. carrier := cp.Extract(headersWithFirst)
  12. if len(carrier) > 0 {
  13. c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), cp.InternalContextKey{}, carrier))
  14. }
  15. c.Next()
  16. }

请求内部传播

由于在读取请求 header 中,我们对原有的 Context 进行了扩展,将 headers 信息记录到了 c.Request.Context() 中。

因此,在仅接着的的 Context 上下文传递中,需要使用 c.Request.Context() 来代替 c 进行上下文传递即可。

例如:

  1. func TestApi(c *gin.Context) {
  2. SelfFunction1(c.Request.Context())
  3. }

请求发送

在 Go 语言发送 http 请求时,默认基于的是 net/http 库,但是 net/http 库本身在发送请求时,无法基于 Context 上下文进行行为定制。
而我们其实是希望在 http 客户端发送请求时,可以从 context 上下文中获取对应的请求头信息并增加到 header 中传递出去。

因此,我们需要使用一个官方提供的http库 golang.org/x/net/context/ctxhttp 来代替原有lib库发送HTTP请求。

示例如下:

  1. resp, err := ctxhttp.Get(c.Request.Context(), client, "http://127.0.0.1:8080/test")

其中,ctxhttp 与 http 的功能基本相同,唯一的差别在于需要主动传递 context 上下文和实例化的 client 对象。

至此为止,我们其实只是传递了对应的上下文到ctxhttp中,但实际上还并没有实现http 客户端发送请求时,
可以从 context 上下文中获取对应的请求头信息并增加到 header 中传递出去。

因此,下面我们需要对 http.Client 对象进行一次扩展,实现可以自动从 context 上下文中获取对应的请求头信息并增加到 header 中传递出去。

示例代码如下:

  1. func WrapClient(c *http.Client) *http.Client {
  2. if c == nil {
  3. c = http.DefaultClient
  4. }
  5. copied := *c
  6. copied.Transport = WrapRoundTripper(copied.Transport)
  7. return &copied
  8. }
  9. func WrapRoundTripper(r http.RoundTripper) http.RoundTripper {
  10. if r == nil {
  11. r = http.DefaultTransport
  12. }
  13. return &roundTripper{rt: r}
  14. }
  15. type roundTripper struct {
  16. rt http.RoundTripper
  17. }
  18. func (s *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
  19. carrier := r.Context().Value(cp.InternalContextKey{})
  20. headers := cp.Inject(carrier)
  21. for k, v := range headers {
  22. r.Header.Set(k, v)
  23. }
  24. return s.rt.RoundTrip(r)
  25. }

在上述代码中,我们定义了一个 WrapClient 函数,它可以对已有的 http.Client 进行扩展,
具体来说,可以从上下文中获取对应的 headers 信息,然后注入到请求发送的 header 体中。

具体到使用中,其实非常简单,只需要对 http.Client 进行一次 Wrap 即可:

  1. package main
  2. import cphttp "github.com/AminoApps/context-propagation-go/module/context-propagation-http"
  3. import "golang.org/x/net/context/ctxhttp"
  4. client := cphttp.WrapClient(&http.Client{})
  5. // Please use the ctxhttp to wrap the request.
  6. resp, err := ctxhttp.Get(ctx, client, "http://127.0.0.1:8080/test")

实战

最后,我们以一个实战的示例来演示对于一个 gin 框架的项目而言,是如何实现 headers 透传的:

  1. package main
  2. import (
  3. cp "github.com/AminoApps/context-propagation-go"
  4. cpgin "github.com/AminoApps/context-propagation-go/module/context-propagation-gin"
  5. cphttp "github.com/AminoApps/context-propagation-go/module/context-propagation-http"
  6. "github.com/gin-gonic/gin"
  7. "golang.org/x/net/context/ctxhttp"
  8. "net/http"
  9. )
  10. func main() {
  11. r := gin.New()
  12. r.Use(cpgin.Middleware())
  13. r.GET("/JSON", func(c *gin.Context) {
  14. value := cp.GetValueFromContext(c.Request.Context(), "easyenv")
  15. println("token: ", value)
  16. client := cphttp.WrapClient(&http.Client{})
  17. resp, err := ctxhttp.Get(c.Request.Context(), client, "http://127.0.0.1:8080/test")
  18. println("resp: ", resp, err)
  19. c.JSON(http.StatusOK, resp)
  20. })
  21. // Listen and serve on 0.0.0.0:8080
  22. r.Run(":8080")
  23. }

相关的实现也可以参考 在 Go 的 Gin WEB 框架中如何实现 headers 透传