一. 简介

本文将介绍 Go 语言中的 SectionReader,包括 SectionReader的基本使用方法、实现原理、使用注意事项。从而能够在合适的场景下,更好得使用SectionReader类型,提升程序的性能。

二. 问题引入

这里我们需要实现一个基本的HTTP文件服务器功能,可以处理客户端的HTTP请求来读取指定文件,并根据请求的Range头部字段返回文件的部分数据或整个文件数据。

这里一个简单的思路,可以先把整个文件的数据加载到内存中,然后再根据请求指定的范围,截取对应的数据返回回去即可。下面提供一个代码示例:

func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
// 打开文件
file, _ := os.Open(filePath)
defer file.Close() // 读取整个文件数据
fileData, err := ioutil.ReadAll(file)
if err != nil {
// 错误处理
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} // 根据Range头部字段解析请求的范围
rangeHeader := r.Header.Get("Range")
ranges, err := parseRangeHeader(rangeHeader)
if err != nil {
// 错误处理
http.Error(w, err.Error(), http.StatusBadRequest)
return
} // 处理每个范围并返回数据
for _, rng := range ranges {
start := rng.Start
end := rng.End
// 从文件数据中提取范围的字节数据
rangeData := fileData[start : end+1] // 将范围数据写入响应
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
w.Header().Set("Content-Length", strconv.Itoa(len(rangeData)))
w.WriteHeader(http.StatusPartialContent)
w.Write(rangeData)
}
} type Range struct {
Start int
End int
} // 解析HTTP Range请求头
func parseRangeHeader(rangeHeader string) ([]Range, error){}

上述的代码实现比较简单,首先,函数打开filePath指定的文件,使用ioutil.ReadAll函数读取整个文件的数据到fileData中。接下来,从HTTP请求头中Range头部字段中获取范围信息,获取每个范围请求的起始和终止位置。接着,函数遍历每一个范围信息,提取文件数据fileData 中对应范围的字节数据到rangeData中,然后将数据返回回去。基于此,简单实现了一个支持范围请求的HTTP文件服务器。

但是当前实现其实存在一个问题,即在每次请求都会将整个文件加载到内存中,即使用户只需要读取其中一小部分数据,这种处理方式会给内存带来非常大的压力。假如被请求文件的大小是100M,一个32G内存的机器,此时最多只能支持320个并发请求。但是用户每次请求可能只是读取文件的一小部分数据,比如1M,此时将整个文件加载到内存中,往往是一种资源的浪费,同时从磁盘中读取全部数据到内存中,此时性能也较低。

那能不能在处理请求时,HTTP文件服务器只读取请求的那部分数据,而不是加载整个文件的内容,go基础库有对应类型的支持吗?

其实还真有,Go语言中其实存在一个SectionReader的类型,它可以从一个给定的数据源中读取数据的特定片段,而不是读取整个数据源,这个类型在这个场景下使用非常合适。

下面我们先仔细介绍下SectionReader的基本使用方式,然后将其作用到上面文件服务器的实现当中。

三. 基本使用

3.1 基本定义

SectionReader类型的定义如下:

type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}

SectionReader包含了四个字段:

  • r:一个实现了ReaderAt接口的对象,它是数据源。
  • base: 数据源的起始位置,通过设置base字段,可以调整数据源的起始位置。
  • off:读取的起始位置,表示从数据源的哪个偏移量开始读取数据,初始化时一般与base保持一致。
  • limit:数据读取的结束位置,表示读取到哪里结束。

同时还提供了一个构造器方法,用于创建一个SectionReader实例,定义如下:

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
// ... 忽略一些验证逻辑
// remaining 代表数据读取的结束位置,为 base(偏移量) + n(读取字节数)
remaining = n + off
return &SectionReader{r, off, off, remaining}
}

NewSectionReader接收三个参数,r 代表实现了ReadAt接口的数据源,off表示起始位置的偏移量,也就是要从哪里开始读取数据,n代表要读取的字节数。通过NewSectionReader函数,可以很方便得创建出SectionReader对象,然后读取特定范围的数据。

3.2 使用方式

SectionReader 能够像io.Reader一样读取数据,唯一区别是会被限定在指定范围内,只会返回特定范围的数据。

下面通过一个例子来说明SectionReader的使用,代码示例如下:

package main

import (
"fmt"
"io"
"strings"
) func main() {
// 一个实现了 ReadAt 接口的数据源
data := strings.NewReader("Hello,World!") // 创建 SectionReader,读取范围为索引 2 到 9 的字节
// off = 2, 代表从第二个字节开始读取; n = 7, 代表读取7个字节
section := io.NewSectionReader(data, 2, 7)
// 数据读取缓冲区长度为5
buffer := make([]byte, 5)
for {
// 不断读取数据,直到返回io.EOF
n, err := section.Read(buffer)
if err != nil {
if err == io.EOF {
// 已经读取到末尾,退出循环
break
}
fmt.Println("Error:", err)
return
} fmt.Printf("Read %d bytes: %s\n", n, buffer[:n])
}
}

上述函数使用 io.NewSectionReader 创建了一个 SectionReader,指定了开始读取偏移量为 2,读取字节数为 7。这意味着我们将从第三个字节(索引 2)开始读取,读取 7 个字节。

然后我们通过一个无限循环,不断调用Read方法读取数据,直到读取完所有的数据。函数运行结果如下,确实只读取了范围为索引 2 到 9 的字节的内容:

Read 5 bytes: llo,W
Read 2 bytes: or

因此,如果我们只需要读取数据源的某一部分数据,此时可以创建一个SectionReader实例,定义好数据读取的偏移量和数据量之后,之后可以像普通的io.Reader那样读取数据,SectionReader确保只会读取到指定范围的数据。

3.3 使用例子

这里回到上面HTTP文件服务器实现的例子,之前的实现存在一个问题,即每次请求都会读取整个文件的内容,这会代码内存资源的浪费,性能低,响应时间比较长等问题。下面我们使用SectionReader 对其进行优化,实现如下:

func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
// 打开文件
file, err := os.Open(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close() // 获取文件信息
fileInfo, err := file.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} // 根据Range头部字段解析请求的范围
rangeHeader := r.Header.Get("Range")
ranges, err := parseRangeHeader(rangeHeader)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
} // 处理每个范围并返回数据
for _, rng := range ranges {
start := rng.Start
end := rng.End // 根据范围创建SectionReader
section := io.NewSectionReader(file, int64(start), int64(end-start+1)) // 将范围数据写入响应
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
w.WriteHeader(http.StatusPartialContent)
io.CopyN(w, section, section.Size())
}
} type Range struct {
Start int
End int
}
// 解析HTTP Range请求头
func parseRangeHeader(rangeHeader string) ([]Range, error) {}

在上述优化后的实现中,我们使用 io.NewSectionReader 创建了 SectionReader,它的范围是根据请求头中的范围信息计算得出的。然后,我们通过 io.CopyNSectionReader 中的数据直接拷贝到响应的 http.ResponseWriter 中。

上述两个HTTP文件服务器实现的区别,只在于读取特定范围数据方式,前一种方式是将整个文件加载到内存中,再截取特定范围的数据;而后者则是通过使用 SectionReader,我们避免了一次性读取整个文件数据,并且只读取请求范围内的数据。这种优化能够更高效地处理大文件或处理大量并发请求的场景,节省了内存和处理时间。

四. 实现原理

4.1 设计初衷

SectionReader的设计初衷,在于提供一种简洁,灵活的方式来读取数据源的特定部分。

4.2 基本原理

SectionReader 结构体中offbaselimit字段是实现只读取数据源特定部分数据功能的重要变量。

type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}

由于SectionReader需要保证只读取特定范围的数据,故需要保存开始位置和结束位置的值。这里是通过baselimit这两个字段来实现的,base记录了数据读取的开始位置,limit记录了数据读取的结束位置。

通过设定baselimit两个字段的值,限制了能够被读取数据的范围。之后需要开始读取数据,有可能这部分待读取的数据不会被一次性读完,此时便需要一个字段来说明接下来要从哪一个字节继续读取下去,因此SectionReader也设置了off字段的值,这个代表着下一个带读取数据的位置。

在使用SectionReader读取数据的过程中,通过baselimit限制了读取数据的范围,off则不断修改,指向下一个带读取的字节。

4.3 代码实现

4.3.1 Read方法说明

func (s *SectionReader) Read(p []byte) (n int, err error) {
// s.off: 将被读取数据的下标
// s.limit: 指定读取范围的最后一个字节,这里应该保证s.base <= s.off
if s.off >= s.limit {
return 0, EOF
}
// s.limit - s.off: 还剩下多少数据未被读取
if max := s.limit - s.off; int64(len(p)) > max {
p = p[0:max]
}
// 调用 ReadAt 方法读取数据
n, err = s.r.ReadAt(p, s.off)
// 指向下一个待被读取的字节
s.off += int64(n)
return
}

SectionReader实现了Read 方法,通过该方法能够实现指定范围数据的读取,在内部实现中,通过两个限制来保证只会读取到指定范围的数据,具体限制如下:

  • 通过保证 off 不大于 limit 字段的值,保证不会读取超过指定范围的数据
  • 在调用ReadAt方法时,保证传入切片长度不大于剩余可读数据长度

通过这两个限制,保证了用户只要设定好了数据开始读取偏移量 base 和 数据读取结束偏移量 limit字段值,Read方法便只会读取这个范围的数据。

4.3.2 ReadAt 方法说明

func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
// off: 参数指定了偏移字节数,为一个相对数值
// s.limit - s.base >= off: 保证不会越界
if off < 0 || off >= s.limit-s.base {
return 0, EOF
}
// off + base: 获取绝对的偏移量
off += s.base
// 确保传入字节数组长度 不超过 剩余读取数据范围
if max := s.limit - off; int64(len(p)) > max {
p = p[0:max]
// 调用ReadAt 方法读取数据
n, err = s.r.ReadAt(p, off)
if err == nil {
err = EOF
}
return n, err
}
return s.r.ReadAt(p, off)
}

SectionReader还提供了ReadAt方法,能够指定偏移量处实现数据读取。它根据传入的偏移量off字段的值,计算出实际的偏移量,并调用底层源的ReadAt方法进行读取操作,在这个过程中,也保证了读取数据范围不会超过baselimit字段指定的数据范围。

这个方法提供了一种灵活的方式,能够在限定的数据范围内,随意指定偏移量来读取数据,不过需要注意的是,该方法并不会影响实例中off字段的值。

4.3.3 Seek 方法说明

func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
switch whence {
default:
return 0, errWhence
case SeekStart:
// s.off = s.base + offset
offset += s.base
case SeekCurrent:
// s.off = s.off + offset
offset += s.off
case SeekEnd:
// s.off = s.limit + offset
offset += s.limit
}
// 检查
if offset < s.base {
return 0, errOffset
}
s.off = offset
return offset - s.base, nil
}

SectionReader也提供了Seek方法,给其提供了随机访问和灵活读取数据的能力。举个例子,假如已经调用Read方法读取了一部分数据,但是想要重新读取该数据,此时便可以使Seek方法将off字段设置回之前的位置,然后再次调用Read方法进行读取。

五. 使用注意事项

5.1 注意off值在base和limit之间

当使用 SectionReader 创建实例时,确保 off 值在 baselimit 之间是至关重要的。保证 off 值在 baselimit 之间的好处是确保读取操作在有效的数据范围内进行,避免读取错误或超出范围的访问。如果 off 值小于 base 或大于等于 limit,读取操作可能会导致错误或返回 EOF。

一个良好的实践方式是使用 NewSectionReader 函数来创建 SectionReader 实例。NewSectionReader 函数会检查 off 值是否在有效范围内,并自动调整 off 值,以确保它在 baselimit 之间。

5.2 及时关闭底层数据源

当使用SectionReader时,如果没有及时关闭底层数据源可能会导致资源泄露,这些资源在程序执行期间将一直保持打开状态,直到程序终止。在处理大量请求或长时间运行的情况下,可能会耗尽系统的资源。

下面是一个示例,展示了没有关闭SectionReader底层数据源可能引发的问题:

func main() {
file, err := os.Open("data.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close() section := io.NewSectionReader(file, 10, 20) buffer := make([]byte, 10)
_, err = section.Read(buffer)
if err != nil {
log.Fatal(err)
} // 没有关闭底层数据源,可能导致资源泄露或其他问题
}

在上述示例中,底层数据源是一个文件。在程序结束时,没有显式调用file.Close()来关闭文件句柄,这将导致文件资源一直保持打开状态,直到程序终止。这可能导致其他进程无法访问该文件或其他与文件相关的问题。

因此,在使用SectionReader时,要注意及时关闭底层数据源,以确保资源的正确管理和避免潜在的问题。

六. 总结

本文主要对SectionReader进行了介绍。文章首先从一个基本HTTP文件服务器的功能实现出发,解释了该实现存在内存资源浪费,并发性能低等问题,从而引出了SectionReader

接下来介绍了SectionReader的基本定义,以及其基本使用方法,最后使用SectionReader对上述HTTP文件服务器进行优化。接着还详细讲述了SectionReader的实现原理,从而能够更好得理解和使用SectionReader

最后,讲解了SectionReader的使用注意事项,如需要及时关闭底层数据源等。基于此完成了SectionReader的介绍。

提升性能的利器:深入解析SectionReader的更多相关文章

  1. Oracle ADDM性能诊断利器及报告解读

    性能优化是一个永恒的话题,性能优化也是最具有价值,最值得花费精力深入研究的一个课题,因为资源是有限的,时间是有限的.在Oracle数据库中,随着Oracle功能的不断强大和完善,Oralce数据库在性 ...

  2. 通过jdbc使用PreparedStatement,提升性能,防止sql注入

    为什么要使用PreparedStatement? 一.通过PreparedStatement提升性能 Statement主要用于执行静态SQL语句,即内容固定不变的SQL语句.Statement每执行 ...

  3. atitit.提升性能AppCache

    atitit.提升性能AppCache 1.1. 起源1 2. 离线存储2 3. AppCache2 3.1. Appcache事件点如图2 3.2. Manifest文件4 3.3. 自动化工具4 ...

  4. Go语言性能剖析利器--pprof实战

    作者:耿宗杰 前言 关于pprof的文章在网上已是汗牛充栋,却是千篇一律的命令介绍,鲜有真正实操的,本文将参考Go社区资料,结合自己的经验,实战Go程序的性能分析与优化过程. 优化思路 首先说一下性能 ...

  5. SQL Server中使用Check约束提升性能

        在SQL Server中,SQL语句的执行是依赖查询优化器生成的执行计划,而执行计划的好坏直接关乎执行性能.     在查询优化器生成执行计划过程中,需要参考元数据来尽可能生成高效的执行计划, ...

  6. paip.提升性能--多核cpu中的java/.net/php/c++编程

    paip.提升性能--多核cpu中的java/.net/php/c++编程 作者Attilax  艾龙,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:http ...

  7. paip. 提升性能---hibernate的缓存使用 总结

    paip. 提升性能---hibernate的缓存使用 总结 作者Attilax  艾龙,  EMAIL:1466519819@qq.com  来源:attilax的专栏 地址:http://blog ...

  8. Android ViewPager Fragment使用懒加载提升性能

     Android ViewPager Fragment使用懒加载提升性能 Fragment在如今的Android开发中越来越普遍,但是当ViewPager结合Fragment时候,由于Androi ...

  9. paip.提升性能---mysql 优化cpu多核以及lan性能的关系.

    paip.提升性能---mysql 优化cpu多核以及lan性能的关系. 作者Attilax  艾龙,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http:/ ...

  10. paip.提升性能---mysql 性能 测试以及 参数调整.txt

    paip.提升性能---mysql 性能 测试以及 参数调整.txt 作者Attilax  艾龙,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://b ...

随机推荐

  1. Redis 源码解析之通用双向链表(adlist)

    Redis 源码解析之通用双向链表(adlist) 概述 Redis源码中广泛使用 adlist(A generic doubly linked list),作为一种通用的双向链表,用于简单的数据集合 ...

  2. ES_ChatGPT问答

    Q1:springboot项目,如何使用elasticsearch的api增删改查?查询中有哪些方式,如果模糊查询.排序查询.分页查询?分别阐述下这些查询方式的用法?最后举一个完整的例子 答: 在Sp ...

  3. ARouter源码分析

    源码看过好几遍了,但是总是会忘记,特此记录下 先从注解处理器开始 BaseProcessor是其他三个注解处理器的抽象类,子类去实现process方法.在其中的init方法中会获取我们的module模 ...

  4. STM32启动分析之main函数是怎样跑起来的

    1.MDK目标文件 1)MDK中C程序编译后的结果,即可执行文件数据分类: RAM ZI bss 存储未初始化的或初始化为0的全局变量和静态变量 heap 堆,系统malloc和free操作的内存 s ...

  5. 手动编写Swagger文档与部署指南

    Swagger介绍 在Web开发中,后端开发者在完成接口开发后,需要给前端相应的接口使用说明,所以一般会写一份API文档.一般来说,有两种方式提供API接口文档,一种是利用插件在代码中自动生成,另一种 ...

  6. 突破传统监测模式:业务状态监控HM的新思路

    作者:京东保险 管顺利 一.传统监控系统的盲区,如何打造业务状态监控. 在系统架构设计中非常重要的一环是要做数据监控和数据最终一致性,关于一致性的补偿,已经由算法部的大佬总结过就不在赘述.这里主要讲如 ...

  7. Java方法的返回值及注意事项

    方法的返回值 为什么要有带返回值的方法呢? 调用处拿到方法的结果之后,才能根据结果进行下一步操作 带返回值方法的定义和调用: 如果在调用处,要根据方法的结果去编写另一段代码逻辑 为了在调用处拿到方法产 ...

  8. awk判断整除(包含小数和负数)

    awk判断整除常用的方法是用内置的int或者求余数的算符% 被整数整除 输出0-100之间能被9整除的整数 使用 num/9==int(num/9) 的判断方法可以很好实现. awk 'BEGIN{ ...

  9. 使用select需要注意的细节

    使用select需要注意的细节 在学校的时候就使用过select,但是在项目中使用的时候却犯了个错误. select如何使用就不进行总结了,网上教程太多,以下是项目中我写的一小段代码,便于总结. in ...

  10. 基于ChatGPT用AI实现自然对话

    1.概述 ChatGPT是当前自然语言处理领域的重要进展之一,通过预训练和微调的方式,ChatGPT可以生成高质量的文本,可应用于多种场景,如智能客服.聊天机器人.语音助手等.本文将详细介绍ChatG ...