【问题标题】:Handling read/write udp connection in Go在 Go 中处理读/写 udp 连接
【发布时间】:2014-02-24 10:54:07
【问题描述】:

我需要创建 UDP 连接,通过它我可以同时写入和读取数据包。 (使用不同的 goroutine 和 GOMAXPROCS(n),其中 n>1)第一次尝试是这样的:

func new_conn(port, chan_buf int) (conn *net.UDPConn, inbound chan Packet, err error) {
    inbound = make(chan Packet, chan_buf)

    conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: port})
    if err != nil {return}

    go func () {
        for {
            b := make([]byte, UDP_PACKET_SIZE)
            n, addr, err := conn.ReadFromUDP(b)
            if err != nil {
                log.Printf("Error: UDP read error: %v", err)
                continue
            }
            inbound <- Packet{addr, b[:n]}
        }
    }
}

所以为了读取数据包,我使用了 packet := 并写入 conn.WriteTo(data_bytes, remote_addr)。但是race detector 会在连接时同时读/写时发出警告。所以我将代码重写为这样的:

func new_conn(port, chan_buf int) (inbound, outbound chan Packet, err error) {
    inbound = make(chan Packet, chan_buf)
    outbound = make(chan Packet, chan_buf)

    conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: port})
    if err != nil {return}

    go func () {
        for {
            select {
            case packet := <- outbound:
                _, err := conn.WriteToUDP(packet.data, packet.addr)
                if err != nil {
                    log.Printf("Error: UDP write error: %v", err)
                    continue
                }
            default:
                b := make([]byte, UDP_PACKET_SIZE)
                n, addr, err := conn.ReadFromUDP(b)
                if err != nil {
                    log.Printf("Error: UDP read error: %v", err)
                    continue
                }
                inbound <- Packet{addr, b[:n]}
            }
        }
    }
}

此代码将不再触发竞争条件,但如果没有入站数据包,则有阻塞 goroutine 的风险。我看到的唯一解决方案是在调用 ReadFromUDP 之前调用 SetReadDeadline(time.Now()+10*time.Millisecond) 之类的东西。这段代码可能会起作用,但我不太喜欢它。有没有更优雅的方法来解决这个问题?

UPD:警告信息:

==================
WARNING: DATA RACE
Read by goroutine 553:
  net.ipToSockaddr()
      /usr/local/go/src/pkg/net/ipsock_posix.go:150 +0x18a
  net.(*UDPAddr).sockaddr()
      /usr/local/go/src/pkg/net/udpsock_posix.go:45 +0xd9
  net.(*UDPConn).WriteToUDP()
      /usr/local/go/src/pkg/net/udpsock_posix.go:123 +0x4df
  net.(*UDPConn).WriteTo()
      /usr/local/go/src/pkg/net/udpsock_posix.go:139 +0x2f6
  <traceback which points on conn.WriteTo call>

Previous write by goroutine 556:
  syscall.anyToSockaddr()
      /usr/local/go/src/pkg/syscall/syscall_linux.go:383 +0x336
  syscall.Recvfrom()
      /usr/local/go/src/pkg/syscall/syscall_unix.go:223 +0x15c
  net.(*netFD).ReadFrom()
      /usr/local/go/src/pkg/net/fd_unix.go:227 +0x33c
  net.(*UDPConn).ReadFromUDP()
      /usr/local/go/src/pkg/net/udpsock_posix.go:67 +0x164
  <traceback which points on conn.ReadFromUDP call>

Goroutine 553 (running) created at:
  <traceback>

Goroutine 556 (running) created at:
  <traceback>
==================

【问题讨论】:

  • 确切的错误是什么?
  • 这不是错误,而是警告。如果我使用 --race 标志运行第一个示例,我将收到来自竞争检测的警告。 (我将尝试重新创建此警告并将其粘贴到下一条评论中)
  • 它可能会抱怨你在两个 goroutine 中访问同一个变量(它是一个指针),但是任何实现 Conn 的东西都可以处理多个访问它的 goroutine:“多个 goroutine 可以同时调用 Conn 上的方法。”
  • 更新了带有比赛警告的初始帖子。

标签: go udp


【解决方案1】:

根据竞争检测器的跟踪,检测到的竞争似乎是由于在后续写入中重复使用了读取调用返回的UDPAddr。特别是其IP 字段引用的数据。

目前尚不清楚这是否真的是一个问题,因为syscall.ReadFrom 在每次调用时都会分配一个新的地址结构,并且不会长期保留该结构。您可以在将地址发送到出站 goroutine 之前尝试复制地址。例如:

newAddr := new(net.UDPAddr)
*newAddr = *addr
newAddr.IP = make(net.IP, len(addr.IP))
copy(newAddr.IP, add.IP)

但如果不了解您的程序的更多信息,很难说出为什么这被标记为比赛。不过,也许这足以为您指明正确的方向。根据您发布的内容,我无法使用此测试程序重现比赛:http://play.golang.org/p/suDG6hCYYP

【讨论】:

  • 似乎问题确实出在 UDPAddr 上。在调用 WriteTo 之前复制它似乎解决了竞争警告的问题。但这很奇怪,因为程序必须像服务器一样工作,即从入站数据包中重用地址,所以读取总是在写入之后。或者 Go 竞赛检测器可以通过非常接近的写入读取发出误报吗?好吧,无论如何,这是进一步改进的良好起点。感谢您的帮助。
  • 我也有点惊讶,因为我在答案末尾链接的测试程序不会触发比赛检测器。也许看看您的代码与简单示例有何不同,看看是否可以解释问题。
【解决方案2】:

为什么不启动两个 goroutine,一个用于写入,一个用于读取,并且是全双工的?即:

func new_conn(port, chan_buf int) (inbound, outbound chan Packet, err error) {
    inbound = make(chan Packet, chan_buf)
    outbound = make(chan Packet, chan_buf)

    conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: port})
    if err != nil {
        return
    }

    go func() {
        for packet := range outbound {
            _, err := conn.WriteToUDP(packet.data, packet.addr)
            if err != nil {
                log.Printf("Error: UDP write error: %v", err)
                continue
            }
        }
    }()

    go func() {

        b := make([]byte, UDP_PACKET_SIZE)
        for {

            n, addr, err := conn.ReadFromUDP(b)
            if err != nil {
                log.Printf("Error: UDP read error: %v", err)
                continue
            }
            b2 := make([]byte, UDP_PACKET_SIZE)
            copy(b2, b)
            inbound <- Packet{addr, b2[:n]}
        }
    }()

}

【讨论】:

  • 这种方法基本上等同于我帖子中的第一种方法。所以它会触发竞争条件。
  • 您实际上不需要为两者使用相同的conn,它是 UDP,而不是 TCP。
  • 但是如果我尝试在同一个端口上打开第二个连接,我会收到错误“绑定:地址已在使用中”。从一个端口读取并写入另一个端口不是解决方案,因为我需要从同一个端口写入和读取。
  • 我很好奇你实际上得到了这个错误——从 UDP 套接字写入和读取应该是线程安全的。你能发布确切的警告吗?这可能是你可以忽略的东西。
  • 更新了带有比赛警告的初始帖子。
猜你喜欢
  • 2014-11-19
  • 1970-01-01
  • 1970-01-01
  • 2014-09-25
  • 2012-11-19
  • 2016-10-19
  • 2013-09-19
  • 1970-01-01
  • 2017-03-27
相关资源
最近更新 更多