Mercurial > code
changeset 471:35729a52fda5
Socket:
- No more Ipv4 and Ipv6, Ip only. It's possible to control which domain to use
by default with Ip::setDefault.
- Add lot of more documentation,
- Add State::Disconnected.
author | David Demelier <markand@malikania.fr> |
---|---|
date | Thu, 05 Nov 2015 14:54:08 +0100 |
parents | bc3a211c5e33 |
children | f8594d3394ab |
files | C++/modules/Socket/Sockets.cpp C++/modules/Socket/Sockets.h C++/tests/Socket/main.cpp |
diffstat | 3 files changed, 223 insertions(+), 143 deletions(-) [+] |
line wrap: on
line diff
--- a/C++/modules/Socket/Sockets.cpp Thu Nov 05 09:07:34 2015 +0100 +++ b/C++/modules/Socket/Sockets.cpp Thu Nov 05 14:54:08 2015 +0100 @@ -166,10 +166,13 @@ /* {{{ Addresses */ -Ip::Ip(int domain) noexcept - : m_domain{domain} +/* Default domain */ +int Ip::m_default{AF_INET}; + +Ip::Ip(Type domain) noexcept + : m_domain(static_cast<int>(domain)) { - assert(domain == AF_INET6 || domain == AF_INET); + assert(m_domain == AF_INET6 || m_domain == AF_INET); if (m_domain == AF_INET6) { std::memset(&m_sin6, 0, sizeof (sockaddr_in6)); @@ -178,10 +181,10 @@ } } -Ip::Ip(int domain, const std::string &host, int port) - : m_domain{domain} +Ip::Ip(const std::string &host, int port, Type domain) + : m_domain(static_cast<int>(domain)) { - assert(domain == AF_INET6 || domain == AF_INET); + assert(m_domain == AF_INET6 || m_domain == AF_INET); if (host == "*") { if (m_domain == AF_INET6) {
--- a/C++/modules/Socket/Sockets.h Thu Nov 05 09:07:34 2015 +0100 +++ b/C++/modules/Socket/Sockets.h Thu Nov 05 14:54:08 2015 +0100 @@ -405,6 +405,7 @@ Accepted, //!< Socket has been accepted (client) Accepting, //!< The client acceptation is in progress Closed, //!< The socket has been closed + Disconnected, //!< The connection was lost }; /* }}} */ @@ -795,7 +796,6 @@ * * @param option the option * @throw Error on errors - * @example socket.set(option::ReuseAddress{true}); */ template <typename Option> inline void set(const Option &option) @@ -833,7 +833,6 @@ * * @return the same value as get() in the option * @throw Error on errors - * @example socket.get<option::ReuseAddress>(); */ template <typename Option> inline auto get() -> decltype(std::declval<Option>().get(*this)) @@ -1109,7 +1108,7 @@ unsigned nbread = m_proto.recv(*this, data, length); assert((m_action == Action::None && m_condition == Condition::None) || - (m_action != Action::Receive && m_condition != Condition::None)); + (m_action == Action::Receive && m_condition != Condition::None)); return nbread; } @@ -1153,7 +1152,7 @@ unsigned nbsent = m_proto.send(*this, data, length); assert((m_action == Action::None && m_condition == Condition::None) || - (m_action != Action::Send && m_condition != Condition::None)); + (m_action == Action::Send && m_condition != Condition::None)); return nbsent; } @@ -1376,6 +1375,9 @@ /* {{{ Options */ +/** + * Namespace of predefined options. + */ namespace option { /** @@ -1566,8 +1568,7 @@ * Predefined addressed to be used * ------------------------------------------------------------------ * - * - Ipv6, - * - Ipv4, + * - Ip, * - Local. */ @@ -1578,7 +1579,24 @@ * @brief Base class for IPv6 and IPv4, you can use it if you don't know in advance if you'll use IPv6 or IPv4. */ class Ip { +public: + /** + * @enum Type + * @brief Type of ip address. + */ + enum Type { + v4 = AF_INET, //!< AF_INET + v6 = AF_INET6 //!< AF_INET6 + }; + private: + /* + * Default domain when using default constructors. + * + * Note: AF_INET or AF_INET6, not + */ + static int m_default; + union { sockaddr_in m_sin; sockaddr_in6 m_sin6; @@ -1589,23 +1607,43 @@ public: /** + * Set the default domain to use when using default Ip constructor. By default, AF_INET is used. + * + * @pre domain must be Type::v4 or Type::v6 + */ + static inline void setDefault(Type domain) noexcept + { + assert(domain == Type::v4 || domain == Type::v6); + + m_default = static_cast<int>(domain); + } + + /** + * Construct using the default domain. + */ + inline Ip() noexcept + : Ip(static_cast<Type>(m_default)) + { + } + + /** * Default initialize the Ip domain. * * @pre domain must be AF_INET or AF_INET6 only * @param domain the domain (AF_INET or AF_INET6) */ - Ip(int domain = AF_INET6) noexcept; + Ip(Type domain) noexcept; /** * Construct an address suitable for bind() or connect(). * - * @pre domain must be AF_INET or AF_INET6 only + * @pre domain must be Type::v4 or Type::v6 * @param domain the domain (AF_INET or AF_INET6) * @param host the host (* for any) * @param port the port number * @throw Error on errors */ - Ip(int domain, const std::string &host, int port); + Ip(const std::string &host, int port, Type domain = v4); /** * Construct an address from a storage. @@ -1665,82 +1703,6 @@ } }; -/** - * @class Ipv6 - * @brief Use IPv6 address (helper). - */ -class Ipv6 : public Ip { -public: - /** - * Construct an empty address. - */ - inline Ipv6() noexcept - : Ip{AF_INET6} - { - } - - /** - * Construct an address on a specific host. - * - * @param host the host ("*" for any) - * @param port the port - * @throw Error on errors - */ - inline Ipv6(const std::string &host, int port) - : Ip{AF_INET6, host, port} - { - } - - /** - * Construct an address from a storage. - * - * @param ss the storage - * @param length the length - */ - inline Ipv6(const sockaddr_storage *ss, socklen_t length) noexcept - : Ip{ss, length} - { - } -}; - -/** - * @class Ipv4 - * @brief Use IPv4 address (helper). - */ -class Ipv4 : public Ip { -public: - /** - * Construct an empty address. - */ - inline Ipv4() noexcept - : Ip{AF_INET} - { - } - - /** - * Construct an address on a specific host. - * - * @param host the host ("*" for any) - * @param port the port - * @throw Error on errors - */ - inline Ipv4(const std::string &host, int port) - : Ip{AF_INET, host, port} - { - } - - /** - * Construct an address from a storage. - * - * @param ss the storage - * @param length the length - */ - inline Ipv4(const sockaddr_storage *ss, socklen_t length) noexcept - : Ip{ss, length} - { - } -}; - #if !defined(_WIN32) /** @@ -1832,6 +1794,9 @@ /** * @class Tcp * @brief Clear TCP implementation. + * + * This is the basic TCP protocol that implements recv, send, connect and accept as wrappers of the usual + * C functions. */ class Tcp { public: @@ -1840,26 +1805,43 @@ * * @return SOCK_STREAM */ - inline int type() noexcept + inline int type() const noexcept { return SOCK_STREAM; } /** * Do nothing. + * + * This function is just present for compatibility, it should never be called. */ template <typename Address> - inline void create(Socket<Address, Tcp> &) noexcept + inline void create(Socket<Address, Tcp> &) const noexcept { /* No-op */ } /** - * Standard connect. Wrapper of connect(2) + * Standard connect. + * + * If the socket is marked non-blocking and the connection cannot be established immediately, then the + * following is true: + * + * - state is set to State::Connecting, + * - action is set to Action::Connect, + * - condition is set to Condition::Writable. + * + * Then the user must wait until the socket is writable and call connect() with 0 arguments. + * + * If the socket is blocking, this function blocks until the connection is complete or an error occurs, in + * that case state is either set to State::Connected or State::Disconnected but action and condition are + * not set. * * @param sc the socket * @param address the address * @param length the length + * @throw net::Error on errors + * @note Wrapper of connect(2) */ template <typename Address, typename Protocol> void connect(Socket<Address, Protocol> &sc, const sockaddr *address, socklen_t length) @@ -1877,6 +1859,7 @@ sc.setAction(Action::Connect); sc.setCondition(Condition::Writable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "connect", error}; } #else @@ -1885,6 +1868,7 @@ sc.setAction(Action::Connect); sc.setCondition(Condition::Writable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "connect"}; } #endif @@ -1894,10 +1878,16 @@ } /** - * Continue the connection. Just check for SOL_SOCKET/SO_ERROR. + * Continue the connection. This function must only be called when the socket is ready for writing, + * the user is responsible of waiting for that condition. + * + * This function check for SOL_SOCKET/SO_ERROR status. + * + * If the connection is complete, status is set to State::Connected, otherwise it is set to + * State::Disconnected. In both cases, action and condition are not set. * * @param sc the socket - * @throw Error on errors + * @throw net::Error on errors */ template <typename Address, typename Protocol> void connect(Socket<Address, Protocol> &sc) @@ -1905,6 +1895,7 @@ int error = sc.template get<int>(SOL_SOCKET, SO_ERROR); if (error == Failure) { + sc.setState(State::Disconnected); throw Error{Error::System, "connect", error}; } @@ -1912,13 +1903,25 @@ } /** - * Accept a clear client. Wrapper of accept(2). + * Accept a clear client. + * + * If the socket is marked non-blocking and there are no pending connection, this function throws an + * error. The user must wait that the socket is readable before calling this function. + * + * If the socket is blocking, this function blocks until a new client is connected or throws an error on + * errors. + * + * If the socket is correctly returned, its state is set to State::Accepted and its action and condition + * are not set. + * + * In any case, action and condition of this socket are not set. * * @param sc the socket * @param address the address destination * @param length the address length * @return the socket - * @throw Error on errors + * @throw net::Error on errors + * @note Wrapper of accept(2) */ template <typename Address, typename Protocol> Socket<Address, Protocol> accept(Socket<Address, Protocol> &sc, sockaddr *address, socklen_t *length) @@ -1934,21 +1937,32 @@ /** * Continue accept. + * + * This function is just present for compatibility, it should never be called. */ template <typename Address, typename Protocol> - inline void accept(Socket<Address, Protocol> &) noexcept + inline void accept(Socket<Address, Protocol> &) const noexcept { /* no-op */ } /** - * Receive data. Wrapper of recv(2). + * Receive data. + * + * If the socket is marked non-blocking and no data is available, 0 is returned and condition is set to + * Condition::Readable. If 0 is returned and condition is not set, then the state is set to + * State::Disconnected. + * + * If the socket is blocking, this function blocks until some data is available or if an error occurs. + * + * In any case, action is never set. * * @param sc the socket * @param data the destination * @param length the destination length * @return the number of bytes read * @throw Error on errors + * @note Wrapper of recv(2) */ template <typename Address> unsigned recv(Socket<Address, Tcp> &sc, void *data, unsigned length) @@ -1960,32 +1974,42 @@ int error = WSAGetLastError(); if (error == WSAEWOULDBLOCK) { - sc.setAction(Action::Receive); sc.setCondition(Condition::Readable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "recv", error}; } #else if (errno == EAGAIN || errno == EWOULDBLOCK) { - sc.setAction(Action::Receive); sc.setCondition(Condition::Readable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "recv"}; } #endif + } else if (nbread == 0) { + sc.setState(State::Disconnected); } return static_cast<unsigned>(nbread); } /** - * Send some data. Wrapper for send(2). + * Send some data. + * + * If the socket is marked non-blocking and the operation would block, then 0 is returned and condition is set to + * Condition::Writable. + * + * If the socket is blocking, this function blocks until the data has been sent. + * + * On any other errors, this function throw net::Error. * * @param sc the socket * @param data the buffer to send * @param length the buffer length * @return the number of bytes sent - * @throw Error on errors + * @throw net::Error on errors + * @note Wrapper of send(2) */ template <typename Address> unsigned send(Socket<Address, Tcp> &sc, const void *data, unsigned length) @@ -1997,16 +2021,18 @@ int error = WSAGetLastError(); if (error == WSAEWOULDBLOCK) { - sc.setAction(Action::Send); + nbsent = 0; sc.setCondition(Condition::Writable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "send", error}; } #else if (errno == EAGAIN || errno == EWOULDBLOCK) { - sc.setAction(Action::Send); + nbsent = 0; sc.setCondition(Condition::Writable); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "send"}; } #endif @@ -2153,7 +2179,16 @@ /** * @class Tls - * @brief OpenSSL secure layer for TCP + * @brief OpenSSL secure layer for TCP. + * + * **Note:** This protocol is much more difficult to use with non-blocking sockets, if some operations would block, the + * user is responsible of calling the function again by waiting for the appropriate condition. See the functions for + * more details. + * + * @see Tls::accept + * @see Tls::connect + * @see Tls::recv + * @see Tls::send */ class Tls : private Tcp { private: @@ -2176,7 +2211,7 @@ bool m_verify{false}; /* - * Construct with a context and ssl, for accept(). + * Construct with a context and ssl, for Tls::accept. */ Tls(Context context, Ssl ssl) : m_context{std::move(context)} @@ -2194,6 +2229,9 @@ return msg == nullptr ? "" : msg; } + /* + * Update the states after an uncompleted operation. + */ template <typename Address, typename Protocol> inline void updateStates(Socket<Address, Protocol> &sc, State state, Action action, int code) { @@ -2223,6 +2261,7 @@ if (no == SSL_ERROR_WANT_READ || no == SSL_ERROR_WANT_WRITE) { updateStates(sc, State::Connecting, Action::Connect, no); } else { + sc.setState(State::Disconnected); throw Error{Error::System, "connect", error(no)}; } } else { @@ -2244,6 +2283,7 @@ if (no == SSL_ERROR_WANT_READ || no == SSL_ERROR_WANT_WRITE) { updateStates(sc, State::Accepting, Action::Accept, no); } else { + sc.setState(State::Disconnected); throw Error(Error::System, "accept", error(no)); } } else { @@ -2255,7 +2295,7 @@ /** * @copydoc Tcp::type */ - inline int type() noexcept + inline int type() const noexcept { return SOCK_STREAM; } @@ -2318,6 +2358,7 @@ * Initialize the SSL objects after have created. * * @param sc the socket + * @throw net::Error on errors */ template <typename Address> inline void create(Socket<Address, Tls> &sc) @@ -2344,10 +2385,18 @@ /** * Connect to a secure host. * + * If the socket is marked non-blocking and the connection cannot be established yet, then the state is set + * to State::Connecting, the condition is set to Condition::Readable or Condition::Writable, the user must + * wait for the appropriate condition before calling the overload connect which takes 0 argument. + * + * If the socket is blocking, this functions blocks until the connection is complete. + * + * If the connection was completed correctly the state is set to State::Connected. + * * @param sc the socket * @param address the address * @param length the address length - * @throw Error on errors + * @throw net::Error on errors */ template <typename Address, typename Protocol> void connect(Socket<Address, Protocol> &sc, const sockaddr *address, socklen_t length) @@ -2365,7 +2414,11 @@ /** * Continue the connection. * + * This function must be called when the socket is ready for reading or writing (check with Socket::condition), + * the state may change exactly like the initial connect call. + * * @param sc the socket + * @throw net::Error on errors */ template <typename Address, typename Protocol> void connect(Socket<Address, Protocol> &sc) @@ -2384,10 +2437,20 @@ /** * Accept a secure client. * + * Because SSL needs several round-trips, if the socket is marked non-blocking and the connection is not + * completed yet, a new socket is returned but with the State::Accepting state. Its condition is set to + * Condition::Readable or Condition::Writable, the user is responsible of calling accept overload which takes + * 0 arguments on the returned socket when the condition is met. + * + * If the socket is blocking, this function blocks until the client is accepted and returned. + * + * If the client is accepted correctly, its state is set to State::Accepted. This instance does not change. + * * @param sc the socket * @param address the address destination * @param length the address length * @return the client + * @throw net::Error on errors */ template <typename Address> Socket<Address, Tls> accept(Socket<Address, Tls> &sc, sockaddr *address, socklen_t *length) @@ -2411,8 +2474,12 @@ /** * Continue accept. * + * This function must be called on the client that is being accepted. + * + * Like accept or connect, user is responsible of calling this function until the connection is complete. + * * @param sc the socket - * @throw Error on errors + * @throw net::Error on errors */ template <typename Address, typename Protocol> inline void accept(Socket<Address, Protocol> &sc) @@ -2423,11 +2490,16 @@ /** * Receive some secure data. * + * If the socket is marked non-blocking, 0 is returned if no data is available yet or if the connection + * needs renegociation. If renegociation is required case, the action is set to Action::Receive and condition + * is set to Condition::Readable or Condition::Writable. The user must wait that the condition is met and + * call this function again. + * * @param sc the socket * @param data the destination * @param len the buffer length * @return the number of bytes read - * @throw Error on errors + * @throw net::Error on errors */ template <typename Address> unsigned recv(Socket<Address, Tls> &sc, void *data, unsigned len) @@ -2438,6 +2510,7 @@ auto no = SSL_get_error(m_ssl.get(), nbread); if (no == SSL_ERROR_WANT_READ || no == SSL_ERROR_WANT_WRITE) { + nbread = 0; updateStates(sc, sc.state(), Action::Receive, no); } else { throw Error{Error::System, "recv", error(no)}; @@ -2450,11 +2523,14 @@ /** * Send some data. * + * Like recv, if the socket is marked non-blocking and no data can be sent or a negociation is required, + * condition and action are set. See receive for more details + * * @param sc the socket * @param data the data to send * @param len the buffer length * @return the number of bytes sent - * @throw Error on errors + * @throw net::Error on errors */ template <typename Address> unsigned send(Socket<Address, Tls> &sc, const void *data, unsigned len) @@ -2465,6 +2541,7 @@ auto no = SSL_get_error(m_ssl.get(), nbsent); if (no == SSL_ERROR_WANT_READ || no == SSL_ERROR_WANT_WRITE) { + nbread = 0 updateStates(sc, sc.state(), Action::Send, no); } else { throw Error{Error::System, "send", error(no)};
--- a/C++/tests/Socket/main.cpp Thu Nov 05 09:07:34 2015 +0100 +++ b/C++/tests/Socket/main.cpp Thu Nov 05 14:54:08 2015 +0100 @@ -85,8 +85,8 @@ class TcpServerTest : public testing::Test { protected: - SocketTcp<Ipv4> m_server; - SocketTcp<Ipv4> m_client; + SocketTcp<Ip> m_server; + SocketTcp<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -109,7 +109,7 @@ TEST_F(TcpServerTest, connect) { m_tserver = std::thread([this] () { - m_server.bind(Ipv4{"*", 16000}); + m_server.bind(Ip{"*", 16000}); m_server.listen(); m_server.accept(nullptr); m_server.close(); @@ -118,7 +118,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - m_client.connect(Ipv4{"127.0.0.1", 16000}); + m_client.connect(Ip{"127.0.0.1", 16000}); m_client.close(); }); } @@ -126,7 +126,7 @@ TEST_F(TcpServerTest, io) { m_tserver = std::thread([this] () { - m_server.bind(Ipv4{"*", 16000}); + m_server.bind(Ip{"*", 16000}); m_server.listen(); auto client = m_server.accept(nullptr); @@ -140,7 +140,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - m_client.connect(Ipv4{"127.0.0.1", 16000}); + m_client.connect(Ip{"127.0.0.1", 16000}); m_client.send("hello world"); ASSERT_EQ("hello world", m_client.recv(512)); @@ -155,8 +155,8 @@ class UdpServerTest : public testing::Test { protected: - SocketUdp<Ipv4> m_server; - SocketUdp<Ipv4> m_client; + SocketUdp<Ip> m_server; + SocketUdp<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -179,8 +179,8 @@ TEST_F(UdpServerTest, io) { m_tserver = std::thread([this] () { - Ipv4 client; - Ipv4 info{"*", 16000}; + Ip client; + Ip info{"*", 16000}; m_server.bind(info); auto msg = m_server.recvfrom(512, &client); @@ -194,7 +194,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - Ipv4 info{"127.0.0.1", 16000}; + Ip info{"127.0.0.1", 16000}; m_client.sendto("hello world", info); @@ -450,8 +450,8 @@ class ListenerTest : public testing::Test { protected: Listener<Select> m_listener; - SocketTcp<Ipv4> m_masterTcp; - SocketTcp<Ipv4> m_clientTcp; + SocketTcp<Ip> m_masterTcp; + SocketTcp<Ip> m_clientTcp; std::thread m_tserver; std::thread m_tclient; @@ -460,7 +460,7 @@ ListenerTest() { m_masterTcp.set(SOL_SOCKET, SO_REUSEADDR, 1); - m_masterTcp.bind(Ipv4{"*", 16000}); + m_masterTcp.bind(Ip{"*", 16000}); m_masterTcp.listen(); } @@ -491,7 +491,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - m_clientTcp.connect(Ipv4{"127.0.0.1", 16000}); + m_clientTcp.connect(Ip{"127.0.0.1", 16000}); }); } @@ -513,7 +513,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - m_clientTcp.connect(Ipv4{"127.0.0.1", 16000}); + m_clientTcp.connect(Ip{"127.0.0.1", 16000}); m_clientTcp.send("hello"); }); } @@ -524,8 +524,8 @@ class NonBlockingConnectTest : public testing::Test { protected: - SocketTcp<Ipv4> m_server; - SocketTcp<Ipv4> m_client; + SocketTcp<Ip> m_server; + SocketTcp<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -551,8 +551,8 @@ class TcpAcceptTest : public testing::Test { protected: - SocketTcp<Ipv4> m_server; - SocketTcp<Ipv4> m_client; + SocketTcp<Ip> m_server; + SocketTcp<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -561,7 +561,7 @@ TcpAcceptTest() { m_server.set(SOL_SOCKET, SO_REUSEADDR, 1); - m_server.bind(Ipv4{"*", 16000}); + m_server.bind(Ip{"*", 16000}); m_server.listen(); } @@ -580,8 +580,8 @@ class TcpRecvTest : public testing::Test { protected: - SocketTcp<Ipv4> m_server; - SocketTcp<Ipv4> m_client; + SocketTcp<Ip> m_server; + SocketTcp<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -590,7 +590,7 @@ TcpRecvTest() { m_server.set(SOL_SOCKET, SO_REUSEADDR, 1); - m_server.bind(Ipv4{"*", 16000}); + m_server.bind(Ip{"*", 16000}); m_server.listen(); } @@ -614,7 +614,7 @@ std::this_thread::sleep_for(100ms); m_tclient = std::thread([this] () { - m_client.connect(Ipv4{"127.0.0.1", 16000}); + m_client.connect(Ip{"127.0.0.1", 16000}); m_client.send("hello"); m_client.close(); }); @@ -626,8 +626,8 @@ class TlsRecvTest : public testing::Test { protected: - SocketTls<Ipv4> m_server{nullptr}; - SocketTls<Ipv4> m_client; + SocketTls<Ip> m_server{nullptr}; + SocketTls<Ip> m_client; std::thread m_tserver; std::thread m_tclient; @@ -641,9 +641,9 @@ protocol.setPrivateKey("Socket/test.key"); protocol.setVerify(false); - m_server = SocketTls<Ipv4>{std::move(protocol)}; + m_server = SocketTls<Ip>{std::move(protocol)}; m_server.set(SOL_SOCKET, SO_REUSEADDR, 1); - m_server.bind(Ipv4{"*", 16000}); + m_server.bind(Ip{"*", 16000}); m_server.listen(); } @@ -672,7 +672,7 @@ m_tclient = std::thread([this] () { try { - m_client.connect(Ipv4{"127.0.0.1", 16000}); + m_client.connect(Ip{"127.0.0.1", 16000}); m_client.send("hello"); } catch (const net::Error &ex) { FAIL() << ex.function() << ": " << ex.what();