linux udp传输收不到_golang中udp的连接性

golang中udp分为已连接和未连接两种,两者在发送、接收消息行为模式上有重大区别

背景

前段时间,我们组开发一个紧急需求,需要与其它部门某组进行协议交互,暂且称之为B组。 B组底层通信采用UDP形式,使用pb为传输协议,本来很简单的事情,可是联调过程中却遇到一个大坑,关于golang中udp的连接性问题。

我们这边采用golang技术栈,以DialUDP的接口与B组交互,先send数据,再recv数据。就这么简单的逻辑,却出问题了,B组能收到我们的请求数据,我们这边却无法接收到B组的返回数据。

经过与B组交流,他们那边的架构比较奇怪。

我们设计系统框架,一般会设计一个网关服务,其负责对外提供接口能力,内部再拆分为其它具体服务模块。 外部的请求方将请求发送到网关服务,网关服务再根据服务发现逻辑,将请求转发给具体的内部服务,待内部服务处理完毕后,数据原路返回,通过网关服务返回给外部调用方。

d376f48da13e445710b446889d7f61b0.png

可是这个B组的架构设计,他们采用了一种取巧的搞法。 由于udp协议,是一种无连接协议,网关服务接收业务方请求后,记录下请求方的ip、port信息,一并发送给内部具体处理服务,该服务处理完毕后,并不是将响应数据发送给网关服务,由其转发给外部调用方。而是其通过udp的形式,直接将响应数据发送给外部调用方。

4629759aa797dbb0c110f83cacbdefe4.png

问题分析

可是,就算这样,又有什么问题呢? 我们是UDP服务,B组的网关服务,会将其收到请求的客户端的ip,port信息发送给其内部服务,其再通过udp的形式将数据发送给客户端。

按照道理,我们是能够收到数据的,可是验证结果,客户端就是收不到返回数据。

情急之下,我用C语言复写了一下请求逻辑,并进行请求验证,可以正常接收数据,这更增加了我的疑惑。 对着golang代码和C语言代码辨析,没什么差别。 无奈之下,只能先通过cgo的形式调用C语言函数完成业务需求,待有时间再进行详细分析。

待业务需求完毕,查阅资料和golang源码后终于搞懂这里面的差别。

golang中的udp状态分为已连接未连接

通过DialUDP的形式创建的udp为已连接状态,其会记录remote的ip、port,相当于在两者之间建立了持续通路,发送、接收函数为Write、Read,不需要填remote信息。

而通过ListenUDP建立的udp为未连接形式,发送、接收函数为WriteTo、ReadFrom,需要填写remote信息。

在我们与B组交互的这个模式下,由于是通过DialUDP建立的连接,而响应数据并不是通过原通路返回,所以这里无法接收数据。 改为使用ListenUDP返回的UDPConn进行数据发送、接收,则可正常接收数据,包括在此种特殊交互模式下。

源码分析

下面从源码层面分析DialUDPListenUDP有何不同,为什么会导致使用上的如此差异?

func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error) {
    switch network {
    case "udp", "udp4", "udp6":
    default:
        return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: laddr.opAddr(), Err: UnknownNetworkError(network)}
    }
    if laddr == nil {
        laddr = &UDPAddr{}
    }
    sl := &sysListener{network: network, address: laddr.String()}
    c, err := sl.listenUDP(context.Background(), laddr)
    if err != nil {
        return nil, &OpError{Op: "listen", Net: network, Source: nil, Addr: laddr.opAddr(), Err: err}
    }
    return c, nil
}

func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error) {
    switch network {
    case "udp", "udp4", "udp6":
    default:
        return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: UnknownNetworkError(network)}
    }
    if raddr == nil {
        return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: nil, Err: errMissingAddress}
    }
    sd := &sysDialer{network: network, address: raddr.String()}
    c, err := sd.dialUDP(context.Background(), laddr, raddr)
    if err != nil {
        return nil, &OpError{Op: "dial", Net: network, Source: laddr.opAddr(), Addr: raddr.opAddr(), Err: err}
    }
    return c, nil
}

sl.listenUDP、sd.dialUDP如下:

func (sl *sysListener) listenUDP(ctx context.Context, laddr *UDPAddr) (*UDPConn, error) {
    fd, err := internetSocket(ctx, sl.network, laddr, nil, syscall.SOCK_DGRAM, 0, "listen", sl.ListenConfig.Control)
    if err != nil {
        return nil, err
    }
    return newUDPConn(fd), nil
}

func (sd *sysDialer) dialUDP(ctx context.Context, laddr, raddr *UDPAddr) (*UDPConn, error) {
    fd, err := internetSocket(ctx, sd.network, laddr, raddr, syscall.SOCK_DGRAM, 0, "dial", sd.Dialer.Control)
    if err != nil {
        return nil, err
    }
    return newUDPConn(fd), nil
}

可见,两者最终都是调用internetSocket,但是参数层面有差异,特别是listenUDP调用时raddr为nil,而dialUDP会传入该值。

继续往下看,internetSocket内部会调用socket函数,以linux环境为例,其实现在sock_posix.go

func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only bool, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) (fd *netFD, err error) {
    s, err := sysSocket(family, sotype, proto)
    if err != nil {
        return nil, err
    }
    if err = setDefaultSockopts(s, family, sotype, ipv6only); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }
    if fd, err = newFD(s, family, sotype, net); err != nil {
        poll.CloseFunc(s)
        return nil, err
    }

    // This function makes a network file descriptor for the
    // following applications:
    //
    // - An endpoint holder that opens a passive stream
    //   connection, known as a stream listener
    //
    // - An endpoint holder that opens a destination-unspecific
    //   datagram connection, known as a datagram listener
    //
    // - An endpoint holder that opens an active stream or a
    //   destination-specific datagram connection, known as a
    //   dialer
    //
    // - An endpoint holder that opens the other connection, such
    //   as talking to the protocol stack inside the kernel
    //
    // For stream and datagram listeners, they will only require
    // named sockets, so we can assume that it's just a request
    // from stream or datagram listeners when laddr is not nil but
    // raddr is nil. Otherwise we assume it's just for dialers or
    // the other connection holders.

    if laddr != nil && raddr == nil {
        switch sotype {
        case syscall.SOCK_STREAM, syscall.SOCK_SEQPACKET:
            if err := fd.listenStream(laddr, listenerBacklog(), ctrlFn); err != nil {
                fd.Close()
                return nil, err
            }
            return fd, nil
        case syscall.SOCK_DGRAM:
            if err := fd.listenDatagram(laddr, ctrlFn); err != nil {
                fd.Close()
                return nil, err
            }
            return fd, nil
        }
    }
    if err := fd.dial(ctx, laddr, raddr, ctrlFn); err != nil {
        fd.Close()
        return nil, err
    }
    return fd, nil
}

首先创建socket描述符,如果laddr不为nil,而raddr为nil,说明是监听socket,需要调用Listen函数,接下来调用fd.dial

func (fd *netFD) dial(ctx context.Context, laddr, raddr sockaddr, ctrlFn func(string, string, syscall.RawConn) error) error {
    if ctrlFn != nil {
        c, err := newRawConn(fd)
        if err != nil {
            return err
        }
        var ctrlAddr string
        if raddr != nil {
            ctrlAddr = raddr.String()
        } else if laddr != nil {
            ctrlAddr = laddr.String()
        }
        if err := ctrlFn(fd.ctrlNetwork(), ctrlAddr, c); err != nil {
            return err
        }
    }
    var err error
    var lsa syscall.Sockaddr
    if laddr != nil {
        if lsa, err = laddr.sockaddr(fd.family); err != nil {
            return err
        } else if lsa != nil {
            if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
                return os.NewSyscallError("bind", err)
            }
        }
    }
    var rsa syscall.Sockaddr  // remote address from the user
    var crsa syscall.Sockaddr // remote address we actually connected to
    if raddr != nil {
        if rsa, err = raddr.sockaddr(fd.family); err != nil {
            return err
        }
        if crsa, err = fd.connect(ctx, lsa, rsa); err != nil {
            return err
        }
        fd.isConnected = true
    } else {
        if err := fd.init(); err != nil {
            return err
        }
    }
    // Record the local and remote addresses from the actual socket.
    // Get the local address by calling Getsockname.
    // For the remote address, use
    // 1) the one returned by the connect method, if any; or
    // 2) the one from Getpeername, if it succeeds; or
    // 3) the one passed to us as the raddr parameter.
    lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
    if crsa != nil {
        fd.setAddr(fd.addrFunc()(lsa), fd.addrFunc()(crsa))
    } else if rsa, _ = syscall.Getpeername(fd.pfd.Sysfd); rsa != nil {
        fd.setAddr(fd.addrFunc()(lsa), fd.addrFunc()(rsa))
    } else {
        fd.setAddr(fd.addrFunc()(lsa), raddr)
    }
    return nil
}

导致ListenUDPDialUDP的行为差异的核心实现即在此函数

如果laddr不为nil,还需要调用bind函数,绑定本地ip、port信息,如果raddr不为nil,会调用fd.connect与remote建立连接。

如此即导致ListenUDP函数构造的UDPConn为未连接状态,而DialUDP函数构造的UDPConn为已连接状态,因而DialUDP只能从指定远端接收数据,而ListenUDP则可以从任何远端接收数据。

至此,谜底彻底揭开。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值