Search code examples
c++udpboost-asio

May boost::asio::ip::udp::socket::send_to even fail?


Please consider following code snippet.

It first resolves address of remote host, then opens socket and sends some data to it. Note, it throws immediately when error occurs.

No concurrency involved. Message fits to 1K. Basically the only difference between this code snippet and "real" code is following: message may be sent in several seconds after endpoint was resolved and socket is opened.

using namespace boost::asio;
io_context io_context;

ip::udp::resolver resolver{io_context};
const auto endpoints = resolver.resolve(ip::udp::v4(), "host", "port");
if (endpoints.empty())
    throw std::runtime_error("No endpoints found");
const auto endpoint = endpoints->endpoint();

ip::udp::socket socket{io_context};
socket.open(ip::udp::v4());

const auto message = buffer("asdf"); // fits to 1K

// may the line below fail provided the code above is executed successfully?
socket.send_to(message, endpoint);

For me, provided the endpoint is valid and socket is opened successfully, it seems that call to socket.send_to should be always successful, even if remote host becomes unavailable (since UDP is used).

  1. What exceptions should I expect in the last line?
  2. Can I just assume that no errors will appear?
  3. Should I expect any IO related error code there since we are doing IO anyway?

Solution

  • ASIO implements this in terms of underlying operating system calls. On POSIX, that would be sendto. The possible error conditions are documented (see below).

    However, first things first:


    You could receive a segfault for addressing out of bounds into unknown address space. Depending on your platform it might manifest as EFAULT (boost::asio::error::fault).

    const_buffer buffer{"asdf", 10};
    

    The preferred spelling here is:

    auto buffer = boost::asio::buffer("asdf"); // char[5] includes NUL
    

    Which will send the char[] (including terminating NUL character) (see overload). If you don't want that, consider e.g. boost::asio::buffer("asdf"sv), which uses a string view, without you having to call strlen.

    Note how you create naming conflicts where buffer hides boost::asio::buffer because of using namespace. You did the same with io_context. I'd advise against this level of flirting with danger in C++

    Other Notes

    if (ec)
        throw std::system_error(ec);
    

    Is not needed. If you don't supply ec, the exception boost::system::system_error (but from boost) will already be raised in the same fashion.

    size_t sent = socket.send_to(
            ba::buffer("asdf"),
            endpoints->endpoint());
    

    You use endpoints->endpoint() without validating the resolver results. Depending on situation zero or more resolutions might exist. You might be dereferencing an invalid iterator. This, again, can cause error conditions.

    Other Error Codes

    You can get inspiration from the POSIX documentation: https://pubs.opengroup.org/onlinepubs/009695399/functions/sendto.html

    The vast majority of the conditions are not applicable, partly due to it

    • being datagram, not stream protocol
    • the socket not being in non-blocking mode here (or that being abstracted away by ASIO)
    • the socket being "known good" (assuming no concurrent code that you didn't show)

    However a few remain:

    • EACCESS can occur if you use a regular endpoint as if it were multicast
    • EDESTADDRREQ if you pass an invalid endpoint (e.g. default constructed)
    • EINTR unless you have signals ignored
    • ENOBUFS (when the adapter is jammed - doesn't happen on linux where packets are just dropped)

    Depending on the actual arguments in your real code:

    • EMSGSIZE if your buffer exceeds the limit that can be sent atomically
    • EOPNOTSUPP if you pass invalid flags

    SUMMARY

    The real question is: do you anticipate any errors that you should deal with? If not simply accept the exceptions (I suggest not passing error_code parameters).

    The only such condition I can think of is a failure to resolve the hostname. However, a quick test tells me the resultset will not be empty, but instead resolve throws Host not found (authoritative).

    So, just simplify:

    Live On Coliru

    #include <boost/asio.hpp>
    
    using namespace std::literals;
    namespace ba = boost::asio;
    using ba::ip::udp;
    
    int main() {
        ba::io_context io;
        udp::resolver resolver{io};
        auto endpoints = resolver.resolve(udp::v4(), "127.0.0.1", "6767");
    
        udp::socket socket{io};
        socket.open(udp::v4());
    
        return socket.send_to(ba::buffer("asdf"sv), endpoints->endpoint());
    }
    

    With

    nc -u -l -p 6767 & sleep 1; ./a.out
    

    Prints

    asdf