You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang实现Redis风格TCP服务器时请求阻塞无响应问题排查及输入处理优化咨询

Golang实现Redis风格TCP服务器时请求阻塞无响应问题排查及输入处理优化咨询

核心问题诊断

你的服务器卡住无响应的根本原因在parser函数里的io.Copy(&buf, buffer)调用。

io.Copy会持续从连接的读取器中读取所有数据,直到连接被关闭(也就是你在客户端按Ctrl+C断开的时候)。这就导致:

  • 客户端发送请求后,服务器的handleConnection goroutine会一直卡在requests := parser(reader)这一行,根本没机会执行后面的请求处理和响应写入逻辑;
  • 直到你断开客户端,io.Copy才会完成读取,parser返回,服务器才会打印日志,但此时客户端已经断开,自然收不到任何响应。

修复方案:按RESP协议逐帧读取解析

Redis使用RESP(REdis Serialization Protocol)协议,请求格式是明确的帧结构(比如*2\r\n$4\r\nECHO\r\n$3\r\nhey\r\n),我们需要按这个格式逐帧读取,而不是等待整个连接关闭再读取所有数据。

1. 修复后的parser函数

替换原来的parser,改成基于RESP协议的逐段读取:

func parser(reader *bufio.Reader) [][]string {
    requests := [][]string{}

    for {
        // 读取请求帧的开头:*<argc>\r\n
        line, err := reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                break // 客户端断开连接,结束解析
            }
            fmt.Println("读取请求帧头失败:", err.Error())
            return requests
        }
        line = strings.TrimSpace(line)
        if len(line) == 0 || line[0] != '*' {
            fmt.Println("无效的RESP请求帧头:", line)
            continue
        }

        // 解析参数个数
        argcStr := line[1:]
        argc, err := strconv.Atoi(argcStr)
        if err != nil {
            fmt.Println("解析参数个数失败:", err.Error())
            continue
        }
        if argc <= 0 {
            continue
        }

        // 读取每个参数的长度和内容
        args := make([]string, argc)
        validArgs := true
        for i := 0; i < argc; i++ {
            // 读取参数长度行:$<len>\r\n
            lenLine, err := reader.ReadString('\n')
            if err != nil {
                fmt.Println("读取参数长度失败:", err.Error())
                validArgs = false
                break
            }
            lenLine = strings.TrimSpace(lenLine)
            if len(lenLine) == 0 || lenLine[0] != '$' {
                fmt.Println("无效的参数长度行:", lenLine)
                validArgs = false
                break
            }
            argLenStr := lenLine[1:]
            argLen, err := strconv.Atoi(argLenStr)
            if err != nil {
                fmt.Println("解析参数长度失败:", err.Error())
                validArgs = false
                break
            }

            // 读取指定长度的参数内容,加上后面的\r\n
            argBuf := make([]byte, argLen+2) // +2是为了读取结尾的\r\n
            _, err = io.ReadFull(reader, argBuf)
            if err != nil {
                fmt.Println("读取参数内容失败:", err.Error())
                validArgs = false
                break
            }
            // 去掉末尾的\r\n,得到纯净的参数内容
            args[i] = string(argBuf[:argLen])
        }

        if validArgs {
            requests = append(requests, args)
        }
    }

    return requests
}

2. 优化handleConnection的循环处理逻辑

原来的handleConnection只处理一次请求就关闭连接,不符合Redis的长连接特性,改成循环处理直到客户端断开:

func handleConnection(c net.Conn) {
    defer c.Close()

    reader := bufio.NewReader(c)

    // 循环处理客户端的每个请求帧,直到连接断开
    for {
        requests := parser(reader)
        if len(requests) == 0 {
            break // 客户端断开连接,退出循环
        }

        for _, subRequest := range requests {
            if len(subRequest) == 0 {
                continue
            }
            currentCommand := strings.ToUpper(subRequest[0])

            if currentCommand == "ECHO" {
                response := echoHandler(subRequest)
                if response == "" {
                    fmt.Println("bad response")
                    continue
                }
                fmt.Println("response", response)
                _, err := c.Write([]byte(response))
                if err != nil {
                    fmt.Println("写入响应失败:", err.Error())
                    return // 写入失败,直接关闭连接
                }
            }
            // 在这里可以扩展其他Redis命令,比如PING、SET、GET等
        }
    }
}

其他实用优化建议

  • 避免panic:把parser里的panic(err)改成打印错误后优雅退出解析,防止单个连接的错误导致整个服务器崩溃;
  • 连接超时:给net.Conn设置读写超时(比如c.SetDeadline(time.Now().Add(30 * time.Second))),防止闲置连接占用资源;
  • 协议严格校验:增加对RESP协议格式的严格检查(比如确保每个字段后都有\r\n),避免格式错误导致的解析异常;
  • 资源复用:可以考虑复用bufio.Reader的缓冲区,减少内存分配。

验证修复效果

修改完成后,用redis-cli测试:

redis-cli echo "hello world"

服务器会立即处理并返回响应,客户端能正常收到$11\r\nhello world\r\n格式的结果,不会再出现卡住无响应的情况。

备注:内容来源于stack exchange,提问作者ajay_edupuganti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 08:34:50