Golang中如何提前停止io.Reader而不读取到io.EOF
Golang中如何提前停止io.Reader而不读取到io.EOF 我正在开发一个自定义网络协议并编写数据包读取器。我想在 net.Conn 上创建一个包装器,以便能够控制 io.Reader 的实现。
在这种情况下,例如我想使用 io.Copy 将数据流写入文件,将 net.Conn(带包装器)作为参数传递。关键在于,当找到 ETX(文本结束)控制字符时,我希望返回 io.EOF(停止 io.Copy)。这样我就可以使用同一个连接发送多个数据包,并控制 io.Reader。
问题是,根据下面的代码,这会是一个性能较差的实现吗?逐个字节读取,直到找到 ETX。
package main
import "io"
type reader struct {
end bool
raw io.Reader
}
func newReader(raw io.Reader) *reader {
return &reader{
end: false,
raw: raw,
}
}
// Read reads up to len(buffer) bytes into buffer. It returns the number of
// bytes read (0 <= sum <= len(buffer)) and any error encountered. After the
// ETX control character is found, it returns the io.EOF error on the next Read
// attempt.
func (rea *reader) Read(buffer []byte) (int, error) {
sum := 0
buf := make([]byte, 1)
for i := 0; i <= len(buffer); i++ {
if rea.end {
return 0, io.EOF
}
if _, err := rea.Read(buf); err != nil {
return 0, err
}
if rea.end = buf[0] == ETX; rea.end {
return sum, nil
}
sum++
copy(buffer, buf)
}
return sum, nil
}
我知道我们有 bufio.Reader,但它依赖于 io.Reader,而这正是我想要控制数据读取的方法,这样我就可以安全地将其与其他方法一起使用。在阅读 bufio.Reader 的实现时,我注意到它使用了一个内部缓冲区,而在上述代码的情况下,我们将直接从 net.Conn 的 io.Reader 读取。
任何帮助和建议都将受到欢迎。
更多关于Golang中如何提前停止io.Reader而不读取到io.EOF的实战教程也可以访问 https://www.itying.com/category-94-b0.html
我会读取数据块而不是单个字节,并使用 bytes.IndexByte 检查接收到的字节中是否存在分隔符。
假设:从文件中读取32字节,通过不同实现方式的操作成本: 逐字节读取:每次读取操作需要1次寻址和1次读取,成本为64 每次16字节:需要1次寻址和1次读取,成本为4
上面的例子并不意味着更大的块大小就能获得最佳性能,实际性能受许多参数影响,比如带宽、网络缓冲区、开放连接等。
更多关于Golang中如何提前停止io.Reader而不读取到io.EOF的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你的实现确实存在性能问题,因为它逐个字节读取数据。对于网络连接这种I/O操作,频繁的小数据读取会导致大量系统调用,显著降低性能。更好的方法是使用缓冲读取,在找到ETX字符时返回已读取的数据和EOF。
以下是改进的实现:
package main
import (
"bytes"
"io"
)
const ETX = 0x03 // 文本结束控制字符
type PacketReader struct {
reader io.Reader
buffer []byte
pos int
foundEOF bool
}
func NewPacketReader(reader io.Reader) *PacketReader {
return &PacketReader{
reader: reader,
buffer: make([]byte, 4096), // 4KB缓冲区
pos: 0,
}
}
func (pr *PacketReader) Read(p []byte) (int, error) {
if pr.foundEOF {
return 0, io.EOF
}
if pr.pos == 0 {
// 缓冲区为空,从底层reader读取数据
n, err := pr.reader.Read(pr.buffer)
if err != nil {
return 0, err
}
pr.buffer = pr.buffer[:n]
}
// 在缓冲区中查找ETX字符
etxIndex := bytes.IndexByte(pr.buffer[pr.pos:], ETX)
if etxIndex >= 0 {
// 找到ETX字符
pr.foundEOF = true
copyLength := etxIndex
if copyLength > len(p) {
copyLength = len(p)
}
copy(p, pr.buffer[pr.pos:pr.pos+copyLength])
pr.pos += copyLength
return copyLength, nil
}
// 没有找到ETX,返回所有可用数据
copyLength := len(pr.buffer) - pr.pos
if copyLength > len(p) {
copyLength = len(p)
}
copy(p, pr.buffer[pr.pos:pr.pos+copyLength])
pr.pos += copyLength
// 如果缓冲区已耗尽,重置位置
if pr.pos == len(pr.buffer) {
pr.pos = 0
}
return copyLength, nil
}
// 重置读取器以处理下一个数据包
func (pr *PacketReader) Reset() {
pr.pos = 0
pr.foundEOF = false
pr.buffer = pr.buffer[:cap(pr.buffer)]
}
使用示例:
func main() {
conn, _ := net.Dial("tcp", "example.com:8080")
packetReader := NewPacketReader(conn)
// 使用io.Copy读取直到ETX
file, _ := os.Create("output.dat")
io.Copy(file, packetReader)
// 重置读取器以读取下一个数据包
packetReader.Reset()
// 读取下一个数据包
buffer := make([]byte, 1024)
n, _ := packetReader.Read(buffer)
fmt.Printf("Read %d bytes\n", n)
}
这个实现提供了:
- 缓冲读取减少系统调用
- 在找到ETX时正确返回EOF
- 可重置以处理多个数据包
- 保持与标准io.Reader接口的兼容性
性能对比:原始实现每个字节都需要一次系统调用,而改进版本每次读取4KB数据,系统调用次数减少约4000倍。

