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

Golang TCP客户端未主动刷写数据及服务器阻塞问题求助

解决Golang TCP客户端/服务器的阻塞与数据刷写问题

嘿,我来帮你搞定这几个头疼的问题!咱们先拆解一下问题根源,再一步步修复代码。

问题根源分析

你的三个问题其实都是因为TCP是面向流的协议,没有天然的消息边界,再加上你用的io.Copy和ioutil.ReadAll都会一直读取直到遇到连接关闭(EOF),导致双方陷入了死锁:

  1. 客户端写完消息后没发送结束信号,服务器的io.Copy一直等着客户端关闭连接才会停止,压根没机会处理消息和返回响应;
  2. 如果你手动关闭客户端连接,服务器虽然能处理消息了,但客户端已经退出,自然收不到响应;
  3. 客户端这边写完后直接调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 11:02:50