net — 网络 I/O 基础
net 提供了可移植的网络 I/O 接口,包括 TCP、UDP、Unix Domain Socket 和 DNS 解析。它是 net/http、crypto/tls 等更高级包的基础。
1. IP 地址与 CIDR
// 解析 IP 地址
ip := net.ParseIP("192.168.1.1")
fmt.Println(ip.IsLoopback()) // false
fmt.Println(ip.IsPrivate()) // true
fmt.Println(ip.IsGlobalUnicast()) // false
// IPv4 / IPv6 转换
ipv4 := ip.To4() // 返回 nil(如果不是 IPv4)
ipv6 := ip.To16() // 总是返回 16 字节
// CIDR 判断
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
fmt.Println(cidr.Contains(net.ParseIP("192.168.1.100"))) // true2. TCP Socket
2.1 🔬 TCP 服务器三步曲
// 1. 监听
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
// 2. Accept 循环
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
// 3. 每个连接一个 goroutine
go handleConn(conn)
}
func handleConn(conn net.Conn) {
defer conn.Close()
// 设置超时
conn.SetDeadline(time.Now().Add(30 * time.Second))
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if err != nil {
if err != io.EOF {
log.Printf("read error: %v", err)
}
return
}
// echo
conn.Write(buf[:n])
}
}🔬 深入原理:
net.Listener.Accept()是阻塞的,每次返回代表一个新连接。Go 的 netpoller(在 runtime 中)确保成千上万个阻塞的 Accept/Read/Write 只占用少量 goroutine。
2.2 TCP 客户端
// 基础 Dial
conn, err := net.Dial("tcp", "localhost:8080")
// 带超时
conn, err := net.DialTimeout("tcp", "localhost:8080", 5*time.Second)
// 带本地地址
localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.1.100:0")
conn, err := net.DialTCP("tcp", localAddr, remoteAddr)
// 通信
conn.Write([]byte("hello\n"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println(string(buf[:n]))
conn.Close()2.3 🔬 TCP 参数调优
if tcpConn, ok := conn.(*net.TCPConn); ok {
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)
tcpConn.SetNoDelay(true) // 禁用 Nagle 算法(低延迟)
tcpConn.SetLinger(0) // 关闭时立即发送 RST
tcpConn.SetReadBuffer(65536)
tcpConn.SetWriteBuffer(65536)
}| 参数 | 作用 | 何时启用 |
|---|---|---|
SetKeepAlive |
检测死连接 | 长连接建议开启 |
SetNoDelay |
关闭 Nagle | 低延迟场景(HTTP/游戏) |
SetLinger(0) |
立即关闭 | 避免 TIME_WAIT 积累 |
SetReadBuffer |
调大读缓冲 | 高吞吐场景 |
3. UDP Socket
3.1 UDP 的关键特性
- 无连接:不需要
Listen+Accept,直接ListenUDP。 - 不可靠:数据可能丢失、乱序、重复。
- 面向数据报:一次
ReadFromUDP= 一个完整的数据报。
// 服务器端
addr, _ := net.ResolveUDPAddr("udp", ":9090")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()
buf := make([]byte, 4096)
for {
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
continue
}
fmt.Printf("from %v: %s\n", clientAddr, buf[:n])
conn.WriteToUDP([]byte("ack"), clientAddr)
}
// 客户端
serverAddr, _ := net.ResolveUDPAddr("udp", "localhost:9090")
conn, _ := net.DialUDP("udp", nil, serverAddr)
conn.Write([]byte("hello"))
n, _ := conn.Read(buf)
fmt.Println(string(buf[:n]))4. Unix Domain Socket
UDS 用于同一台机器上的进程间通信,比 TCP loopback 快 2-3 倍(跳过 TCP/IP 栈)。
// 服务器
os.Remove("/tmp/mysocket.sock") // 清理
listener, _ := net.Listen("unix", "/tmp/mysocket.sock")
defer listener.Close()
defer os.Remove("/tmp/mysocket.sock")
for {
conn, _ := listener.Accept()
go handleConn(conn)
}
// 客户端
conn, _ := net.Dial("unix", "/tmp/mysocket.sock")
conn.Write([]byte("hello"))5. DNS 查询
// A 记录
ips, _ := net.LookupIP("google.com")
// 指定 IP 版本
ipv4s, _ := net.LookupIPv4("google.com")
// 反向查询
names, _ := net.LookupAddr("8.8.8.8")
// MX 记录
mxs, _ := net.LookupMX("gmail.com")
for _, mx := range mxs {
fmt.Printf("Host: %s, Pref: %d\n", mx.Host, mx.Pref)
}
// TXT 记录
txts, _ := net.LookupTXT("example.com")
// NS 记录
nss, _ := net.LookupNS("example.com")
// 自定义 Resolver
resolver := &net.Resolver{
PreferGo: true, // 使用 Go 内置 DNS(而非系统调用)
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, "udp", "8.8.8.8:53") // 指定 DNS 服务器
},
}
ips, _ := resolver.LookupHost(context.Background(), "example.com")6. 网络接口信息
interfaces, _ := net.Interfaces()
for _, iface := range interfaces {
fmt.Printf("Interface: %s, MTU: %d, Flags: %v\n", iface.Name, iface.MTU, iface.Flags)
addrs, _ := iface.Addrs()
for _, addr := range addrs {
fmt.Printf(" Address: %s\n", addr)
}
}7. 🚨 常见陷阱
| 陷阱 | 说明 |
|---|---|
忘记 conn.Close() |
连接泄漏,最终耗尽文件描述符 |
Read 不保证一次读完 |
必须处理部分读取,或用 io.ReadFull |
使用 net.Dial 连接 UDP |
UDP 不是连接导向的,DialUDP 建立的是"伪连接" |
| TCP 粘包 | TCP 是流协议,需要自定义消息边界(长度前缀/分隔符) |
| 并发写 socket | net.Conn 的并发写是安全的,但多个 Write 可能交错 |
TCP 粘包解决方案
TCP 是流协议(不分消息边界),多次 Write 的数据可能在接收端合并成一条,也可能一次 Write 的数据被拆成多次 Read。需要自定义消息边界。
// 方案 1:长度前缀(最常用)
// 发消息前先发送 4 字节的"消息长度",接收方先读 4 字节知道长度再读消息体
func writeFrame(conn net.Conn, data []byte) error {
// 先写 4 字节的长度(以大端序编码,确保跨平台一致性)
lenBuf := make([]byte, 4)
binary.BigEndian.PutUint32(lenBuf, uint32(len(data)))
// PutUint32:将 uint32 按大端序写入 lenBuf 的前 4 字节
// 例如 data 长度是 256 → lenBuf = [0x00, 0x00, 0x01, 0x00]
if _, err := conn.Write(lenBuf); err != nil {
return err
}
_, err := conn.Write(data)
return err
}
func readFrame(conn net.Conn) ([]byte, error) {
lenBuf := make([]byte, 4)
// io.ReadFull 保证读满 4 字节(普通 Read 可能只读到部分)
if _, err := io.ReadFull(conn, lenBuf); err != nil {
return nil, err
}
// Uint32 将 lenBuf 前 4 字节按大端序解码为 uint32
length := binary.BigEndian.Uint32(lenBuf)
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
return data, err
}
// 🔬 binary.BigEndian.PutUint32 详解:
// - 所属包:encoding/binary
// - 签名:func (bigEndian) PutUint32(b []byte, v uint32)
// - 将 uint32 值 v 以大端序(高位在前)写入 b 的前 4 字节
// - b 的长度必须 >= 4,否则 panic
// - 对应读取方法:Uint32(b []byte) uint32
// - 更多端序操作见:go-std/08-计算机基础/计算机基础知识点.md#1-大小端-endianness// 方案 2:分隔符(适合文本协议,如 HTTP 用 \r\n\r\n 分隔头部和体)
func writeLine(conn net.Conn, line string) error {
_, err := fmt.Fprintf(conn, "%s\n", line) // 以 \n 分隔
return err
}
func readLine(conn net.Conn) (string, error) {
reader := bufio.NewReader(conn)
return reader.ReadString('\n')
}