Skip to content
Go
net — 网络 I/O 基础

net — 网络 I/O 基础

net 提供了可移植的网络 I/O 接口,包括 TCP、UDP、Unix Domain Socket 和 DNS 解析。它是 net/httpcrypto/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"))) // true

2. 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')
}