Golang TCP客户端未主动刷写数据及服务器阻塞问题求助
解决Golang TCP客户端/服务器的阻塞与数据刷写问题
嘿,我来帮你搞定这几个头疼的问题!咱们先拆解一下问题根源,再一步步修复代码。
问题根源分析
你的三个问题其实都是因为TCP是面向流的协议,没有天然的消息边界,再加上你用的io.Copy和ioutil.ReadAll都会一直读取直到遇到连接关闭(EOF),导致双方陷入了死锁:
- 客户端写完消息后没发送结束信号,服务器的
io.Copy一直等着客户端关闭连接才会停止,压根没机会处理消息和返回响应; - 如果你手动关闭客户端连接,服务器虽然能处理消息了,但客户端已经退出,自然收不到响应;
- 客户端这边写完后直接调用
ReadAll,也在等服务器的EOF,可服务器还卡在io.Copy呢,两边都堵着。
修复方案
咱们有两种常用的解决思路,选哪种看你的需求:
方案1:使用消息边界(推荐,适合复杂交互场景)
给消息加个明确的结束标记(比如换行符),或者先发送消息长度,让服务器知道什么时候读完一条完整的消息,不用等连接关闭。
修改后的客户端代码(client.go)
package main import ( "fmt" "io/ioutil" "net" ) func writeMessage(conn net.Conn) { msg := "Hello from Client\n" // 加上换行作为消息结束标记 _, writeErr := conn.Write([]byte(msg)) if writeErr != nil { fmt.Println("Write error", writeErr) } } func main() { conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("Dial error", err) return // 别忘了加return,避免后续空指针操作 } defer conn.Close() // 用defer确保最后关闭连接,防止资源泄漏 writeMessage(conn) msg, err := ioutil.ReadAll(conn) if err != nil { fmt.Println("Read error", err) return } fmt.Println("Received message", string(msg), "from", conn.RemoteAddr()) }
修改后的服务器代码(server.go)
package main import ( "bufio" // 引入bufio按行读取消息 "fmt" "net" "os" ) func handleConn(conn net.Conn) { defer conn.Close() // 处理完连接后自动关闭 localAddr := conn.LocalAddr() fmt.Println("Handling connection for", localAddr) // 用bufio.Scanner按行读取,直到遇到换行符(消息结束标记) scanner := bufio.NewScanner(conn) if scanner.Scan() { msg := scanner.Text() fmt.Println("Received message:", msg) fmt.Println("Processed", len(msg), "bytes") _, err := conn.Write([]byte("Successfully processed")) if err != nil { fmt.Println("Write response error:", err) } } if err := scanner.Err(); err != nil { fmt.Println("Read error:", err) } } func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println("Error creating TCP server") os.Exit(1) } defer ln.Close() fmt.Println("Listening on port :8080") for { conn, err := ln.Accept() if err != nil { fmt.Println("Connection error:", err) continue } go handleConn(conn) // 开goroutine处理每个连接,避免阻塞其他请求 } }
方案2:半关闭连接(适合简单的一发一收场景)
客户端写完消息后调用conn.CloseWrite(),告诉服务器“我这边写完了,不会再发数据了,但还能收你的响应”,这样服务器的io.Copy会收到EOF,处理完消息后返回响应,客户端就能读到了。
修改后的客户端main函数
func main() { conn, err := net.Dial("tcp", "127.0.0.1:8080") if err != nil { fmt.Println("Dial error", err) return } defer conn.Close() writeMessage(conn) // 半关闭写端,告诉服务器我发完数据了 if err := conn.(*net.TCPConn).CloseWrite(); err != nil { fmt.Println("Close write error:", err) return } msg, err := ioutil.ReadAll(conn) if err != nil { fmt.Println("Read error", err) return } fmt.Println("Received message", string(msg), "from", conn.RemoteAddr()) }
服务器代码只需加一个goroutine
原来的服务器是同步处理连接,改成goroutine避免阻塞:
go handleConn(conn)
额外注意点
- 服务器必须用
go handleConn(conn)开goroutine处理每个连接,不然同一时间只能处理一个客户端,其他连接会被阻塞; - 养成用
defer conn.Close()的习惯,避免TCP连接资源泄漏; - 生产环境优先选择消息边界的方案,半关闭只适合简单的单次交互场景,复杂业务还是需要明确的消息格式(比如先发送长度再发内容)。
内容的提问来源于stack exchange,提问作者Shubhang b
相关产品推荐
相关产品推荐

