golang中udp分为已连接和未连接两种,两者在发送、接收消息行为模式上有重大区别
背景
前段时间,我们组开发一个紧急需求,需要与其它部门某组进行协议交互,暂且称之为B组。 B组底层通信采用UDP形式,使用pb为传输协议,本来很简单的事情,可是联调过程中却遇到一个大坑,关于golang中udp的连接性问题。
我们这边采用golang技术栈,以DialUDP的接口与B组交互,先send数据,再recv数据。就这么简单的逻辑,却出问题了,B组能收到我们的请求数据,我们这边却无法接收到B组的返回数据。
经过与B组交流,他们那边的架构比较奇怪。
我们设计系统框架,一般会设计一个网关服务,其负责对外提供接口能力,内部再拆分为其它具体服务模块。 外部的请求方将请求发送到网关服务,网关服务再根据服务发现逻辑,将请求转发给具体的内部服务,待内部服务处理完毕后,数据原路返回,通过网关服务返回给外部调用方。
![d376f48da13e445710b446889d7f61b0.png](https://i-blog.csdnimg.cn/blog_migrate/f081bd69754c02fc743d80af10a7c7a4.jpeg)
可是这个B组的架构设计,他们采用了一种取巧的搞法。 由于udp协议,是一种无连接协议,网关服务接收业务方请求后,记录下请求方的ip、port信息,一并发送给内部具体处理服务,该服务处理完毕后,并不是将响应数据发送给网关服务,由其转发给外部调用方。而是其通过udp的形式,直接将响应数据发送给外部调用方。
![4629759aa797dbb0c110f83cacbdefe4.png](https://i-blog.csdnimg.cn/blog_migrate/683a159e4610f81aed4b17b6d82bd6d4.jpeg)
问题分析
可是,就算这样,又有什么问题呢? 我们是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
则可以从任何远端接收数据。
至此,谜底彻底揭开。