Golang net包读写阻塞求助:客户端服务端握手过程挂起
首先得敲黑板强调:TCP是无消息边界的字节流协议,这就是你遇到挂起问题的核心根源。当你分两次发送消息时,服务端的Read调用根本没法区分“第一条消息结束”和“第二条消息开始”——它只会傻等足够的字节,或者直到连接关闭,两边就这么互相卡住了。而合并成一条发送时,服务端一次读到完整内容,自然流程顺畅。
你遗漏的关键逻辑,就是消息的边界处理。必须在客户端和服务端之间提前约定好消息的分隔规则,常见的靠谱方案有这几种:
1. 固定长度消息
约定每条消息的字节长度固定,比如每条都是100字节,不足的部分用空字节填充。服务端每次读取固定长度的字节,就知道一条消息收尾了。
示例服务端读取逻辑:
buf := make([]byte, 100) // 读取第一条消息 n, err := conn.Read(buf) if err != nil { fmt.Println("read error:", err) return } msg1 := string(buf[:n]) // 读取第二条消息 n, err = conn.Read(buf) if err != nil { fmt.Println("read error:", err) return } msg2 := string(buf[:n]) // 处理完回复客户端 conn.Write([]byte("两条消息已收到"))
2. 特殊分隔符标记
在每条消息末尾加一个特殊分隔符(比如\n或者自定义的__MSG_END__),服务端读取字节时,碰到分隔符就判定一条消息结束。
示例服务端读取逻辑(以\n为分隔符):
import "bytes" func readMsg(conn net.Conn) (string, error) { buf := make([]byte, 1024) var msg []byte for { n, err := conn.Read(buf) if err != nil { return "", err } // 检查是否包含分隔符 if idx := bytes.Index(buf[:n], []byte("\n")); idx != -1 { msg = append(msg, buf[:idx]...) return string(msg), nil } msg = append(msg, buf[:n]...) } } func handleConn(conn net.Conn) { defer conn.Close() // 读取第一条消息 msg1, err := readMsg(conn) if err != nil { fmt.Println("read msg1 error:", err) return } // 读取第二条消息 msg2, err := readMsg(conn) if err != nil { fmt.Println("read msg2 error:", err) return } fmt.Printf("收到消息:%s | %s\n", msg1, msg2) conn.Write([]byte("两条消息已收到")) }
3. 先发送消息长度(最可靠常用)
在每条消息前先发送消息的长度(比如用4字节的大端序int表示),服务端先读取长度,再读取对应长度的字节,精准定位消息边界。
示例服务端读取逻辑:
import "encoding/binary" // 读取消息长度 func readMsgLen(conn net.Conn) (int, error) { lenBuf := make([]byte, 4) _, err := conn.Read(lenBuf) if err != nil { return 0, err } return int(binary.BigEndian.Uint32(lenBuf)), nil } // 读取完整消息内容 func readMsgContent(conn net.Conn, length int) (string, error) { contentBuf := make([]byte, length) totalRead := 0 for totalRead < length { n, err := conn.Read(contentBuf[totalRead:]) if err != nil { return "", err } totalRead += n } return string(contentBuf), nil } func handleConn(conn net.Conn) { defer conn.Close() // 读取第一条消息 len1, err := readMsgLen(conn) if err != nil { fmt.Println("read msg1 len error:", err) return } msg1, err := readMsgContent(conn, len1) if err != nil { fmt.Println("read msg1 content error:", err) return } // 读取第二条消息 len2, err := readMsgLen(conn) if err != nil { fmt.Println("read msg2 len error:", err) return } msg2, err := readMsgContent(conn, len2) if err != nil { fmt.Println("read msg2 content error:", err) return } fmt.Printf("收到消息:%s | %s\n", msg1, msg2) conn.Write([]byte("两条消息已收到")) }
另外还要注意:Golang的net.Conn.Read不一定会一次返回你想要的所有字节,它可能只返回部分数据,所以必须循环读取直到获取足够的字节——这也是你可能忽略的细节,比如服务端第一次Read只读到第一条消息的一半,然后一直等剩下的部分,客户端发完两条消息在等回复,两边就都僵住了。
补全你提供的服务端代码片段(还原核心逻辑):
/* ======= Server start ==============================================*/ package main import ( "fmt" "net" ) func handleConn(conn net.Conn) { defer conn.Close() buf := make([]byte, 1024) // 第一次读取,可能只拿到部分数据 n, err := conn.Read(buf) if err != nil { fmt.Println("read error:", err) return } fmt.Println("received first msg:", string(buf[:n])) // 第二次读取,这里会一直等待,因为没有消息边界,不知道第一条已经结束 n, err = conn.Read(buf) if err != nil { fmt.Println("read error:", err) return } fmt.Println("received second msg:", string(buf[:n])) // 回复客户端 conn.Write([]byte("hello client")) } func main() { listener, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("listen error:", err) return } defer listener.Close() for { conn, err := listener.Accept() if err != nil { fmt.Println("accept error:", err) continue } go handleConn(conn) } }
总结一下,你只需要给客户端和服务端加上消息边界的约定逻辑,同时保证读取时循环获取足够字节,分两次发送消息的流程就能正常跑通,不会再挂起。
内容的提问来源于stack exchange,提问作者cpuNram

