go-echo实现对gprc的代理

385 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

目前公司框架准备开源自研的go微服务框架,而HTTP模块则是用的业界比较成熟的echo框架,考虑到后期框架的使用者会使用HTTP协议访问GRPC服务,本文章会详细对这块的设计以及实现做详细说明.

如何实现

  1. 入口:
   // 正常往ECHO中注册路由
	echo.GET("/ping", func(ctx echo.Context) error {
		return ctx.JSON(200, "pong")
	})
	echo.GET("/hehe", func(context echo.Context) error {
		return context.JSON(200,"hello")
	})
	 // GRPC SERVER
	g := greeter.Greeter{}
	// 通过HTTP请求代理GRPC服务
	// xecho.GRPCProxyWrapper():核心方法 如何代理都是根据该方法实现
	//下文会做详细讲解
	echo.GET("/grpc",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())
	echo.POST("/grpc-post",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())

2.GRPCProxyWrapper:核心方法

//h:grpc 服务引用
func GRPCProxyWrapper(h interface{}) echo.HandlerFunc {
	// 利用反射判断传递的参数是否为函数类型
	// 下方的回调函数中会反射调用该方法
	t := reflect.TypeOf(h)
	if t.Kind() != reflect.Func {
		panic("reflect error: handler must be func")
	}
	// 该闭包返回需要满足echo的要求(具体要求:后面会讲解)
	return func(c echo.Context) error {
	    //关键步骤一:反射获取GRPC函数参数,并将参数值进行绑定
		var req = reflect.New(t.In(1).Elem()).Interface()
		if err := c.Bind(req); err != nil {
			return ProtoError(c, StatusBadRequest, errBadRequest)
		}
		var md = metadata.MD{}
		// append Header
		for k, vs := range c.Request().Header {
			for _, v := range vs {
				bs := bytes.TrimFunc([]byte(v), func(r rune) bool {
					return r == '\n' || r == '\r' || r == '\000'
				})
				md.Append(k, string(bs))
			}
		}
		ctx := metadata.NewOutgoingContext(context.TODO(), md)
		var inj = inject.New()
		inj.Map(ctx)
		inj.Map(req)
		// 关键步骤二: 执行具体的 GRPC 方法
		vs, err := inj.Invoke(h)
		if err != nil {
			return ProtoError(c, StatusInternalServerError, errMicroInvoke)
		}
		if len(vs) != 2 {
			return ProtoError(c, StatusInternalServerError, errMicroInvokeLen)
		}
		repV, errV := vs[0], vs[1]
		if !errV.IsNil() || repV.IsNil() {
			if e, ok := errV.Interface().(error); ok {
				// error logic
				return ProtoError(c, StatusOK, e)
			}
			return ProtoError(c, StatusInternalServerError, errMicroInvokeInvalid)
		}
		if !repV.IsValid() {
			return ProtoError(c, StatusInternalServerError, errMicroResInvalid)
		}
		// 发送带有状态代码和数据的Protobuf JSON响应
		return ProtoJSON(c, StatusOK, repV.Interface())
	}
}

3.ProtoJSON()

func ProtoJSON(c echo.Context, code int, i interface{}) error {
	var acceptEncoding = c.Request().Header.Get(HeaderAcceptEncoding)
	var ok bool
	var m proto.Message
	if m, ok = i.(proto.Message); !ok {
		c.Response().Header().Set(HeaderHRPCErr, "true")
		m = statusMSDefault
	}
	// protobuf output
	if strings.Contains(acceptEncoding, MIMEApplicationProtobuf) {
		c.Response().Header().Set(HeaderContentType, MIMEApplicationProtobuf)
		c.Response().WriteHeader(code)
		bs, _ := proto.Marshal(m)
		_, err := c.Response().Write(bs)
		return err
	}
	// json output
	c.Response().Header().Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8)
	c.Response().WriteHeader(code)
	return jsonpbMarshaler.Marshal(c.Response().Writer, m)
}

4.以上基本上简单的实现了上述功能,具体细节由于时间问题描述的不是很完善,各位有问题的话,欢迎下方评论.

echo路由注册解析

func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
	return e.Add(http.MethodGet, path, h, m...)
}

截取了一段Echo中GET方法 参数:h HandlerFunc

	// HandlerFunc defines a function to serve HTTP requests.
	HandlerFunc func(Context) error

该参数是一个回调函数,当路由注册后,相对应的请求会由该回调进行处理,GRPCProxyWrapper 中也是基于该回调进行实现。 参数:m ...MiddlewareFunc

	// MiddlewareFunc defines a function to process middleware.
	MiddlewareFunc func(HandlerFunc) HandlerFunc

根据该参数我们可以传递相关的中间件,比如在最开始的时候我们在调用方法时,就传递了自定义的中间件(xecho.AccessLogger)

echo.POST("/grpc-post",xecho.GRPCProxyWrapper(g.SayHello),xecho.AccessLogger())

xecho.AccessLogger()代码:

func AccessLogger() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(ctx echo.Context) (err error) {
			err = next(ctx)
			if trace, ok := xlog.ExtractTraceMD(ctx); ok {
				trace.Info(zap.String("method", ctx.Request().Method))
				trace.Info(zap.Int("code", ctx.Response().Status))
				trace.Info(zap.String("host", ctx.Request().Host))

				if cost := time.Since(trace.BeginTime).Milliseconds(); cost > 500 {
					trace.Warn(zap.Int64("slow", cost))
				}
			}
			return err
		}
	}
}

各位可以根据自己的需求,比如说进行采样,打点等传递自己定义的中间件,当然要满足ECHO中间件的要求