UDP in Go
December 3, 2020Go uses the net.Conn interface to abstract different types of
network connections. A net.Conn has both Read and Write methods, and is
usable as an io.Reader and an io.Writer. Some
common implementations of net.Conn are net.TCPConn, which
uses TCP to provide reliable streams, and tls.Conn, which wraps
an existing net.Conn and uses TLS to provide secure streams. A net.Conn
object is usually created with a Dialer object, or with the
net.Dial function. Dial can operate over different transport
protocols (or no transport at all via direct "ip" connections!), which is
why it accepts both "tcp" and "udp" as the network type argument.
When working with TCP, the Go APIs correspond with the system calls you would use if you were writing the equivalent code in C:
Readmaps torecv(2), which, when used with no flags, is the same asread(2), and reads bytes from a socket into a buffer.Writemaps tosend(2), which, when used with no flags, is the samewrite(2), and takes a sequence of bytes to send over a socket.
For TCP clients, a Go net.TCPConn corresponds with a C socket descriptor of type
SOCK_STREAM that has already has been passed to connect(2),
which establishes a TCP connection via the three-way TCP handshake. In Go,
the connect happens when you call net.Dial("tcp", address). For TCP servers,
the Go net.Listener interface provides an
Accept function. This corresponds with the accept(2) system
call on a socket, which spawns a new connected socket by completing the TCP
handshake with each client that sends a SYN. A connected socket is a socket
where the remote address is bound to the socket itself. A connected socket
can only be used with a single remote host. It cannot be used to send network
packets to multiple remote hosts. A non-connected socket does not have a
bound remote address. A non-connected socket can be used with multiple
remote hosts. For TCP servers in C, the accept function uses a single
non-connected socket, created with socket(2), to create many
connected sockets. In Go, a net.TCPListener
implementing the net.Listener interface begets net.Conn objects
implemented by net.TCPConn via the equivalent Accept method. All
connected TCP sockets and net.Conn objects implemented by net.TCPConn can
be used with recv/Read and send/Write, respectively.
This is not the case for UDP sockets. UDP does not have a handshake, and unlike TCP, a UDP socket used for data transfer is not always connected. In the UDP protocol, there is no distinction between clients and servers. Creating a UDP socket or “connection” does not involve sending any packets. A UDP client is simply the the initiator, the party that sends the first packet, rather than the responder, the party that receives the first packet. The initiator necessarily knows the remote address a priori, since the intiator has to send the first packet. The responder can learn the remote address when it receives the packet.
The common instantiation of a UDP client in Go is net.Dial("udp", address).
This returns a net.Conn object implemented by a
net.UDPConn. It provides both Read and Write methods.
This is the equivalent of creating a socket of type SOCK_DGRAM and calling
connect to bind the socket to a specific remote host. The process of
calling connect means that the socket is now a connected socket, despite
the fact that UDP is a “connectionless” protocol. Unlike with TCP, calling
connect will not cause any packets to be transmitted, since there is no
UDP handshake.
On the server side, UDP looks a bit different from TCP. Since UDP doesn’t
require a three-way handshake, there’s no need for the accept system call.
In Go, this means that there are no UDP listeners. Unlike
net.ListenTCP, which returns a net.Listener, the
net.ListenUDP function directly returns a net.Conn,
implemented by net.UDPConn. This connection will be bound to a source
address, but not a remote address. The net.UDPConn is effectively a
non-connected socket ready to receive packets from (or send packets to!) any
host on the network. Unlike TCP, the newly created net.UDPConn did not
cause any handshake packets to be sent. In C, to create a non-connected UDP
socket, call socket to create a socket of type SOCK_DGRAM, and then do
not call connect.
A non-connected socket does not have a bound remote end. To handle this, POSIX introduces two additional system calls:
recvfrom(2), which takes a buffer to receive data into, and a pointer into which it writes out the source address of the received data (equivalent to the remote address)sendto(2), which takes a buffer of data to send, a remote address to send the data to.
Since the relevant address is a parameter, a non-connected socket used with
recvfrom and sendto does not need to know the remote end of the
“connection” in advance—it can receive data from and send data to any host
on the network. In Go, in addition to implementing the net.Conn interface,
a UDPConn implements the net.PacketConn interface
which includes the ReadFrom and WriteTo methods. These correspond to the
recvfrom and sendto system calls. A net.UDPConn wrapping a
non-connected socket, such as those returned by net.ListenUDP, can use
ReadFrom and WriteTo to talk to arbitrary hosts specified as arguments.
The read system call still works on non-connected sockets. Similarly, a
non-connected net.UDPConn can still call Read. This is equivalent to
calling recvfrom or ReadFrom with a null source address. The application
data is returned, but the address information is lost. The send system call
does not work on non-connected socket; there is no way for the system to
determine who the remote host is. Calling send on a non-connected socket
will fail. Similarly, calling Write on a non-connected net.UDPConn will
fail. In C, a non-connected UDP socket can be made connected via the
connect system call. In Golang, there is no way to turn a non-connected
UDPConn into a connected net.UDPConn without going through the syscall
interface. Therefore, only WriteTo can write data through a connection
opened by net.ListenUDP.
The behavior of net.UDPConn might seem odd, but ultimately it reflects the
behavior of the relevant system calls. At any given time, a UDPConn can
only be used with a subset of its available methods, but by tracking what
the underlying system calls would be, you can determine which methods are
safe to use for connection.
Failing that, here’s a table:
ListenTCP, DialTCP |
ListenUDP |
DialUDP |
|
|---|---|---|---|
Read |
Yes | Drops address information | Yes |
Write |
Yes | No | Yes |
ReadFrom |
Method Unavailable | Yes | Always returns the dialed address |
WriteTo |
Method Unavailable | Yes | No |