万字长文:细数那些Go网络编程的事情

这些函数都是 Go 语言标准库 net 包中用于创建网络监听器的函数,它们的主要区别在于监听的网络类型、返回的连接类型以及使用的场景。

在介绍具体函数之前,需要先理解两个核心概念:

  • Listener (监听器): 负责监听指定网络地址上的连接请求。可以理解为服务器的“接线员”,等待客户端拨号。
  • Conn (连接): 代表一个客户端和服务器之间的网络连接。可以理解为“通话中的线路”,用于双方的数据传输。

一、那些 Listener 和创建函数

1.1 net.Listen(network, address string) (Listener, error)

  • 功能: 这是最通用的监听函数,根据传入的网络类型 ( network) 创建相应的监听器。

  • 参数:

    • TCP/IP: "host:port",例如 "127.0.0.1:8080"":8080"(监听所有本地 IP 的 8080 端口)。

    • Unix: 文件路径,例如 "/var/run/my.sock"

    • "tcp": TCP 协议。

    • "tcp4": IPv4 的 TCP 协议。

    • "tcp6": IPv6 的 TCP 协议。

    • "unix": Unix 域套接字(用于同一台机器上的进程间通信)。

    • network: 网络类型,字符串。常见的有:

    • address: 监听地址,字符串。格式取决于网络类型:

  • 返回值:

    • Listener: 监听器接口。具体类型取决于 network,例如 *net.TCPListener(TCP 监听器)。
    • error: 错误信息,如果监听失败则返回。

示例:

listener, err := net.Listen("tcp", ":8080")
if err != nil {
    // 处理错误
}
defer listener.Close() // 记得关闭监听器

// 接受连接
conn, err := listener.Accept()
if err != nil {
    // 处理错误
}
defer conn.Close() // 记得关闭连接

1.2 net.ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)

  • 功能: 创建一个 TCP 监听器。它是 net.Listen("tcp", address) 的更具体版本。

  • 参数:

    • network: 网络类型,字符串,只能是 "tcp""tcp4""tcp6"
    • laddr: 本地地址, *net.TCPAddr 类型。可以使用 net.ResolveTCPAddr 函数创建:
addr, err := net.ResolveTCPAddr("tcp", ":8080")

  • 返回值:

    • *net.TCPListener: TCP 监听器。
    • error: 错误信息。

1.3 net.ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)

  • 功能: 创建一个 UDP 连接。注意,UDP 是无连接的,所以这里返回的是 *net.UDPConn,而不是 Listener。UDPConn 既可以用于服务端监听,也可以用于客户端发送数据。

  • 参数:

    • network: 网络类型,字符串,只能是 "udp""udp4""udp6"
    • laddr: 本地地址, *net.UDPAddr 类型。可以使用 net.ResolveUDPAddr 函数创建。
  • 返回值:

    • *net.UDPConn: UDP 连接。
    • error: 错误信息。

1.4 net.ListenPacket(network, address string) (PacketConn, error)

  • 功能: 创建一个可以发送和接收数据包的连接。适用于 UDP 和其他面向数据包的协议。

  • 参数:

    • network: 网络类型,字符串。常见的有 "udp""udp4""udp6""ip4:ICMP" 等。
    • address: 本地地址,字符串。
  • 返回值:

    • PacketConn: 数据包连接接口。
    • error: 错误信息。

1.5 net.ListenIP(network string, laddr *IPAddr) (Conn, error)

  • 功能: 创建一个 IP 监听器,允许直接操作 IP 数据包。

  • 参数:

    • network: 网络类型,字符串。例如 "ip4:ICMP""ip6:ipv6-icmp"。network 参数的格式是 “ip4:” 或 “ip6:”, 其中部分指定了 IP 协议之上的协议。
    • laddr: 本地地址, *net.IPAddr 类型。可以使用 net.ResolveIPAddr 函数创建。
  • 返回值:

    • Conn: 连接接口。
    • error: 错误信息。

1.6 ListenConfig.Listen(ctx context.Context, network, address string) (Listener, error)ListenConfig.ListenPacket(ctx context.Context, network, address string) (PacketConn, error)

  • 功能: 这两个方法允许使用 ListenConfig 进行更细粒度的监听配置,例如控制双栈模式、仅监听 IPv4 或 IPv6 等。 ctx context.Context 参数可以用于控制监听的生命周期。

  • 参数:

    • ctx: 上下文,用于取消监听操作。
    • network: 网络类型,字符串。
    • address: 本地地址,字符串。
  • 返回值:

    • ListenerPacketConn: 监听器或数据包连接接口。
    • error: 错误信息。

总结:

  • net.Listen 是最通用的监听函数,根据 network 参数创建不同类型的监听器。
  • net.ListenTCPnet.ListenUDPnet.ListenIP 是针对特定协议的便捷函数。
  • net.ListenPacket 用于处理面向数据包的协议。
  • ListenConfig.ListenListenConfig.ListenPacket 提供了更高级的配置选项。

二、那些 Dial 函数

Listen 函数用于服务器端监听连接,而 Dial 函数用于客户端发起连接。它们就像电话的两端: Listen 是“接听电话”, Dial 是“拨打电话”。

net 包中提供了几种 Dial 函数,分别对应不同的网络类型:

2.1 net.Dial(network, address string) (Conn, error)

  • 功能: 这是最通用的拨号函数,根据传入的网络类型 ( network) 和地址 ( address) 建立连接。

  • 参数:

    • TCP/IP: "host:port",例如 "192.168.1.100:8080""example.com:443"

    • UDP/IP: "host:port",例如 "192.168.1.100:53"

    • Unix:文件路径,例如 "/var/run/my.sock"

    • network:网络类型,字符串。常见的有: 稍后单独介绍 network,因为它的格式很重要。

    • address:连接地址,字符串。格式取决于网络类型:

  • 返回值:

    • Conn:连接接口。具体类型取决于 network,例如 *net.TCPConn(TCP 连接)。
    • error:错误信息,如果连接失败则返回。

2.2 net.DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)

  • 功能: 建立一个 TCP 连接。它是 net.Dial("tcp", address) 的更具体版本,允许指定本地地址 ( laddr)。

  • 参数:

    • network:网络类型,字符串,只能是 "tcp""tcp4""tcp6"
    • laddr:本地地址, *net.TCPAddr 类型。如果为 nil,则由系统自动选择。
    • raddr:远程地址, *net.TCPAddr 类型。
  • 返回值:

    • *net.TCPConn:TCP 连接。
    • error:错误信息。

2.3 net.DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)

  • 功能: 建立一个 UDP 连接。同样允许指定本地地址。

  • 参数:

    • network:网络类型,字符串,只能是 "udp""udp4""udp6"
    • laddr:本地地址, *net.UDPAddr 类型。如果为 nil,则由系统自动选择。
    • raddr:远程地址, *net.UDPAddr 类型。
  • 返回值:

    • *net.UDPConn:UDP 连接。
    • error:错误信息。

2.4 net.DialIP(network string, laddr, raddr *IPAddr) (Conn, error)

  • 功能: 建立一个 IP 连接,用于原始 IP 通信。

  • 参数:

    • network:网络类型,字符串。例如 "ip4:ICMP""ip6:ipv6-icmp"
    • laddr:本地地址, *net.IPAddr 类型。如果为 nil,则由系统自动选择。
    • raddr:远程地址, *net.IPAddr 类型。
  • 返回值:

    • Conn:连接接口。
    • error:错误信息。

2.5 net.DialTimeout(network, address string, timeout time.Duration) (Conn, error)

  • 功能:net.Dial 类似,但增加了超时时间。如果在指定时间内无法建立连接,则返回错误。

  • 参数:

    • network:网络类型,字符串。
    • address:连接地址,字符串。
    • timeout:超时时间, time.Duration 类型。
  • 返回值:

    • Conn:连接接口。
    • error:错误信息。

2.6 net.DialContext(ctx context.Context, network, address string) (Conn, error)

  • 功能: 允许使用 context.Context 来控制连接的生命周期,例如取消连接操作。

  • 参数:

    • ctx:上下文,用于取消连接操作。
    • network:网络类型,字符串。
    • address:连接地址,字符串。
  • 返回值:

    • Conn:连接接口。
    • error:错误信息。

2.7 network 的格式

支持的网络类型 (Known networks):

  • "tcp": TCP 协议(同时支持 IPv4 和 IPv6)。
  • "tcp4": TCP 协议(仅支持 IPv4)。
  • "tcp6": TCP 协议(仅支持 IPv6)。
  • "udp": UDP 协议(同时支持 IPv4 和 IPv6)。
  • "udp4": UDP 协议(仅支持 IPv4)。
  • "udp6": UDP 协议(仅支持 IPv6)。
  • "ip": 原始 IP 通信(同时支持 IPv4 和 IPv6)。
  • "ip4": 原始 IP 通信(仅支持 IPv4)。
  • "ip6": 原始 IP 通信(仅支持 IPv6)。
  • "unix": Unix 域套接字。
  • "unixgram": 基于数据报的 Unix 域套接字。
  • "unixpacket": 基于数据包的 Unix 域套接字。

地址格式:

  • TCP 和 UDP: "host:port"

    • host: 可以是文字 IP 地址、可解析为 IP 地址的主机名,或者留空(表示本地系统)。
    • port: 端口号或服务名称(例如, "http" 表示端口 80)。
    • IPv6 地址需要用方括号括起来,例如 "[2001:db8::1]:80"
  • IP (原始 IP 通信):

    • network: "ip", "ip4", 或 "ip6"

    • protocol: 协议名称(例如, "icmp")或协议号(例如, "1" 表示 ICMP 协议)。

    • "network:protocol""network:protocol_number"

    • host: 可以是文字 IP 地址、可解析为 IP 地址的主机名,或者留空(表示本地系统)。

关键点:

  • TCP 和 UDP 连接的 address 必须包含端口号 ( host:port)。
  • 原始 IP 通信 ( ip, ip4, ip6) 需要指定协议 ,必须是 network:protocol 形式。
  • 空主机 ( "") 或未指定 IP 地址(例如,TCP/UDP 的 ":80""0.0.0.0:80""[::]:80",IP 的 """0.0.0.0""::")表示本地系统。
  • Unix 网络

2.8 总结:

Listen 函数 对应的 Dial 函数 功能
net.Listen(network, address) net.Dial(network, address) 通用连接函数,根据网络类型建立连接。
net.ListenTCP(network, laddr) net.DialTCP(network, laddr, raddr) 建立 TCP 连接,可指定本地地址。
net.ListenUDP(network, laddr) net.DialUDP(network, laddr, raddr) 建立 UDP 连接,可指定本地地址。
net.ListenIP(network, laddr) net.DialIP(network, laddr, raddr) 建立 IP 连接,用于原始 IP 通信。
net.Listen(network, address) net.DialTimeout(network, address, timeout) 建立连接,并设置超时时间。
net.Listen(network, address) net.DialContext(ctx context.Context, network, address) 建立连接,并使用 Context 控制连接生命周期。

理解 ListenDial 的对应关系是进行网络编程的基础。希望以上信息能够帮助你更好地使用 Go 的网络库。

三、那些 Conn 和读写方法

现在我来详细介绍 net.Conn 接口的各种实现,它们的功能,以及发送和接收方法,并说明发送和接收的数据包类型。

3.1 net.Conn 接口:

net.Conn 是 Go 语言 net 包中定义的一个接口,它代表一个通用的面向流的网络连接。多个 Goroutine 可以同时调用 Conn 上的方法。它定义了基本的网络 I/O 操作:

type Conn interface {
    Read(b []byte) (n int, err error)
    Write(b []byte) (n int, err error)
    Close() error
    LocalAddr() Addr
    RemoteAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

  • Read(b []byte) (n int, err error): 从连接中读取数据到 b 中。
  • Write(b []byte) (n int, err error): 将 b 中的数据写入连接。
  • Close() error: 关闭连接。
  • LocalAddr() Addr: 返回本地网络地址。
  • RemoteAddr() Addr: 返回远程网络地址。
  • SetDeadline(t time.Time) error: 设置读写操作的截止时间。
  • SetReadDeadline(t time.Time) error: 设置读操作的截止时间。
  • SetWriteDeadline(t time.Time) error: 设置写操作的截止时间。

3.2 net.TCPConn

TCP 连接。通过 net.DialTCPnet.Dial("tcp", address) 创建。

  • 功能: 提供可靠的、面向连接的字节流传输。保证数据按序到达,并提供错误检测和重传机制。

  • 发送/接收方法: 使用 Read()Write() 方法。

  • 数据包类型: 发送和接收的是 TCP segment(TCP 分段)。在网络传输过程中,TCP segment 会被封装在 IP packet 中,然后可能进一步封装在 Ethernet frame 中。

    • 发送过程: Write() 写入的 payload 数据会被 TCP 协议栈分段成 TCP segment,加上 TCP 头部(包含源端口、目标端口、序列号、确认号等),然后交给 IP 层封装成 IP packet,再交给数据链路层封装成 Ethernet frame。
    • 接收过程: 接收端收到 Ethernet frame 后,依次解封装 IP packet 和 TCP segment,然后将 TCP segment 中的 payload 传递给 Read()
  • 有效负载 (Payload): Read() 返回的是 TCP segment 的 payload,即应用程序实际发送的数据。 Write 写入的也是 payload 数据,不包含各种协议头。

  • 半关闭 (Half-close): TCP 连接支持半关闭,即可以单独关闭读或写方向。

    • CloseRead(): 关闭连接的读方向。调用此方法后,无法再从连接中读取数据,但仍然可以向连接写入数据。
    • CloseWrite(): 关闭连接的写方向。调用此方法后,无法再向连接写入数据,但仍然可以从连接中读取数据。
    • Close(): 同时关闭连接的读和写方向。

3.3 net.UDPConn

UDP 连接。通过 net.DialUDPnet.Dial("udp", address) 创建。

  • 功能: 提供无连接的、不可靠的数据报传输。不保证数据按序到达,也不提供错误检测和重传机制。

  • 发送/接收方法: 使用 ReadFrom()WriteTo() 方法。

  • 数据包类型: 发送和接收的是 UDP datagram(UDP 数据报)。同样会被封装在 IP packet 和 Ethernet frame 中。

    • 发送过程: WriteTo() 写入的 payload 数据会被封装成 UDP datagram,加上 UDP 头部(包含源端口和目标端口),然后交给 IP 层封装成 IP packet,再交给数据链路层封装成 Ethernet frame。
    • 接收过程: 接收端收到 Ethernet frame 后,依次解封装 IP packet 和 UDP datagram,然后将 UDP datagram 中的 payload 传递给 ReadFrom()
  • 有效负载 (Payload): ReadFrom() 返回的是 UDP datagram 的 payload。 Write 写入的也是 payload 数据,不包含各种协议头。

  • 总结

    • 使用 DialUDP 创建的客户端连接,可以使用 Read()Write() 方法进行简单的读写操作。
    • 使用 ListenUDP 创建的服务器连接,必须使用 ReadFrom()WriteTo() 方法,以便处理不同的客户端地址。
    • ReadMsgUDPWriteMsgUDP 用于处理带外数据,通常用于传递控制信息。
    • ReadFromUDPAddrPortWriteToUDPAddrPortReadMsgUDPAddrPortWriteMsgUDPAddrPort 使用 netip.AddrPort 类型,可以提高性能。

3.4 net.IPConn

IP 连接(使用 IP 层的 rawsocket, AF_INET)。通过 net.DialIPnet.ListenIP 创建。

  • 功能: 允许直接发送和接收 IP 数据包,绕过 TCP 和 UDP 协议栈。用于实现自定义网络协议或进行网络诊断工具开发。

  • 发送/接收方法: 使用 ReadFrom()WriteTo() 方法。

  • 数据包类型: 发送和接收的是 IP packet。可能进一步封装在 Ethernet frame 中。

    • 发送过程: WriteTo() 写入的数据会被封装成 IP packet,加上 IP 头部(包含源 IP 地址、目标 IP 地址、协议号等),然后交给数据链路层封装成 Ethernet frame。
    • 接收过程: 接收端收到 Ethernet frame 后,解封装 IP packet,然后将 IP packet 传递给 ReadFrom()
  • 有效负载 (Payload): ReadFrom() 返回的是 IP packet 的 payload,这部分数据可能包含上层协议头部(例如 ICMP 头部)和更上层的数据。你需要根据 IP 头部中的协议号来判断 payload 的类型。

  • 总结 Read 方法族:

Write 方法族:

关键要点:

  • 使用 net.IPConn 进行原始 IP 通信时, 必须(或强烈建议)在 network 参数中指定协议(例如 "ip4:ICMP")。

  • 你需要 自行构建完整的 IP 数据包(包括 IP 头部和任何上层协议头部)。Go 语言的 net 包不会自动添加 IP 头部。

  • ReadWrite 方法 不应该 用于 ListenIP 创建的连接。

  • Write(b []byte) 不推荐 用于 ListenIP 创建的连接。仅在 DialIP 创建且已知对端地址时使用。需要自行构建完整的 IP 数据包。

  • WriteTo(b []byte, addr Addr) 将 IP 数据包发送到指定地址。需要自行构建完整的 IP 数据包。

  • WriteToIP(b []byte, addr *IPAddr) 将 IP 数据包发送到指定地址。需要自行构建完整的 IP 数据包。推荐使用。

  • WriteMsgIP(b, oob []byte, addr *IPAddr) 发送 IP 数据包、带外数据。需要自行构建 IP 头部。

  • Read(b []byte) 不推荐 用于 ListenIP 创建的连接。仅在 DialIP 创建且已知对端地址时使用。

  • ReadFrom(b []byte) 读取 IP 数据包并返回发送方地址( Addr 接口类型)。

  • ReadFromIP(b []byte) 读取 IP 数据包并返回发送方地址( *IPAddr 类型)。推荐使用。

  • ReadMsgIP(b, oob []byte) 读取 IP 数据包、带外数据和标志。

3.5 net.PacketConn

面向数据包的连接。 net.PacketConn 代表一个通用的面向数据包的网络连接,例如 UDP 连接。

  • 实现类型: *net.UDPConn*net.UnixConn(用于 unixgramunixpacket)等。

  • 读写方法:

    • ReadFrom(b []byte) (n int, addr Addr, err error): 从连接中读取一个数据包到 b 中。返回读取的字节数 n、发送方地址 addr 和可能的错误 err
    • WriteTo(b []byte, addr Addr) (n int, err error):b 中的数据包发送到地址 addr。返回写入的字节数 n 和可能的错误 err
  • 数据包类型: UDP datagram(UDP 数据报)。

  • 有效负载: 应用数据。

3.6 syscall.RawConn

原始连接。 net.RawConn 提供了对网络连接底层操作的访问,允许直接操作 socket 文件描述符。这通常用于高级网络编程,例如实现自定义网络协议或进行性能优化。

  • 获取 net.RawConn: 可以通过类型断言从 net.TCPConnnet.UDPConnnet.IPConn 获取 net.RawConn。例如:
rawConn, err := conn.(*net.TCPConn).SyscallConn()
if err != nil {
    // 处理错误
}

  • 主要方法:

    • Control(f func(fd uintptr) error) error: 允许你通过函数 f 直接控制底层的 socket 文件描述符 fd。这可以用来设置 socket 选项、执行特定的系统调用等。
    • Read(b []byte) (int, error): 从连接中读取数据。
    • Write(b []byte) (int, error): 将数据写入连接。
  • 使用 Control 方法进行高级操作: Control 方法接受一个函数 f 作为参数,该函数接收一个 uintptr 类型的参数 fd,它代表底层的 socket 文件描述符。你可以在 f 中使用 syscall 包执行底层的系统调用。例如,设置 socket 的 SO_REUSEADDR 选项:

rawConn, err := conn.(*net.TCPConn).SyscallConn()
if err != nil {
    // 处理错误
}

err = rawConn.Control(func(fd uintptr) error {
    err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
    if err != nil {
        return err
    }
    return nil
})

3.7 各种 Conn 的读写数据行为

Go 语言 net 标准库中, net.Conn 接口有多种实现,针对不同的网络类型( network), ReadXXXWriteXXX 方法操作的数据类型有所不同。下面是一个列表,详细说明了在 networkudp, tcp, ip:udp, ip:icmp 时, ReadXXXWriteXXX 操作的数据类型:

Network Conn 类型 ReadXXX 数据类型 WriteXXX 数据类型 说明
udp *net.UDPConn UDP payload (用户数据) UDP payload (用户数据) 当使用 udp 时, ReadFromWriteTo 方法直接操作 UDP 数据报中的 payload 部分,不包含 UDP 头部。
tcp *net.TCPConn TCP payload (用户数据流) TCP payload (用户数据流) TCP 是面向连接的流式协议,操作的是 TCP 数据流,而不是单个的数据包。
ip:udp *net.IPConn UDP datagram (包含 UDP 头部和 payload) UDP datagram (包含 UDP 头部和 payload) 使用 ip:udp 创建的 IPConnReadFromWriteTo 操作的数据 包含 UDP 头部和 payload,但不包含 IP 头部。Go 会自动处理 IP 头部。这意味着你需要构建/解析 UDP 头部,但无需关心 IP 头部。
ip:icmp *net.IPConn ICMP message (包含 ICMP 头部和 data) ICMP message (包含 ICMP 头部和 data) 类似于 ip:udpip:icmp 操作的数据 包含 ICMP 头部和 data,但不包含 IP 头部。Go 会自动处理 IP 头部。你需要构建/解析 ICMP 头部,但无需关心 IP 头部。

接下来我介绍更准确的读写时是否包含 IP header。

3.8 IPConn 读取时

如果没有特殊设置,IPConn 使用 ReadFrom 读取数据时剥离头部的代码:

func (c *IPConn) readFrom(b []byte) (int, *IPAddr, error) {
// TODO(cw,rsc): consider using readv if we know the family
// type to avoid the header trim/copy
    var addr *IPAddr
    n, sa, err := c.fd.readFrom(b)
    switch sa := sa.(type) {
    case *syscall.SockaddrInet4:
      addr = &IPAddr{IP: sa.Addr[0:]}
      n = stripIPv4Header(n, b)
    case *syscall.SockaddrInet6:
      addr = &IPAddr{IP: sa.Addr[0:], Zone: zoneCache.name(int(sa.ZoneId))}
   }
    return n, addr, err
}

func stripIPv4Header(n int, b []byte) int {
   if len(b) < 20 {
      return n
   }
    l := int(b[0]&0x0f) << 2
    if 20 > l || l > len(b) {
        return n
	}
    if b[0]>>4 != 4 {
        return n
    }
    copy(b, b[l:])
    return n - l
}

这里 b[:n] 的格式是 udp header + payload。

使用 ReadMsgIP 时,数据中包含 ip header,这时 b[:n] 的格式是 ip header + udp header + payload。

3.9 IPConn 写入时

无论 WriteTo 还是 WriteMsgIP,都无需(没有办法)在数据中增加 IP header。

 for {
  n, _, _, remote, err := conn.ReadMsgIP(buf, nil) // payload = ip header + udp header + udp payload
  if err != nil {
   panic(err)
  }

  fmt.Printf("received request from %s: %s\n", remote.String(), buf[28:n])

  // conn.WriteTo(buf[20:n], remote) // udp header + udp payload
  conn.WriteMsgIP(buf[20:n], nil, remote) // udp header + udp payload
 }

3.10 IPConn 读写古怪行为的背后

为什么这么古怪呢?和 IP_HDRINCL 选项有关。

  1. 写入时是否需要 IP 头部?

    • 写入时 需要 提供完整的 IP 头部。

    • 你需要手动构造 IP 头部和传输层数据(如 UDP 头部和数据)。

    • 这种模式通常用于需要完全控制 IP 头部的场景。

    • 写入时 不需要 提供 IP 头部。

    • 操作系统会自动为你构造 IP 头部。

    • 你只需要提供 传输层数据(如 UDP 头部和数据)。

    • 例如,如果你发送 UDP 数据包,只需要构造 UDP 头部和数据部分,操作系统会自动添加 IP 头部。

    • 默认情况下(未设置 IP_HDRINCL

    • 设置了 IP_HDRINCL 选项

  2. 读取时是否包含 IP 头部?

    • 读取时仍然 包含 IP 头部

    • 原始套接字的行为在读取时不受 IP_HDRINCL 选项的影响。

    • 读取时 包含 IP 头部

    • 原始套接字会返回完整的 IP 数据包,包括 IP 头部和传输层数据(如 UDP 头部和数据)。

    • 默认情况下

    • 设置了 IP_HDRINCL 选项

  3. 如何设置 IP_HDRINCL 选项? 在 Go 中,可以通过 syscall.SetsockoptInt 设置 IP_HDRINCL 选项:

import "syscall"

// 创建原始套接字
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_UDP)
if err != nil {
    panic(err)
}

// 设置 IP_HDRINCL 选项
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
if err != nil {
    panic(err)
}

下面的代码和上面的设置 IP_HDRINCL 选项的效果一样,因为它的内部实现就会设置这个选项:

 _, err = ipv4.NewRawConn(conn)
 if err != nil {
  panic(err)
 }

4. 总结

  • 底层始终包含 IP 头部。

  • Go 在 ReadFrom 族方法中为 IPv4 剥离了 IP Header

  • Go 在 ReadMsg 族方法中保留了 IP Header

  • 默认情况下,不需要提供 IP 头部。

  • 设置 IP_HDRINCL 后,需要手动构造 IP 头部。

  • 写入时

  • 读取时

虽然 net.IPConn 提供了丰富的功能,但是因为我们需要记住各种场景的各种设置,反而增加了我们的心智负担,我每次使用它的时候,都不得不查一下我的总结文档。所以你对它的 API 有什么更好的设计?欢迎评论区写出来。

四、ipv4 包中的那些 Conn

golang.org/x/net/ipv4 是 Go 语言的一个扩展包,提供了对 IPv4 协议的底层支持。它允许开发者更精细地控制 IPv4 数据包的处理,包括访问和修改 IP 头部、设置套接字选项等。这个包中的 ConnPacketConnRawConn 是对 net 包中相应接口的扩展,提供了更多与 IPv4 相关的功能。

4.1 ipv4.Conn

ipv4.Conn 是对 net.Conn 的扩展,提供了读写 TOS 和 TTL 的功能。

Conn 的定义如下:

type Conn struct {
 // contains filtered or unexported fields
}

func NewConn(c net.Conn) *Conn
func (c *Conn) SetTOS(tos int) error
func (c *Conn) SetTTL(ttl int) error
func (c *Conn) TOS() (int, error)
func (c *Conn) TTL() (int, error)

常用方法

  • SetTOS(tos int) error:设置 IP 头部的服务类型(Type of Service, TOS)。
  • SetTTL(ttl int) error:设置 IP 头部的生存时间(Time to Live, TTL)。
  • TOS() (int, error): 获取 IP 头部的服务类型。
  • TTL() (int, error): 获取 IP 头部的生存时间。

4.2 ipv4.PacketConn

ipv4.PacketConn 是对 net.PacketConn 的扩展,用于处理基于 IPv4 的面向数据报(如 UDP)的连接。

PacketConn 代表一个使用 IPv4 传输的数据包网络端点。它用于控制多个 IP 级别的套接字选项,包括组播。它还提供基于数据报的网络 I/O 方法,这些方法专门用于 IPv4 及更高层协议,例如 UDP。

PacketConn 的定义如下:

type PacketConn struct {
    // 包含 net.PacketConn 的功能
}
func NewPacketConn(c net.PacketConn) *PacketConn
func (c *PacketConn) ReadBatch(ms []Message, flags int) (int, error)
func (c *PacketConn) ReadFrom(b []byte) (n int, cm *ControlMessage, src net.Addr, err error)
func (c *PacketConn) SetBPF(filter []bpf.RawInstruction) error
func (c *PacketConn) SetControlMessage(cf ControlFlags, on bool) error
func (c *PacketConn) SetTOS(tos int) error
func (c *PacketConn) SetTTL(ttl int) error
func (c *PacketConn) TOS() (int, error)
func (c *PacketConn) TTL() (int, error)
func (c *PacketConn) WriteBatch(ms []Message, flags int) (int, error)
func (c *PacketConn) WriteTo(b []byte, cm *ControlMessage, dst net.Addr) (n int, err error)
...

它好包括多播和广播操作和 ICMPFilter 的设置,这里就不具体介绍了。

ipv4.PacketConn 上面的方法介绍

  • func (c *PacketConn) ReadBatch(ms []Message, flags int) (int, error):批量读取多个 IP 数据包,提高接收效率。 ms 参数是一个 Message 类型的切片,用于存储接收到的数据包和控制信息,每个 Message 包含数据、控制信息和地址信息。 flags 参数控制读取操作的行为,通常使用 0。返回值是成功读取的消息数和可能的错误。适用于需要高性能批量接收数据包的场景。

  • func (c *PacketConn) ReadFrom(b []byte) (n int, cm *ControlMessage, src net.Addr, err error):读取一个 IP 数据包,并接收相关的控制信息。 b 参数是用于存储接收数据的字节切片。返回值包括: n(读取的字节数)、 cm(指向接收到的控制信息的指针,可能为 nil)、 src(发送方地址)和 err(可能发生的错误)。适用于需要接收 IP 头部信息(如 TTL、源地址等)的场景。

  • func (c *PacketConn) SetBPF(filter []bpf.RawInstruction) error:设置 BPF (Berkeley Packet Filter) 过滤器,用于根据复杂的规则选择性地接收特定的数据包。 filter 参数是包含 BPF 指令的切片。如果设置过滤器失败,则返回错误。适用于网络监控或入侵检测等需要复杂数据包过滤的场景。

  • func (c *PacketConn) SetControlMessage(cf ControlFlags, on bool) error:启用或禁用接收特定的控制消息。 cf 参数是一个 ControlFlags 类型的位掩码,用于指定要接收的控制消息类型,例如 ipv4.FlagTTLipv4.FlagSrcipv4.FlagDstipv4.FlagInterface 等。 on 参数为 true 时启用接收,为 false 时禁用。如果设置失败,则返回错误。适用于需要接收 IP 头部特定字段的场景。

  • func (c *PacketConn) SetTOS(tos int) error:设置 IP 数据包的 TOS(Type Of Service)字段,用于指定数据包的服务类型。 tos 参数是 TOS 值,取值范围为 0-255。如果设置失败,则返回错误。

  • func (c *PacketConn) SetTTL(ttl int) error:设置 IP 数据包的 TTL(Time To Live)字段,用于限制数据包在网络中的生存时间。 ttl 参数是 TTL 值,取值范围为 0-255。如果设置失败,则返回错误。

  • func (c *PacketConn) TOS() (int, error):获取当前连接的 TOS 值。返回值是 TOS 值和可能的错误。

  • func (c *PacketConn) TTL() (int, error):获取当前连接的 TTL 值。返回值是 TTL 值和可能的错误。

  • func (c *PacketConn) WriteBatch(ms []Message, flags int) (int, error):批量发送多个 IP 数据包,提高发送效率。 ms 参数是一个 Message 类型的切片,包含要发送的数据包和控制信息。 flags 参数控制发送操作的行为,通常使用 0。返回值是成功发送的消息数和可能的错误。适用于需要高性能批量发送数据包的场景。

  • func (c *PacketConn) WriteTo(b []byte, cm *ControlMessage, dst net.Addr) (n int, err error):发送一个 IP 数据包,并发送相关的控制信息。 b 参数是要发送的数据的字节切片。 cm 参数是指向要发送的控制信息的指针,如果不需要发送控制信息,则为 nildst 参数是目标地址。返回值包括: n(发送的字节数)和 err(可能发生的错误)。适用于需要发送 IP 头部信息(如设置 TTL、TOS)或发送带外数据的场景。

除了实现 TTL 和 TOS 的操作外,你还需要注意四点:

  • ReadFromWriteTo 操作的是 IP 包,包含 IP Header 和它的数据。
  • ReadBatchWriteBatch 支持批量读写。
  • SetBPF 可以设置 cBPF 代码。
  • SetControlMessage 可以设置控制消息,在某些场景下还是很有用的。

4.3 ipv4.RawConn

ipv4.RawConn 是对 net.RawConn 的扩展,用于处理基于 IPv4 的原始套接字连接。

RawConn 代表一个使用 IPv4 传输的数据包网络端点。它用于控制多个 IP 级别的套接字选项,包括 IPv4 头部操作。它还提供基于数据报的网络 I/O 方法,这些方法专门用于处理直接操作 IPv4 数据报的 IPv4 及更高层协议,例如 OSPF 和 GRE。

RawConn 的定义如下:

type RawConn struct {
    // 包含 net.RawConn 的功能
}
func NewRawConn(c net.PacketConn) (*RawConn, error)
func (c *RawConn) ReadBatch(ms []Message, flags int) (int, error)
func (c *RawConn) ReadFrom(b []byte) (h *Header, p []byte, cm *ControlMessage, err error)
func (c *RawConn) SetBPF(filter []bpf.RawInstruction) error
func (c *RawConn) SetControlMessage(cf ControlFlags, on bool) error
func (c *RawConn) SetTOS(tos int) error
func (c *RawConn) SetTTL(ttl int) error
func (c *RawConn) TOS() (int, error)
func (c *RawConn) TTL() (int, error)
func (c *RawConn) WriteBatch(ms []Message, flags int) (int, error)
func (c *RawConn) WriteTo(h *Header, p []byte, cm *ControlMessage) error
...

你可以看到 PacketConnRawConnReadFromWriteTo 是不同的。

  • PacketConn 的读写的数据都是 IP 包,包含 IP Header 和它的数据
  • RawConn 的读写数据将 IP Header 和数据分开开来了,独立处理

原文阅读