Golang UDP的连接性(网关如何阻碍Golang的UDP通信)

鹿沐浔 · · 1372 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

转自知乎专栏(防止挂掉):

https://zhuanlan.zhihu.com/p/94680036

golang中udp的连接性

曾经浮华

装逼招雷劈

​关注他

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

背景

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

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

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

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

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

问题分析

可是,就算这样,又有什么问题呢? 我们是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进行数据发送、接收,则可正常接收数据,包括在此种特殊交互模式下。

源码分析

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

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

}

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

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

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

至此,谜底彻底揭开。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:鹿沐浔

查看原文:Golang UDP的连接性(网关如何阻碍Golang的UDP通信)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1372 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传