RSS | GitHub |
The socket API is defined such that the user supplies a buffer of a pre-determined size that UDP datagrams are read into. If the buffer is smaller than the datagram, then the surplus bytes from the datagram are discarded.
How can we read the entire datagram when we do not know its size in advance?
One approach is to define the maximum datagram size at the application protocol level. This approach is fine if you control both the sending and the receiving end.
Another approach is to use the maximum transmission unit minus header overhead as the datagram size. However, the sender can still send larger datagrams, in which case the IP stack will break them into smaller fragments and reassemble them on the receiving side before passing the resulting datagram on to the user.
A third approach is to get the datagram size from the IP stack before reading it. We will describe this approach below with example code written in Boost.Asio.
We start with synchronous case, and then proceed to the asynchronous case.
The basic idea is to query the size of the next datagram from the UDP socket using the bytes_readable
attribute. Obviously we cannot query the size of the next datagram before it has arrived, so first we have to wait for it to arrive.
The standard trick is to use null_buffers
, which is a special buffer type that is used to indicate that we only want to wait for data without actually reading it. Notice that we ignore the return value, because it is always zero.
While the above works, it does not tell us what remote endpoint the datagram has been sent from. We could wait by peeking instead, if we need the remote endpoint before reading the datagram.
We pass a default-constructed mutable_buffer
, which is a zero-sized buffer. This will only fill in the remote endpoint.
Normally we should keep the buffer alive until the call is completed because buffer
is just a view of our buffer, so passing a view of temporary variable is a generally bad idea – this is not really important in the synchronous case, but in the asynchronous case it will be. In this particular case the operation is safe because the view stores the data pointer and the size, which are {nullptr, 0}
here.
Now we can query the size of the next datagram.
And finally we can allocate a buffer of the correct size, and read the full datagram.
The asynchronous case follows the same pattern.
We are going to start with an asynchronous wait. When the next datagram is available, then we can read it synchronously. Otherwise, the main difference is that we have to handle error codes explicitly to avoid throwing exceptions from an asynchronous operation.
In the example below we assume that socket
is a class member variable, and that the class inherits from enable_shared_from_this
. We wait using null buffers, but we could just as well wait using peeking.