使用kratos自带http transport实现文件下载上传功能,并且可以使用中间件。
文件的下载接口可以使用proto定义,上传目前还未找到完美方法,为了统一,采用不定义在proto中定义,自行在server中实现。
为什么上传接口不能使用proto定义?
上传接口实际上可以使用proto定义,但是在解析入参时是不能解析上传的字节类型,入参的Content-Type类型只支持:x-www-form-urlencoded、json、xml、proto、yaml,而上传文件我希望以只读字节流readCloser的方式,这样可以方便使用io.Copy()的方式实现上传,而不是将文件数据转换成[]byte进行传输
看一段官方的实现方式原代码 · GitHub,已简化。
func downloadFile(ctx http.Context) error {
    f := excelize.NewFile()
    return f.Write(ctx.Response())
}
func main() {
    var opts = []http.ServerOption{
        http.Address(":8001")
    }
    httpSrv := http.NewServer(
        opts...,
    )
    route := httpSrv.Route("/")
    route.POST("/download", downloadFile)
    app := kratos.New(
        kratos.Name("download"),
        kratos.Server(
            httpSrv,
        ),
    )
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}可以看到建议的方式是直接使用httpSrv定义路由,不再使用生成的http代码注册,但是这种方式也导致了中间件逻辑的丢失。
来看另一段可以使用中间件的关键代码原代码 · GitHub,这段代码实际也是kratos根据proto生成http接口的类似代码。
func sayHelloHandler(ctx http.Context) error {
    var in helloworld.HelloRequest
    if err := ctx.BindQuery(&in); err != nil {
        return err
    }
    // binding /hello/{name} to in.Name
    if err := ctx.BindVars(&in); err != nil {
        return err
    }
    http.SetOperation(ctx, "/helloworld.Greeter/SayHello")
    h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
        return &helloworld.HelloReply{Message: "test:" + req.(*helloworld.HelloRequest).Name}, nil
    })
    return ctx.Returns(h(ctx, &in))
}其中下面这一段就是在http接口中使用中间件的核心代码:
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
        return &helloworld.HelloReply{Message: "test:" + req.(*helloworld.HelloRequest).Name}, nil
    })
h(ctx, &in)在ctx.Middleware中包装service层的方法,也是实现一个中间件方法,再使用h(ctx, &in)执行。
而不能使用proto定义的接口的原因也在这里
if err := ctx.BindQuery(&in); err != nil {
    return err
}这三行会解析请求中携带的参数,如果contentType类型不为上述中的类型,那么会造成解析失败,返回错误当前类型未注册。所以在实现上传功能时采用request自带的FormFile("file")方法获取文件流。
将这两种方式结合,下载接口代码示例,上传同理:
func downloadFile(ctx context.Context, _ *http.Request, w http.ResponseWriter) error {
    f := excelize.NewFile()
    return f.Write(w)
    // 或
    // f := io.Open("xxx")
    // _, err = io.Copy(w, f)
}
func downloadWrapper(ctx http.Context) error {  
    h := ctx.Middleware(func(cc context.Context, req interface{}) (interface{}, error) {  
       return nil, downloadFile(cc, ctx.Request(), ctx.Response())  
    })  
    _, err = h(ctx, nil)  
    if err != nil {  
       return err  
    }  
    return nil  
}
func main() {
    var opts = []http.ServerOption{
        http.Address(":8001")
        http.Middleware(// 不要忘记注册中间件
            recovery.Recovery(),  
            logging.Server(logger),
        )
    }
    httpSrv := http.NewServer(
        opts...,
    )
    route := httpSrv.Route("/")
    route.Get("/download", downloadWrapper)
    app := kratos.New(
        kratos.Name("download"),
        kratos.Server(
            httpSrv,
        ),
    )
    if err := app.Run(); err != nil {
        log.Fatal(err)
    }
}在进行路由注册时,不再直接注册处理方法,而是注册route.POST("/download", downloadWrapper),由downloadWrapper内部使用中间件后调用downloadFile方法。
目前我是将main方法中注册路由部分的代码放在server层,将downloadWrapper方法放在service层,downloadFile放在biz层。
如果希望读取到参数,如果是下载接口的话,是可以使用
if err := ctx.BindVars(&in); err != nil {
    return err
}来获取请求数据的,wrapper方法中获取数据后,在downloadFile增加一个入参传过去就可以了,当然也可以直接从request读取。
目前上传没有什么太优雅的方式获取到上传文件流,只能用request中获取数据到。