用net包启动TCP服务端需设缓冲区、读超时并正确处理io.EOF;解析二进制协议头应使用binary.Read而非手动位运算。
用 net 包启动 TCP 服务端监听最简协议交互
go 的 net 包足够轻量,不需要框架就能跑通请求-响应闭环。关键不是“模拟多复杂”,而是先让字节流能收发、不 panic、不漏连接。
常见错误是直接 conn.Read() 不设缓冲区或超时,导致协程卡死;或者忽略 io.EOF 误判为真实错误。
- 用
make([]byte, 1024)分配固定缓冲区,避免频繁分配 - 给
conn.SetReadDeadline()设 30 秒超时,防止客户端断连后服务端一直等 -
conn.Read()返回0, io.EOF是正常结束,不是错误,别直接log.Fatal - 示例片段:
listener, _ := net.Listen("tcp", ":8080") for { conn, _ := listener.Accept() go func(c net.Conn) { defer c.Close() buf := make([]byte, 1024) n, err := c.Read(buf) if err != nil && err != io.EOF { return // 其他错误才处理 } // 解析 buf[:n],写回响应... }(conn) }
解析自定义二进制协议头时别硬写位运算
如果协议要求前 4 字节是长度、后 2 字节是命令 ID(比如你定的 0x01 表示心跳),直接用 binary.Read() 比手撕 buf[0] 更安全、可读。
容易踩的坑是大小端不一致:服务端用 binary.BigEndian,但客户端用小端发包,结果长度永远是 0。
- 统一约定用
binary.BigEndian(网络字节序),双方都按这个读写 - 结构体字段必须导出(首字母大写),否则
binary.Read()会跳过 - 别在协议头里放指针或 slice——
binary包只序列化值,不处理引用 - 示例:
type Header struct { Len uint32 Cmd uint16 Flag byte } var h Header err := binary.Read(conn, binary.BigEndian, &h)
用 bufio.Scanner 处理换行分隔的文本协议更省心
如果你的协议其实是类似 HTTP 或 Redis 的纯文本命令(如 GET key\r\n),别自己 ReadByte() 循环找 \r\n ——bufio.Scanner 内置了分隔符支持,还能自动处理缓冲和边界。
注意默认扫描上限是 64KB,如果某条命令超长,会报 scanner: token too long,不是数据丢了。
- 调用
scanner.Split(bufio.ScanLines)明确按行切 - 用
scanner.Buffer(make([]byte, 4096), 1 把最大 token 改成 1MB - 每次
scanner.Scan()后,用scanner.Bytes()拿原始字节,别转string再切——避免 UTF-8 解码开销 - 错误判断只看
scanner.Err(),Scan()返回 false 不一定出错(可能只是 EOF)
并发模型下小心连接状态共享和资源泄漏
每个连接起 goroutine 很自然,但若协议需要维持会话状态(比如登录态、计数器),直接在 goroutine 里用局部变量存,下次请求就找不到了。
更隐蔽的问题是:没关 conn、没 stop time.Ticker、goroutine 持有大对象指针,会导致内存缓慢上涨,压测半天才暴露。
- 连接级状态存在
map[net.Conn]*Session里,key 用conn.RemoteAddr().String()比用conn指针更安全(避免 GC 问题) - 用
sync.Map替代普通map,避免读写竞争加锁 - 在
defer里清理:删 map 条目、关闭关联的time.Timer、释放大 buffer - 用
go tool pprof定期看 goroutine 数和 heap,比等 OOM 强











