Golang中net.Conn读取数据卡顿或提前中断问题解析

Golang中net.Conn读取数据卡顿或提前中断问题解析 你好。我算是Go语言的新手,目前遇到了一个关于网络连接挂起或无法从网络连接中读取完整消息的问题。我正在尝试通过TCP连接实现一个简单的Redis。

以下是我尝试从网络连接读取信息的示例:

const connChunkSize int = 1024

func readConnectionMessage(conn net.Conn) string {
	buffer := bytes.NewBuffer(nil)
	for {
		chunk := make([]byte, connChunkSize)
		read, err := conn.Read(chunk)

		if read > 0 {
			buffer.Write(chunk[:read])
		}

		if read == 0 || errors.Is(err, io.EOF) {
			break
		} else if err != nil {
			return err.Error()
		}
	}

	return buffer.String()
}

在第一次循环中,我收到了“$1\n$4\nPING”,而在第二次循环中,conn.Read 会挂起。

如果我在读取的字节数 < 1024 时中断if语句,像这样:

if read < 1024 || errors.Is(err, io.EOF) {
    break
}

这对于 redis-cli PING 命令是有效的。但是,当我执行 echo -e "PING\nPING" | redis-cli 时,它只会读取第一个PING命令,就像第一个测试用例一样,功能上只会响应一个命令然后关闭连接。

所以问题是,如何正确地从连接中读取所有数据并避免挂起?我已经尝试了不同的实现从连接读取数据的方法,但结果都一样。


更多关于Golang中net.Conn读取数据卡顿或提前中断问题解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

我认为你应该先比较 errnil,而不是 read > 0

更多关于Golang中net.Conn读取数据卡顿或提前中断问题解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我认为你应该先比较 err 和 nil,而不是读取 >0。

并非如此,如果读取时遇到 EOF 但仍然读取了一些字节的情况。

// 代码示例
func example() {
    // 此处应放置相关代码
}

我的最佳猜测是,redis-cli 在第一个 PING 命令后正在等待您的回复。根据协议规范,您目前处于已完整接收第一个命令的状态:长度为 1 的数组,第一个长度为 4 的批量字符串,然后是 4 个字节。您现在应该向套接字写回 "+PONG\n"

adsr303:

我最好的猜测是 redis-cli 在第一个 PING 命令后正在等待你的回复。根据协议规范,你目前处于完整接收第一个命令的阶段:一个长度为 1 的数组,第一个长度为 4 的批量字符串,然后是这 4 个字节。你现在应该向套接字写回 "+PONG\n"

有道理,谢谢。

在进行协议解析时,经常需要获取指定长度的数据。当出现不符合协议规范长度的数据时,我认为没有必要处理它。这个缓冲区通常被赋予一个标准长度,当传入 io.ReadFull 时,使用 buff[:4] 来读取 4 字节的数据,而不是直接传入 buff。 这只是一个例子,具体细节仍需依据协议规范。

func main() {
    fmt.Println("hello world")
}

如果你要处理协议数据包,应该使用 io.ReadFull 来处理流连接,以便从流中读取完整的数据包,而不是由你提供一个缓冲区来读取。这样你就不必处理是否需要等待完整数据包的问题,否则可能会读取到被截断的数据。

net.Conn 读取数据时,数据通常是交互式的。当发生阻塞时,意味着对方可能没有发送数据,可能是在等待你的响应。当连接发生错误时,阻塞会被解除。我认为你应该先理解 Go 语言 TCP 的示例,然后再编写具体的应用程序。

func main() {
    fmt.Println("hello world")
}

如果你要处理协议数据包,你应该对连接流使用 io.ReadFull,以便从流中读取完整的数据包,而不是你提供一个缓冲区来读取,这样你就不必处理是否要等待完整数据包的问题,否则你可能会读取到被截断的数据。

net.Conn 读取数据时,数据通常是交互式的。当发生阻塞时,意味着对方可能没有发送数据,可能在等待你的响应。连接错误发生时阻塞会被解除。我认为你应该先理解 Go TCP 的示例,然后再编写具体的应用程序。

你需要传递一个缓冲区给 io.ReadFull,而你描述的是 TCP 连接的一般处理情况,这正是我在这里所做的。

在Go中处理网络连接读取时,常见的误区是认为Read会在消息边界处停止。实际上,TCP是流协议,Read可能返回任意数量的字节(不超过缓冲区大小),需要根据应用层协议来解析消息边界。

对于Redis协议(RESP),应该根据协议格式来解析,而不是依赖固定大小的块。以下是改进的实现:

import (
    "bufio"
    "bytes"
    "io"
    "net"
)

func readConnectionMessage(conn net.Conn) (string, error) {
    reader := bufio.NewReader(conn)
    var result bytes.Buffer
    
    for {
        // 读取一行(Redis协议以\r\n结尾)
        line, err := reader.ReadBytes('\n')
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err
        }
        
        result.Write(line)
        
        // 根据RESP协议解析消息类型
        if len(line) > 0 {
            switch line[0] {
            case '*': // 数组
                // 解析数组元素数量,继续读取相应数量的元素
            case '$': // 批量字符串
                // 解析字符串长度,读取指定长度的数据
                length := parseBulkStringLength(line)
                if length > 0 {
                    data := make([]byte, length+2) // +2 for \r\n
                    _, err := io.ReadFull(reader, data)
                    if err != nil {
                        return "", err
                    }
                    result.Write(data)
                }
            }
        }
        
        // 检查是否完成一个完整的Redis命令
        if isCompleteMessage(result.Bytes()) {
            break
        }
    }
    
    return result.String(), nil
}

func parseBulkStringLength(line []byte) int {
    // 解析"$4\r\n"中的数字4
    var length int
    for i := 1; i < len(line); i++ {
        if line[i] >= '0' && line[i] <= '9' {
            length = length*10 + int(line[i]-'0')
        } else {
            break
        }
    }
    return length
}

func isCompleteMessage(data []byte) bool {
    // 实现RESP协议完整性检查
    // 这里需要根据协议格式判断消息是否完整
    return true
}

更简洁的方法是使用bufio.Scanner配合自定义分割函数:

func readRedisCommands(conn net.Conn) ([]string, error) {
    scanner := bufio.NewScanner(conn)
    scanner.Split(respSplitter)
    
    var commands []string
    for scanner.Scan() {
        commands = append(commands, scanner.Text())
    }
    
    if err := scanner.Err(); err != nil {
        return nil, err
    }
    
    return commands, nil
}

func respSplitter(data []byte, atEOF bool) (advance int, token []byte, err error) {
    if len(data) == 0 {
        return 0, nil, nil
    }
    
    // 简单实现:按行分割,实际需要完整解析RESP协议
    if i := bytes.IndexByte(data, '\n'); i >= 0 {
        return i + 1, data[:i], nil
    }
    
    if atEOF {
        return len(data), data, nil
    }
    
    return 0, nil, nil
}

对于生产环境,建议使用现有的Redis客户端库或参考标准库net/textproto的实现方式。关键是要基于应用层协议(RESP)来解析消息边界,而不是依赖固定缓冲区大小。

回到顶部