net.Listener不能直接广播消息,因其仅接收新连接而不维护已连客户端列表,需自行用sync.Map存连接并遍历Write,注意超时、错误处理及并发安全。

为什么 net.Listener 不能直接广播消息
因为 net.Listener 只负责接收新连接,它本身不持有已建立的客户端连接列表。你拿到的 net.Conn 是单向通信管道,每个连接独立,没有内置的“群发”机制。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 自己维护一个
map[net.Conn]bool或sync.Map来存活跃连接(注意并发安全) - 每次
listener.Accept()后,把新conn加入集合;断开时记得从集合中删掉 - 广播时遍历集合,对每个
conn调用conn.Write()—— 但要加超时和错误处理,否则一个卡住的连接会拖垮整个广播 - 别在主线程里同步遍历写,容易阻塞新连接接入;考虑用 goroutine 分发,或把消息推到 channel 由专用广播 goroutine 处理
conn.Read() 阻塞导致客户端假死怎么办
默认 TCP 连接是阻塞式读,如果客户端突然断网、崩溃或没发完数据,conn.Read() 就一直卡着,对应 goroutine 永久占用内存和栈。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 给每个
conn设置读写 deadline:conn.SetReadDeadline(time.Now().Add(30 * time.Second)) - 捕获
net.ErrDeadlineExceeded错误后主动关闭连接,别让它挂着 - 不要依赖
io.EOF判断下线——网络中断可能不触发 EOF,得靠 deadline + 读失败双重判断 - 如果协议是行协议(如每条消息换行分隔),优先用
bufio.Scanner,它内部自动处理 deadline,比裸Read()更稳
多个 goroutine 并发写同一个 conn 会 panic
net.Conn 的写操作不是并发安全的。如果你在心跳 goroutine、消息广播 goroutine、命令响应 goroutine 里同时调用 conn.Write(),大概率触发 write on closed network connection 或更隐蔽的乱序/截断。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个
conn绑定一个专属的写 goroutine,所有发给它的消息都先塞进chan []byte - 写 goroutine 从 channel 读消息并顺序
Write(),这样天然串行化 - channel 容量设小点(比如 16),配合
select配合default做背压:满时直接丢弃或返回错误,别让发送方无限阻塞 - 关闭连接前,先 close channel,再 wait 写 goroutine 退出,避免往已关闭 channel 发送
为什么不用 net/http 改造成 WebSocket 群聊
纯 TCP 群聊看似简单,但一涉及粘包、心跳、重连、消息有序性、连接数上万,就很快撞墙。而 HTTP/WebSocket 生态有成熟连接管理、反向代理支持、TLS 一键集成、浏览器直连能力。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 如果目标用户包含网页端,或未来要加 Web 管理界面,现在就用
gorilla/websocket替代裸 TCP - WebSocket 连接本身仍是基于 TCP,但协议层帮你处理了帧边界、ping/pong 心跳、关闭握手,省去大量边界 case
- 注意:WebSocket 服务端仍需自己做广播逻辑,但连接存储、并发写保护、超时控制这些可复用库的中间件
- 别为了“练手”硬写 TCP 版本而忽略真实部署时的运维成本——日志、监控、证书、负载均衡,HTTP 栈全都有现成方案
真正麻烦的从来不是怎么把字节发出去,而是怎么确保它被对方完整、及时、按序收到,且发着发着不会把服务器拖垮。这些细节藏在 deadline、channel 缓冲大小、map 删除时机里,而不是在 listener.Accept() 那一行代码里。











