使用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
中获取数据到。