在Go语言开发网络服务时,TCP套接字的读写同步直接影响服务的稳定性和并发处理能力。如果同步逻辑设计不当,可能出现数据读写冲突、连接阻塞、资源泄漏等问题,因此需要遵循合理的实践方案。

TCP套接字读写同步的核心问题
TCP是面向流的协议,本身没有消息边界,同时多个goroutine同时操作同一个套接字的读写时,容易出现以下问题:
- 多个写goroutine同时写入数据,导致数据包交错混乱
- 读goroutine和写goroutine没有协调,出现无效等待或者提前关闭连接
- 连接异常关闭时,读写操作没有及时感知,导致goroutine泄漏
最佳实践方案
1. 读写分离与goroutine配对
每个TCP连接建议启动两个独立的goroutine,分别负责读和写操作,两者通过channel进行通信,避免直接共享套接字的操作权限。读goroutine只处理数据读取和解析,写goroutine只处理数据发送,从职责上隔离读写操作。
package main
import (
"net"
"fmt"
"time"
)
// 处理单个TCP连接
func handleConn(conn net.Conn) {
defer conn.Close()
// 写数据通道,读goroutine解析到数据后需要回复时,往这个通道发数据
writeCh := make(chan []byte, 10)
// 连接关闭通知通道,用于读写goroutine感知连接状态
doneCh := make(chan struct{})
// 启动读goroutine
go readLoop(conn, writeCh, doneCh)
// 启动写goroutine
go writeLoop(conn, writeCh, doneCh)
// 等待连接结束
<-doneCh
}
// 读循环
func readLoop(conn net.Conn, writeCh chan []byte, doneCh chan struct{}) {
defer func() {
close(doneCh)
}()
buf := make([]byte, 1024)
for {
// 设置读超时
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
n, err := conn.Read(buf)
if err != nil {
fmt.Println("读错误:", err)
return
}
// 简单解析,假设收到数据后原样返回
data := make([]byte, n)
copy(data, buf[:n])
// 把需要回复的数据发到写通道
writeCh <- data
}
}
// 写循环
func writeLoop(conn net.Conn, writeCh chan []byte, doneCh chan struct{}) {
defer func() {
close(writeCh)
}()
for {
select {
case data, ok := <-writeCh:
if !ok {
return
}
// 设置写超时
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
_, err := conn.Write(data)
if err != nil {
fmt.Println("写错误:", err)
return
}
case <-doneCh:
return
}
}
}
2. 使用带缓冲的channel传递写数据
写goroutine从channel接收待发送的数据,多个业务goroutine如果需要发送数据,只需要往写通道发送即可,不需要直接操作套接字。带缓冲的channel可以避免业务goroutine因为写通道满而阻塞,缓冲大小可以根据业务场景调整,一般设置为10到100之间即可。
3. 统一超时控制与连接关闭
每个读写操作都需要设置合理的超时时间,避免因为对端异常导致goroutine永久阻塞。同时连接的关闭操作要统一处理,当读goroutine或者写goroutine检测到连接错误时,通过doneCh通知另一个goroutine退出,最后在handleConn函数中统一关闭连接,释放资源。
4. 避免共享状态
不要在多个goroutine之间共享同一个连接的相关状态变量,比如已经读取的字节数、解析进度等,这些状态应该只属于读goroutine内部,避免并发读写状态变量导致的竞态问题。如果需要统计连接的相关指标,可以使用Go的sync/atomic包或者互斥锁来保证安全。
常见错误规避
不要在一个goroutine中同时执行读写操作,也不要让多个goroutine同时调用同一个连接的Write方法。如果业务需要同时处理多个连接,每个连接都遵循上述的读写分离方案即可,不需要在连接之间共享同步资源。
另外,当连接关闭时,要确保所有相关的goroutine都能及时退出,避免goroutine泄漏。可以在doneCh关闭后,让写goroutine主动关闭写通道,读goroutine在检测到doneCh关闭后也主动退出,形成完整的退出链路。