Go UDP编程用法

 提示:转载请注明原文链接

 本文链接:https://360us.net/article/99.html

知识点:

  • udp 协议是无连接的协议,发送数据前不需要先和数据接收方建立连接。

  • 每个 UDP socket 都有一个接收缓冲区,没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。

  • 当socket 接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就被丢弃。UDP是没有流量控制的;快的发送者可以很容易地就淹没慢的接收者,导致接收方的 UDP 丢弃数据报。
  • UDP每个数据包之间有界限,每次接收都是一个完整的数据包,即使被下层的ip协议分片传输(udp包大于ip包的最大传输单元)。
  • ip分片传输,某个片丢失,那么整个udp包都会被丢弃,上层应用不知道udp包被丢弃。

单播

单播就是点对点通信,用于两个主机之间端对端的通信。

服务端主要函数:

  • net.ListenUDP: 监听udp端口
  • ReadFromUDP: 读取连接数据
  • WriteToUDP: 写入数据

由于udp是无连接的协议,所以服务端读和写都要传入远端地址。

客户端主要函数:

  • net.DialUD: udp是无连接的,这里并不是真的建立的连接,而是绑定了远端地址,下面读和写就不用指定远端地址了。

  • Write: 写入数据

  • Read: 读取数据

区别:

- ListenUDP返回的*UDPConnunconnected(未连接),读写方法是ReadFromUDPWriteToUDP(以及ReadFromWriteTo)。

- DialUDP返回的*UDPConnconnected(已连接),读写方法是ReadWrite

服务端:

func simpleServer() {
	listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9981})
	if err != nil {
		log.Fatalln(err)
	}

	log.Printf("server local: <%s>\n", listener.LocalAddr())

	data := make([]byte, 1024)
	for {
		n, remoteAddr, err := listener.ReadFromUDP(data)
		if err != nil {
			log.Println(err)
		}
		log.Printf("server client: <%s> %s\n", remoteAddr, data[:n])
		_, err = listener.WriteToUDP([]byte("server hello"), remoteAddr)
		if err != nil {
			log.Println(err)
		}
	}
}

客户端:

func simpleClient() {
	sip := net.ParseIP("127.0.0.1")
	srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
	dstAddr := &net.UDPAddr{IP: sip, Port: 9981}

	conn, err := net.DialUDP("udp", srcAddr, dstAddr)
	if err != nil {
		log.Println(err)
	}
	defer conn.Close()

	conn.Write([]byte("client hello"))
	data := make([]byte, 256)
	n, err := conn.Read(data)
	if err != nil {
		log.Println(err)
	}
	log.Printf("client remote: <%s>\n", conn.RemoteAddr())
	log.Printf("client local: <%s>\n", conn.LocalAddr())
	log.Printf("client server message: <%s> \n", data[:n])
}

广播

用于一个主机对整个局域网上所有主机通信,即一对所有。广播仅用于局域网。

广播地址可以通过子网掩码算出,主机标识位全为1的地址即为网段广播地址。

例如:10.1.0.0 => 10.1.255.255, 192.168.0.0/26 => 192.168.0.63

服务端和单播一样,区别就是客户端,也叫发送端:

func client() {
	// 10.0.2.255 广播地址根据本机子网掩码得出
	ip := net.ParseIP("10.0.2.255")
	srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
	dstAddr := &net.UDPAddr{IP: ip, Port: 9986}
	// 发送方连接广播地址可以使用ListenPacket,ListenUDP,DialUDP和Dial
	// https://github.com/aler9/howto-udp-broadcast-golang
	conn, err := net.ListenUDP("udp", srcAddr)
	if err != nil {
		log.Println(err)
	}
	defer conn.Close()

	_, err = conn.WriteToUDP([]byte("hello"), dstAddr)
	if err != nil {
		log.Println(err)
	}
	data := make([]byte, 1024)
	// 设置读取超时时间
	conn.SetReadDeadline(time.Now().Add(time.Second))
	n, _, err := conn.ReadFrom(data)
	if err != nil {
		log.Println(err)
	}
	log.Printf("boradcast client read: <%s> %s\n", dstAddr, data[:n])
}

通过net.ListenUDP绑定本地地址和端口,再用WriteToUDP方法写入数据到广播地址,局域网所有主机的9986端口都将能收到发送的消息。

多播,组播

多播和组播都是一个意思,多播是对一组特定的主机进行通信,而不是整个局域网上的所有主机,即一对一组。

协议标准上多播支持广域网之间通信,现时是需要硬件设备路由支持,运营商会关闭相关功能,导致不可用,也就仅限于局域网了。

多播地址是特定的D类地址。

标准库多播

服务端:

func stdlib() {
	// 224.0.0.250多播地址,从D类多播地址里面选一个即可
	addr, err := net.ResolveUDPAddr("udp", "224.0.0.250:9985")
	if err != nil {
		log.Println(err)
	}
    // 特定网络接口上监听多播地址
	listener, err := net.ListenMulticastUDP("udp", nil, addr)
	if err != nil {
		log.Println(err)
	}
	log.Printf("Local: <%s> \n", listener.LocalAddr().String())

	go func() {
		data := make([]byte, 1024)
		for {
            //读取数据
			n, remoteAddr, err := listener.ReadFromUDP(data)
			if err != nil {
				log.Printf("error during read: %s", err)
			}
			log.Printf("stdlib receive <%s> %s\n", remoteAddr, data[:n])
		}
	}()
	stdlibClient()
	time.Sleep(time.Second * 2)
}

客户端:

func stdlibClient() {
	ip := net.ParseIP("224.0.0.250")
	srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
	dstAddr := &net.UDPAddr{IP: ip, Port: 9985}
    // 远端地址和端口指定为服务端的组播地址和端口
	conn, err := net.DialUDP("udp", srcAddr, dstAddr)
	if err != nil {
		log.Println(err)
	}
	defer conn.Close()
	conn.Write([]byte("hello"))
	log.Printf("stdlibClient <%s>\n", conn.RemoteAddr())
}

标准库多播比较简单,没什么控制选项,使用场景仅仅是一些简单的小应用。

复杂应用可以使用golang.org/x/net/ipv4golang.org/x/net/ipv6包。

x/net/ipv4包多播

  1. 指定需要使用的网卡
  2. 监听本机地址和端口
  3. 创建一个用上一步创建的socket作为传输通道的网络端点(endpoint)
  4. 把创建的endpoint加入的多播组中,加入组后可以根据需要添加一些控制参数
  5. 监听接收消息
// 通用多播
func general() {
	// 网络接口, linux ifconfig
	en4, err := net.InterfaceByName("enp0s3")
	// windows
	// en4, err := net.InterfaceByName("以太网")
	if err != nil {
		log.Println(err)
		return
	}
	log.Println(en4)
	// 多播组
	// 224.0.0.250 固定的组播地址
	// 组播地址是iana的保留地址
	group := net.IPv4(224, 0, 0, 250)

	// 侦听,绑定端口
	c, err := net.ListenPacket("udp4", "0.0.0.0:1024")
	if err != nil {
		log.Println(err)
		return
	}
	defer c.Close()

	// 应用加入多播组
	p := ipv4.NewPacketConn(c)
	if err := p.JoinGroup(en4, &net.UDPAddr{IP: group}); err != nil {
		log.Println(err)
		return
	}

	// 更多控制
	// 不支持windows
	if err := p.SetControlMessage(ipv4.FlagDst, true); err != nil {
		log.Println(err)
		return
	}

	//接收数据包
	go func() {
		b := make([]byte, 1500)
		for {
			n, cm, src, err := p.ReadFrom(b)
			if err != nil {
				log.Println(err)
				return
			}
			log.Printf("received1: %s from <%s>\n", b[:n], src)
			// 需要设置SetControlMessage
			if cm.Dst.IsMulticast() {
				// 检查包是否同一个组的包
				if !cm.Dst.Equal(group) {
					log.Println("Unknown group")
					continue
				}
				log.Printf("received: %s from <%s>\n", b[:n], src)
				_, err = p.WriteTo([]byte("world"), cm, src)
				if err != nil {
					log.Println(err)
				}
			}
		}
	}()

	// 发送组播数据包,上面接收能打印出来
	dst := &net.UDPAddr{IP: group, Port: 1024}
	if err := p.SetMulticastInterface(en4); err != nil {
		log.Println(err)
		return
	}
	p.SetMulticastTTL(5)
	if _, err := p.WriteTo([]byte("hello"), nil, dst); err != nil {
		log.Println(err)
		return
	}

	time.Sleep(time.Second * 2)
}

文中源码地址:https://github.com/ilaziness/gopkg/tree/main/net/udp

参考文档:

  1. https://colobu.com/2016/10/19/Go-UDP-Programming/
  2. 其他搜索的零星资料

本文链接:https://360us.net/article/99.html