Gin-gonic:自定义响应的压缩格式
Gin-gonic:自定义响应的压缩格式
原创 源自开发者 源自开发者
2025年01月02日 15:04 广东 听全文
在HBO 2014年的情景喜剧《硅谷》中,Richard和他的团队承诺开发出一种功能完善的无损压缩协议。无损压缩是一种在不丢失任何数据的情况下大幅减少数据大小的技术,可以说是软件领域中的“室温超导体”。
不过,剧中的一些内容并非完全虚构。如今,浏览器确实支持GNU ZIP(GZIP),它能够显著减小非二进制文件的大小。但我一直在想,是否还有其他压缩格式可以替代Zip?
在本文中,我希望重新探讨一些与好莱坞相关的技术传说,并编写一个中间件函数,用于压缩服务器发出的所有响应。响应将使用7-Zip进行压缩,因为7-Zip相比Zip具有更高的压缩比。
图片
业务问题
本文要解决的假设问题是,我有一个使用Gin Web框架编写的成功的报告生成系统。然而,当我尝试将服务扩展到偏远地区时遇到了障碍。主要的用户抱怨是下载报告耗时过长。
作为一家虚构的科技公司,我无法直接联系电信公司提高这些地区的网络连接速度,也无法为每个人订阅StarLink。在这种情况下,实际可行的解决方案是将数据压缩成一个归档文件,并让客户端在接收后解压缩。
报告生成系统
这个系统用于生成PDF格式的气候报告。它由一个Gin服务器组成,并绑定了一个处理器到路径/pdf。
为了简化说明,我们假设报告是存储在我电脑上的一个静态PDF文件。这里不会深入探讨处理器的代码,而是专注于响应的后处理。以下是返回Gin处理器的工厂函数,该处理器处理报告的下载:
func PDFHandler(filePath string) gin.HandlerFunc {
returnfunc(c *gin.Context) {
// 打开PDF文件
file, err := os.Open(filePath)
if err != nil {
c.String(http.StatusInternalServerError, "无法打开PDF文件: %v", err)
return
}
defer file.Close()
// 设置PDF响应的头部信息
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", "inline; filename=\"download.pdf\"")
// 将文件流写入响应
_, err = io.Copy(c.Writer, file)
if err != nil {
c.String(http.StatusInternalServerError, "无法提供PDF文件: %v", err)
return
}
c.Status(http.StatusOK)
}
}
以下是启动报告系统所需的代码:
func main() {
r := gin.Default()
// 访问PDF的路由
r.GET("/pdf", PDFHandler("./sample.pdf"))
// 假设还有其他类似的数百个端点
// 启动服务器
r.Run(":8080")
}
为了解决“业务问题”部分提到的问题,我需要拦截并压缩响应。这可以通过替换Gin Context的Writer字段为自定义的ResponseWriter来实现。这个自定义的ResponseWriter可以读取响应内容,对其进行压缩并发送给客户端。
以下是我的方法示意图:
图片
方法示意图
我选择这种方法是因为它更容易抽象出所需的行为,并在每个请求结束时运行。其他替代方案包括:
更新每个处理器,在发送数据之前进行压缩。
将代码组织为分层结构,直接更新响应层。
自定义ResponseWriter
为了设置自定义的ResponseWriter,我会更新请求的Gin Context的Writer字段,同时保留原始的ResponseWriter副本。这个副本将用于写入压缩后的数据作为实际响应。
自定义的ResponseWriter需要能够存储响应的状态码和主体内容。此外,它必须满足gin.ResponseWriter接口的要求。
为了简化实现,我会将gin.ResponseWriter接口嵌入到自定义的ResponseWriter中。这样,我可以避免重新实现那些当前PDF处理器代码不会使用的方法。
自定义的ResponseWriter将包含以下三个字段和一个嵌入的接口:
headers:存储后续处理器设置的头部信息。
buffer:存储响应主体,其类型为指向bytes.Buffer的指针。
status:存储响应的状态码。
ResponseWriter:嵌入的gin.ResponseWriter接口,用于减少需要实现的方法数量。
以下是自定义ResponseWriter的Go代码:
// CompressResponseWriter是一个自定义的Writer,
// 模拟了Gin的ResponseWriter但没有实际的I/O操作。
type CompressResponseWriter struct {
headers http.Header
buffer *bytes.Buffer
status int
gin.ResponseWriter // 注意:如果调用未实现的方法会触发错误
}
接下来,我需要添加一些PDF处理器会用到的方法,包括:
Header:返回响应的头部信息。
Write:用于Gin处理器将数据写入响应主体。在CompressResponseWriter中,它会调用buffer字段的Write方法。
WriteHeader:用于Gin处理器写入响应的HTTP状态码。在这里,它仅设置CompressResponseWriter的status字段。
Written:指示是否已经写入了主体内容。在这里,我通过检查buffer字段的大小是否大于零来实现。
以下是gin.ResponseWriter接口的实现:
func (w *CompressResponseWriter) Header() http.Header {
return w.headers
}
func (w *CompressResponseWriter) Write(data []byte) (int, error) {
return w.buffer.Write(data)
}
func (w *CompressResponseWriter) WriteHeader(statusCode int) {
w.status = statusCode
}
func (w *CompressResponseWriter) Status() int {
return w.status
}
func (w *CompressResponseWriter) Written() bool {
return w.buffer.Len() > 0
}
有了自定义的ResponseWriter,接下来就是编写中间件函数。
神奇的中间件
起初,我以为应该在注册所有处理器之后再添加中间件函数,但事实证明这是错误的。中间件函数需要在任何PDF处理器之前注册,因为它需要将原始的响应写入器替换为CompressResponseWriter。
中间件函数将通过一个工厂函数生成。这个工厂函数有一个参数,其类型为CompressFunc。以下是CompressFunc的函数签名:
type CompressFunc func(data []byte) ([]byte, error)
通过使用这个自定义类型,我可以在不修改中间件代码的情况下使用不同的压缩函数。
中间件的第一步是构造一个CompressResponseWriter实例:
func CompressResponse(cf CompressFunc) gin.HandlerFunc {
return func(c *gin.Context) {
buffer := bytes.Buffer{}
compressWriter := &CompressResponseWriter{
headers: http.Header{},
buffer: &buffer,
status: http.StatusOK,
}
}
}
默认情况下,CompressResponseWriter会假设响应的状态码为200(OK)。
接下来,我会保留原始的ResponseWriter副本,并将compressWriter设置为gin.Context的Writer字段:
func CompressResponse(cf CompressFunc) gin.HandlerFunc {
return func(c *gin.Context) {
originalWriter := c.Writer
c.Writer = compressWriter
}
}
然后,我调用gin.Context的Next方法。这个方法会在所有后续的中间件和处理器执行完毕后返回:
func CompressResponse(cf CompressFunc) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
}
}
接下来,我从虚拟主体(即buffer字段)中读取数据,并将其传递给CompressFunc进行压缩。压缩完成后,我会重置原始的ResponseWriter,以避免因未完全实现gin.ResponseWriter接口而导致的崩溃。
为了告知客户端这是一个压缩文件,我会将Content-Type头部设置为application/x-7z-compressed。然后,我将压缩后的字节写入响应。以下是剩余的中间件代码:
func CompressResponse(cf CompressFunc) gin.HandlerFunc {
returnfunc(c *gin.Context) {
result := buffer.Bytes()
fmt.Println("压缩前数据长度:", len(result))
compressed, err := cf(result)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
fmt.Println("压缩后数据长度:", len(compressed))
c.Writer = originalWriter
if compressWriter.status == http.StatusOK {
c.Writer.Header().Set("Content-Type", "application/x-7z-compressed")
}
c.Writer.WriteHeader(compressWriter.status)
c.Writer.Write(compressed)
}
}
整合代码
为了构造CompressResponse中间件,我需要传递一个CompressFunc。
在本文中,我使用了一个名为Compress7zip的函数。这个函数通过调用7-Zip命令行工具来完成压缩操作。完整的函数代码可以在本文的项目仓库中找到(见“来源”部分)。
有了中间件后,我会更新启动Gin服务器的main函数。更新内容包括在注册任何处理器之前注册CompressResponse中间件。以下是更新后的代码:
func main() {
r := gin.Default()
// 在其他处理器之前绑定中间件
r.Use(
CompressResponse(
Compress7zip,
),
)
// 访问PDF的路由
r.GET("/pdf", PDFHandler("./sample.pdf"))
// 假设还有其他类似的数百个端点
// 启动服务器
r.Run(":8080")
}
运行效果如下:
图片
运行效果
结论
经过这一系列工作,我原以为至少能减少5MB的响应大小,但事实证明我错了。不过,与Zip相比,7-Zip确实有一定的优势。我成功地将响应大小减少了约0.688MB。当然,我对7-Zip命令行工具并不熟练,可能还有进一步优化的空间。
除了性能指标外,我撰写本文的目的是展示如何在不修改任何处理器代码的情况下修改请求响应。在我看来,这体现了Go(以及C)的精髓——一切都可以自定义。
本文中介绍的响应后处理方法也可以通过分层代码来解决,甚至完全跳过。我认为没有完美的解决方案,只有权衡取舍。
尝试编写压缩协议后,我终于理解了《硅谷》中那些角色为何总是崩溃。
感谢阅读!
来源
gin-gonic:https://github.com/gin-gonic/gin
项目仓库:https://github.com/cheikhsimsol/7zip
点击关注并扫码添加进交流群
免费领取「Go 语言」学习资料
图片
阅读 192

浙公网安备 33010602011771号