一次UDP通迅的问题排查

 阅读约 9 分钟

通迅模型

(A-->B-->A):A通过UDP发送数据给B(A可以是指定目的地,也可以是广播发送消息给B),B收到消息后根据来源地址和端口向A回发消息,就这么简单的一个通迅过程。

关于golang udp方面的讲解可以参考下https://colobu.com/2016/10/19/Go-UDP-Programming/这篇文件,讲的挺详细。什么时候udp socket是connected状态,什么时候是unconnected状态,什么时候用read/write,什么时候用readFromUDP/writeToUDP都有说明,写的挺好。

来段code demo吧

服务端:

func main() {
    // 创建监听
    sock, err := net.ListenUDP("udp4", &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 9000,
    })
    if err != nil {
        fmt.Println("listen udp failure!", err)
        return
    }
    defer sock.Close()

    for {
        // 读取数据
        data := make([]byte, 4096)
        readNum, rAddr, err := sock.ReadFromUDP(data)
        if err != nil {
            fmt.Println("read data failure!", err)
            continue
        }
        fmt.Println("read byte number:",readNum, "remote addr:", rAddr)
        fmt.Println("received: ", string(data[:readNum]))

        // 发送数据
        sendBts := []byte("hello client!")
        _, err = sock.WriteToUDP(sendBts, rAddr)
        if err != nil {
            fmt.Println("send msg back failure!", err)
            return
        } else {
            fmt.Println("send msg mack ok.", string(sendBts), "to:", rAddr)
        }
    }
}

客户端

func main() {
    sock, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.IPv4(255, 255, 255, 255), // 广播地址
        Port: 9000,
    })
    // 创建监听
    //soct, err := net.ListenUDP("udp4", &net.UDPAddr{
    //    IP:   net.IPv4(0, 0, 0, 0),
    //    Port: 9001,
    //})
    if err != nil {
        fmt.Println("connect udp failure!", err)
        return
    }
    defer sock.Close()

    // 发送数据
    sendBts := []byte("hello server!")
    _, err = sock.Write(sendBts)
    if err != nil {
        fmt.Println("send msg failure!", err)
        return
    }

    // 接收数据
    data := make([]byte, 4096)
    for {
        readNum, rAddr, err := sock.ReadFromUDP(data)
        if err != nil {
            fmt.Println("read udp failure!", err)
            return
        }
        fmt.Println("read byte number:", readNum, "from:", rAddr)
        fmt.Println("received: ", string(data[:readNum]))
    }
}

问题呈现

运行下上面的demo:
服务端

read byte number: 13 remote addr: 10.200.2.50:54404
received:  hello server!
send msg mack ok. hello client! to: 10.200.2.50:54404

客户端

// nothing

发现客户端啥都没输出,可见咱们遇到问题了。服务端明明发送数据了,而且也指定了目的ip和端口号了!为什么客户端没收到呢?客户端dial的时候,发广播消息不会帧听本地端口??或者说广播消息的时候,侦听的不是同一个网卡?带着问题再来改下客户端的代码。

// 启用客户端注释掉的那段代码
func main() {
    // 创建监听
    sock, err := net.ListenUDP("udp4", &net.UDPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 9001, // 再同一台主机上测试,要跟服务端绑定不同的端口;如果是不同的主机,可以用相同的端口
    })
    if err != nil {
        fmt.Println("connect udp failure!", err)
        return
    }
    defer sock.Close()

    // 发送数据
    sendBts := []byte("hello server!")
    _, err = sock.WriteToUDP(sendBts, &net.UDPAddr{
        IP:   net.IPv4(255, 255, 255, 255),
        Port: 9000,
    })
    if err != nil {
        fmt.Println("send msg failure!", err)
        return
    }

    // 接收数据
    data := make([]byte, 4096)
    for {
        readNum, rAddr, err := sock.ReadFromUDP(data)
        if err != nil {
            fmt.Println("read udp failure!", err)
            return
        }
        fmt.Println("read byte number:", readNum, "from:", rAddr)
        fmt.Println("received: ", string(data[:readNum]))
    }
}

再次运行下
服务端

read byte number: 13 remote addr: 10.200.2.50:54404
received:  hello server!
send msg mack ok. hello client! to: 10.200.2.50:54404

客户端

received:  hello client!

看结果还真印证了没有侦听就不能收消息的猜测。为什么dial的时候没有侦听呢?记得c代码会隐式的绑定(参考https://stackoverflow.com/questions/54443823/udp-socket-sendto-implicit-bindhttps://www.cnblogs.com/skyfsm/p/6287787.html),语言之间会有不同的封装吗?咱们就一同来看看golang的源码。
udp.jpg
看到dialUDP和listenUDP这两个方法,也就红框里的参数不一样,其他可以说都一样。再继续往下看internetSocket -> socket方法
image.png
注意到红框里都这段代码,它是给udp侦听/绑定用的,但是有个前提:要存在local addr(laddr)而且要不存在remote addr(raddr)。

那再改下客户端DialUDP的代码

sock, err := net.DialUDP("udp", &net.UDPAddr{
    IP:   net.IPv4(0,0,0,0),
    Port: 9001,
}, nil)

再次运行客户端的代码。

connect udp failure! dial udp 0.0.0.0:9001: missing address

得到了一个缺少地址的一个错误,那说明DialUDP是不支持隐式绑定的。

总结

在使用udp编程的时候呢,如果是单纯的发送数据,可以使用DialUDP来获得socket句柄,然后调用write方法发送数据。如果想要收消息不管是客户端还是服务端,都得用ListenUDP,侦听/绑定后才可以接收消息。

阅读 1.2k更新于 2019-12-05

推荐阅读
目录