本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
目前公司框架准备开源自研的go微服务框架,而HTTP模块则是用的业界比较成熟的echo框架,考虑到后期框架的使用者会使用HTTP协议访问GRPC服务,本文章会详细对这块的设计以及实现做详细说明.
如何实现
- 入口:
// 正常往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中间件的要求