知识点:
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
返回的*UDPConn
是unconnected
(未连接),读写方法是ReadFromUDP
和WriteToUDP
(以及ReadFrom
和WriteTo
)。
- DialUDP
返回的*UDPConn
是connected
(已连接),读写方法是Read
和Write
。
服务端:
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/ipv4
和golang.org/x/net/ipv6
包。
x/net/ipv4包多播
- 指定需要使用的网卡
- 监听本机地址和端口
- 创建一个用上一步创建的socket作为传输通道的网络端点(endpoint)
- 把创建的endpoint加入的多播组中,加入组后可以根据需要添加一些控制参数
- 监听接收消息
// 通用多播
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
参考文档:
本文链接:https://360us.net/article/99.html