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

2 回复

我会读取数据块而不是单个字节,并使用 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)
}

这个实现提供了:

  1. 缓冲读取减少系统调用
  2. 在找到ETX时正确返回EOF
  3. 可重置以处理多个数据包
  4. 保持与标准io.Reader接口的兼容性

性能对比:原始实现每个字节都需要一次系统调用,而改进版本每次读取4KB数据,系统调用次数减少约4000倍。

回到顶部