提升性能的利器:深入解析SectionReader
一. 简介
本文将介绍 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.CopyN 将 SectionReader 中的数据直接拷贝到响应的 http.ResponseWriter 中。
上述两个HTTP文件服务器实现的区别,只在于读取特定范围数据方式,前一种方式是将整个文件加载到内存中,再截取特定范围的数据;而后者则是通过使用 SectionReader,我们避免了一次性读取整个文件数据,并且只读取请求范围内的数据。这种优化能够更高效地处理大文件或处理大量并发请求的场景,节省了内存和处理时间。
四. 实现原理
4.1 设计初衷
SectionReader的设计初衷,在于提供一种简洁,灵活的方式来读取数据源的特定部分。
4.2 基本原理
SectionReader 结构体中off,base,limit字段是实现只读取数据源特定部分数据功能的重要变量。
type SectionReader struct {
r ReaderAt
base int64
off int64
limit int64
}
由于SectionReader需要保证只读取特定范围的数据,故需要保存开始位置和结束位置的值。这里是通过base和limit这两个字段来实现的,base记录了数据读取的开始位置,limit记录了数据读取的结束位置。
通过设定base和limit两个字段的值,限制了能够被读取数据的范围。之后需要开始读取数据,有可能这部分待读取的数据不会被一次性读完,此时便需要一个字段来说明接下来要从哪一个字节继续读取下去,因此SectionReader也设置了off字段的值,这个代表着下一个带读取数据的位置。
在使用SectionReader读取数据的过程中,通过base和limit限制了读取数据的范围,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方法进行读取操作,在这个过程中,也保证了读取数据范围不会超过base和limit字段指定的数据范围。
这个方法提供了一种灵活的方式,能够在限定的数据范围内,随意指定偏移量来读取数据,不过需要注意的是,该方法并不会影响实例中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 值在 base 和 limit 之间是至关重要的。保证 off 值在 base 和 limit 之间的好处是确保读取操作在有效的数据范围内进行,避免读取错误或超出范围的访问。如果 off 值小于 base 或大于等于 limit,读取操作可能会导致错误或返回 EOF。
一个良好的实践方式是使用 NewSectionReader 函数来创建 SectionReader 实例。NewSectionReader 函数会检查 off 值是否在有效范围内,并自动调整 off 值,以确保它在 base 和 limit 之间。
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的更多相关文章
- Oracle ADDM性能诊断利器及报告解读
性能优化是一个永恒的话题,性能优化也是最具有价值,最值得花费精力深入研究的一个课题,因为资源是有限的,时间是有限的.在Oracle数据库中,随着Oracle功能的不断强大和完善,Oralce数据库在性 ...
- 通过jdbc使用PreparedStatement,提升性能,防止sql注入
为什么要使用PreparedStatement? 一.通过PreparedStatement提升性能 Statement主要用于执行静态SQL语句,即内容固定不变的SQL语句.Statement每执行 ...
- atitit.提升性能AppCache
atitit.提升性能AppCache 1.1. 起源1 2. 离线存储2 3. AppCache2 3.1. Appcache事件点如图2 3.2. Manifest文件4 3.3. 自动化工具4 ...
- Go语言性能剖析利器--pprof实战
作者:耿宗杰 前言 关于pprof的文章在网上已是汗牛充栋,却是千篇一律的命令介绍,鲜有真正实操的,本文将参考Go社区资料,结合自己的经验,实战Go程序的性能分析与优化过程. 优化思路 首先说一下性能 ...
- SQL Server中使用Check约束提升性能
在SQL Server中,SQL语句的执行是依赖查询优化器生成的执行计划,而执行计划的好坏直接关乎执行性能. 在查询优化器生成执行计划过程中,需要参考元数据来尽可能生成高效的执行计划, ...
- paip.提升性能--多核cpu中的java/.net/php/c++编程
paip.提升性能--多核cpu中的java/.net/php/c++编程 作者Attilax 艾龙, EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http ...
- paip. 提升性能---hibernate的缓存使用 总结
paip. 提升性能---hibernate的缓存使用 总结 作者Attilax 艾龙, EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog ...
- Android ViewPager Fragment使用懒加载提升性能
Android ViewPager Fragment使用懒加载提升性能 Fragment在如今的Android开发中越来越普遍,但是当ViewPager结合Fragment时候,由于Androi ...
- paip.提升性能---mysql 优化cpu多核以及lan性能的关系.
paip.提升性能---mysql 优化cpu多核以及lan性能的关系. 作者Attilax 艾龙, EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http:/ ...
- paip.提升性能---mysql 性能 测试以及 参数调整.txt
paip.提升性能---mysql 性能 测试以及 参数调整.txt 作者Attilax 艾龙, EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://b ...
随机推荐
- pandas之去重
"去重"通过字面意思不难理解,就是删除重复的数据.在一个数据集中,找出重复的数据删并将其删除,最终只保存一个唯一存在的数据项,这就是数据去重的整个过程.删除重复数据是数据分析中经常 ...
- 【Note】(坑)一些组合恒等式的实际意义理解(和待填坑的组合数学知识)
目录 排列组合 恒等式 (1) \(C_n^m=C_n^{n-m}\) (2) \(A_n^m+mA_n^{m-1}=A_{n+1}^m\) (3) \(C_n^{m-1}+C_n^{m}=C_{n+ ...
- 详解事务模式和Lua脚本,带你吃透Redis 事务
摘要:Redis事务包含两种模式:事务模式和Lua脚本. 本文分享自华为云社区<一文讲透 Redis 事务>,作者: 勇哥java实战分享. 准确的讲,Redis事务包含两种模式:事务模式 ...
- 迁移学习《Cluster-Guided Semi-Supervised Domain Adaptation for Imbalanced Medical Image Classification》
论文信息 论文标题:Cluster-Guided Semi-Supervised Domain Adaptation for Imbalanced Medical Image Classificati ...
- 详解C++中的extern与static关键字
本章通过问答方式明晰两个关键字及其作用. Q1:对于int x:,不加extern关键字他就是个未赋初值的定义,但是如果加了static或者extern都可以表示这仅是一个声明吗? A:不是的,具体情 ...
- Vue上传XLS文件
data() { return { tableData:[] } }, //原生JS+XLSX包实现 xls文件上传 methods: { //上传excel表格文件 uploadHandler(da ...
- 香,一套逻辑轻松且智能解决PyQt中控件数值验证的问题
在PyQt开发中,时常需要对控件的值进行校验,如需要校验QCheckBox是否被选中,QLabel是否校验值是否为空等等.在复杂的业务场景下,这类控件如果数量很多,逐个校验就显得麻烦,需要一一获得控件 ...
- 33-module
const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module. ...
- Linux grep命令详细教程
[本文出自天外归云的博客园] 简介 Linux grep命令是一种非常常用的文本搜索工具,它可以在给定的文件中搜索匹配的字符串,并输出匹配的行.grep是全称"global search r ...
- 快速上手Linux核心命令(八):网络相关命令
目录 前言 测试主机之间网络是否联通 ifconfig 配置或显示网络信息 route 显示或管理路由表 netstat 查看网络状况 telnet 远程登录主机 ssh 安全的远程登录主机 wget ...