跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

基于Golang的原生高并发长连接TCP套接字封装

Go原生net包可支撑高并发长连接,但需正确处理四点:Accept后立即启goroutine、设SetReadDeadline实现心跳、读写分离至不同goroutine、conn.Close()须待读写协程均退出后再执行。 Go 原生
net
包完全能支撑高并发长连接,但直接裸用
net.Conn
极易在生产环境崩掉——不是性能不够,而是四个关键点没设对:Accept 后不启 goroutine、不设
SetReadDeadline
、读写混在一个协程、
conn.Close()
时机错乱。 Accept 后必须立刻启 goroutine,不能在主循环里 Read 常见错误是把
conn.Read()
写在
for { conn, _ := listener.Accept() }
主循环里。一个客户端卡住(比如发半包、不发数据),整个 Accept 循环就堵死,新连接进不来。 正确做法:每次
listener.Accept()
成功后,**立即**
go handleConn(conn)
handleConn
函数里只做该连接的事,不碰 listener 主循环里 **不要** 出现任何
conn.Read()
或
conn.Write()
哪怕只是 echo,也要起 goroutine;并发量上来后,这点开销远小于阻塞代价 必须用
SetReadDeadline
做心跳,别信
SetDeadline
TCP 层没有心跳机制,NAT、云 LB、家用路由器普遍 5–30 分钟空闲就静默断连。等
conn.Read()
返回
EOF
或
io.EOF
才关连接,已经晚了——客户端可能早就掉线,服务端还留着“幽灵连接”占内存和 fd。 只调
conn.SetReadDeadline()
,**别用**
conn.SetDeadline()
——后者同时影响读写,会误杀业务写操作 每次
conn.Read()
前设置:
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
收到有效数据(哪怕只是
"ping"
)后,**立刻重置** deadline,形成应用层心跳 若
err != nil
且
err.(net.Error).Timeout()
为 true,说明读超时,大概率客户端已失联,应主动
return
让 defer 关闭 读写必须分离到不同 goroutine,否则粘包+阻塞一起爆 把
Read
和
Write
放同一个 goroutine,看似简单,实则埋雷:写操作慢(比如序列化耗时、下游调用阻塞)会导致读超时被误判;更麻烦的是,多个
Write
并发调用
conn.Write()
会因 TCP 缓冲区竞争导致粘包错乱。 立即学习 “ go语言免费学习笔记(深入) ”; 每个连接至少起两个 goroutine:
go readLoop(conn)
和
go writeLoop(conn)
readLoop
负责收包、解包、投递到内部 channel;
writeLoop
从 channel 取包、封包、发送 写操作必须加锁或串行化,例如用
sync.Mutex
包裹
conn.Write()
,或让 writeLoop 独占写通道 解包逻辑(如按长度头、分隔符)必须在
readLoop
里完成,不能丢给业务层拼接
conn.Close()
必须等读写都结束,defer 不是万能的 很多人在
handleConn
开头写
defer conn.Close()
,以为万事大吉。但若读 goroutine 还在跑,
conn.Close()
会中断它,触发大量
use of closed network connection
错误;更糟的是,写 goroutine 可能还在往已关闭的 conn 写,panic 或静默失败。 关闭前要协调读写两个 goroutine 的退出:可用
sync.WaitGroup
+
done chan struct{}
通知 读 goroutine 收到 EOF 或 timeout 后,应 close(done);写 goroutine 检测到 done 关闭就停止取包
conn.Close()
应放在所有 goroutine 退出之后,或由一个“守门 goroutine”统一执行 连接对象本身建议封装状态字段(如
isClosed bool
),
Send()
方法先检查再写,避免竞态 真正难的不是启动多少 goroutine,而是让每个连接的生命周期可控:从 Accept 到 Close,deadline 怎么设、读写怎么分、关闭信号怎么传——这些细节一旦漏掉一环,压测时连接数上不去,线上就慢慢堆积泄漏。

相关文章