From 9232cee44a0c9f46b79725290f613e00a968b4ea Mon Sep 17 00:00:00 2001 From: Marcin Siodelski Date: Mon, 2 Jan 2017 16:25:36 +0100 Subject: [PATCH] [5094] Improved and documented TCPAcceptor tests. --- src/lib/asiolink/io_socket.h | 12 +- src/lib/asiolink/tcp_acceptor.h | 39 ++- .../asiolink/tests/tcp_acceptor_unittest.cc | 270 +++++++++++++++--- 3 files changed, 274 insertions(+), 47 deletions(-) diff --git a/src/lib/asiolink/io_socket.h b/src/lib/asiolink/io_socket.h index efb2eebf7b..9c9cee16fd 100644 --- a/src/lib/asiolink/io_socket.h +++ b/src/lib/asiolink/io_socket.h @@ -1,4 +1,4 @@ -// Copyright (C) 2010-2015 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2010-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -35,6 +35,16 @@ namespace asiolink { /// derived class for testing purposes rather than providing factory methods /// (i.e., getDummy variants below). class IOSocket { +public: + + /// @name Types of objects encapsulating socket options. + //@{ + + /// @brief Represents SO_REUSEADDR socket option. + typedef boost::asio::socket_base::reuse_address ReuseAddress; + + //@} + /// /// \name Constructors and Destructor /// diff --git a/src/lib/asiolink/tcp_acceptor.h b/src/lib/asiolink/tcp_acceptor.h index 5eecdfd7d9..85879fa010 100644 --- a/src/lib/asiolink/tcp_acceptor.h +++ b/src/lib/asiolink/tcp_acceptor.h @@ -1,4 +1,4 @@ -// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -12,48 +12,63 @@ #endif #include +#include #include #include +#include +#include namespace isc { namespace asiolink { template -class TCPAcceptor { +class TCPAcceptor : public IOSocket{ public: TCPAcceptor(IOService& io_service) - : acceptor_(io_service.get_io_service()) { + : IOSocket(), + acceptor_(new boost::asio::ip::tcp::acceptor(io_service.get_io_service())) { + } + + virtual ~TCPAcceptor() { } + + virtual int getNative() const { + return (acceptor_->native()); + } + + virtual int getProtocol() const { + return (IPPROTO_TCP); } void open(const TCPEndpoint& endpoint) { - acceptor_.open(endpoint.getASIOEndpoint().protocol()); - acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true)); - + acceptor_->open(endpoint.getASIOEndpoint().protocol()); } -// void setOption(const SettableSocketOption& socket_option); + template + void setOption(const SettableSocketOption& socket_option) { + acceptor_->set_option(socket_option); + } void bind(const TCPEndpoint& endpoint) { - acceptor_.bind(endpoint.getASIOEndpoint()); + acceptor_->bind(endpoint.getASIOEndpoint()); } void listen() { - acceptor_.listen(); + acceptor_->listen(); } template void asyncAccept(const TCPSocket& socket, C& callback) { - acceptor_.async_accept(socket.getASIOSocket(), callback); + acceptor_->async_accept(socket.getASIOSocket(), callback); } bool isOpen() const { - return (acceptor_.is_open()); + return (acceptor_->is_open()); } private: - boost::asio::ip::tcp::acceptor acceptor_; + boost::shared_ptr acceptor_; }; diff --git a/src/lib/asiolink/tests/tcp_acceptor_unittest.cc b/src/lib/asiolink/tests/tcp_acceptor_unittest.cc index 709ef29bc7..275252b69f 100644 --- a/src/lib/asiolink/tests/tcp_acceptor_unittest.cc +++ b/src/lib/asiolink/tests/tcp_acceptor_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -17,112 +17,222 @@ #include #include #include +#include #include -using namespace boost::asio::ip; using namespace isc::asiolink; namespace { -class TCPClient; +/// @brief Local server address used for testing. +const char SERVER_ADDRESS[] = "127.0.0.1"; +/// @brief Local server port used for testing. +const unsigned short SERVER_PORT = 18123; + +/// @brief Test timeout in ms. +const long TEST_TIMEOUT = 10000; + +/// @brief Simple class representing TCP socket callback. class SocketCallback { public: - void operator()(boost::system::error_code ec = boost::system::error_code(), - size_t length = 0) { - std::cout << "socket callback invoked" << std::endl; + /// @brief Implements callback for the asynchornous operation on the socket. + /// + /// This callback merely checks if error has occurred and reports this + /// error. It does nothing in case of success. + /// + /// @param ec Error code. + /// @param length Length of received data. + void operator()(boost::system::error_code ec, size_t length = 0) { + if (ec) { + ADD_FAILURE() << "error occurred for a socket: " << ec.message(); + } } }; -typedef boost::shared_ptr TCPClientPtr; - +/// @brief Entity which can connect to the TCP server endpoint and close the +/// connection. class TCPClient : public boost::noncopyable { public: - TCPClient(IOService& io_service) + /// @brief Constructor. + /// + /// This constructor creates new socket instance. It doesn't connect. Call + /// connect() to connect to the server. + /// + /// @param io_service IO service to be stopped on error. + explicit TCPClient(IOService& io_service) : io_service_(io_service.get_io_service()), socket_(io_service_) { } - void connect() { - tcp::endpoint endpoint(address::from_string("127.0.0.1"), 18123); - try { - socket_.connect(endpoint); - } catch (const boost::system::system_error& ex) { - ADD_FAILURE() << "an error occured while connecting over TCP socket: " - << ex.what(); + /// @brief Destructor. + /// + /// Closes the underlying socket if it is open. + ~TCPClient() { + close(); + } + /// @brief Connect to the test server address and port. + /// + /// This method asynchronously connects to the server endpoint and uses the + /// connectHandler as a callback function. + void connect() { + boost::asio::ip::tcp::endpoint + endpoint(boost::asio::ip::address::from_string(SERVER_ADDRESS), + SERVER_PORT); + socket_.async_connect(endpoint, + boost::bind(&TCPClient::connectHandler, this,_1)); + } + + /// @brief Callback function for connect(). + /// + /// This function stops the IO service upon error. + /// + /// @param ec Error code. + void connectHandler(const boost::system::error_code& ec) { + if (ec) { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); io_service_.stop(); } } + /// @brief Close connection. void close() { socket_.close(); } private: + /// @brief Holds reference to the IO service. boost::asio::io_service& io_service_; + + /// @brief A socket used for the connecion. boost::asio::ip::tcp::socket socket_; }; +/// @brief Pointer to the TCPClient. +typedef boost::shared_ptr TCPClientPtr; +/// @brief A signature of the function implementing callback for the +/// TCPAcceptor. typedef boost::function TCPAcceptorCallback; +/// @brief TCPAcceptor using TCPAcceptorCallback. +typedef TCPAcceptor TestTCPAcceptor; + +/// @brief Implements asynchronous TCP acceptor service. +/// +/// It creates a new socket into which connection is accepted. The socket +/// is retained until class instance exists. class Acceptor { public: - Acceptor(IOService& io_service, TCPAcceptor& acceptor, + /// @brief Constructor. + /// + /// @param io_service IO service. + /// @param acceptor Reference to the TCP acceptor on which asyncAccept + /// will be called. + /// @param callback Callback function for the asyncAccept. + explicit Acceptor(IOService& io_service, TestTCPAcceptor& acceptor, const TCPAcceptorCallback& callback) : socket_(io_service), acceptor_(acceptor), callback_(callback) { } + /// @brief Destructor. + /// + /// Closes socket. + ~Acceptor() { + socket_.close(); + } + + /// @brief Asynchronous accept new connection. void accept() { acceptor_.asyncAccept(socket_, callback_); } + /// @brief Close connection. void close() { socket_.close(); } private: + /// @brief Socket into which connection is accepted. TCPSocket socket_; - TCPAcceptor& acceptor_; + + /// @brief Reference to the TCPAcceptor on which asyncAccept is called. + TestTCPAcceptor& acceptor_; + + /// @brief Instance of the callback used for asyncAccept. TCPAcceptorCallback callback_; }; +/// @brief Pointer to the Acceptor object. typedef boost::shared_ptr AcceptorPtr; +/// @brief Test fixture class for TCPAcceptor. +/// +/// This class provides means for creating new TCP connections, i.e. simulates +/// clients connecting to the servers via TCPAcceptor. It is possible to create +/// multiple simultaneous connections, which are retained by the test fixture +/// class and closed cleanly when the test fixture is destroyed. class TCPAcceptorTest : public ::testing::Test { public: + /// @brief Constructor. + /// + /// Besides initializing class members it also sets the test timer to guard + /// against endlessly running IO service when TCP connections are + /// unsuccessful. TCPAcceptorTest() - : io_service_(), acceptor_(io_service_), test_timer_(io_service_), - connections_(), clients_(), connections_num_(0), max_connections_(1) { + : io_service_(), acceptor_(io_service_), + asio_endpoint_(boost::asio::ip::address::from_string(SERVER_ADDRESS), + SERVER_PORT), + endpoint_(asio_endpoint_), test_timer_(io_service_), connections_(), + clients_(), connections_num_(0), max_connections_(1) { test_timer_.setup(boost::bind(&TCPAcceptorTest::timeoutHandler, this), - 10000, IntervalTimer::ONE_SHOT); + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); } + /// @brief Destructor. virtual ~TCPAcceptorTest() { - for (auto client = clients_.begin(); client != clients_.end(); - ++client) { - (*client)->close(); - } - - for (auto conn = connections_.begin(); conn != connections_.end(); - ++conn) { - (*conn)->close(); - } } + /// @brief Specifies how many new connections are expected before the IO + /// service is stopped. + /// + /// @param max_connections Connections limit. void setMaxConnections(const unsigned int max_connections) { max_connections_ = max_connections; } + /// @brief Create ASIO endpoint from the provided endpoint by retaining the + /// IP address and modifying the port. + /// + /// This convenience method is useful to create new endpoint from the + /// existing endpoint to test reusing IP address for multiple acceptors. + /// The returned endpoint has the same IP address but different port. + /// + /// @param endpoint Source endpoint. + /// + /// @return New endpoint with the port number increased by 1. + boost::asio::ip::tcp::endpoint + createSiblingEndpoint(const boost::asio::ip::tcp::endpoint& endpoint) const { + boost::asio::ip::tcp::endpoint endpoint_copy(endpoint); + endpoint_copy.port(endpoint.port() + 1); + return (endpoint_copy); + } + + /// @brief Starts accepting TCP connections. + /// + /// This method creates new Acceptor instance and calls accept() to start + /// accepting new connections. The instance of the Acceptor object is + /// retained in the connections_ list. void accept() { TCPAcceptorCallback cb = boost::bind(&TCPAcceptorTest::acceptHandler, this, _1); @@ -131,18 +241,31 @@ public: connections_.back()->accept(); } + /// @brief Connect to the endpoint. + /// + /// This method creates TCPClient instance and retains it in the clients_ + /// list. void connect() { TCPClientPtr client(new TCPClient(io_service_)); clients_.push_back(client); clients_.back()->connect(); } + /// @brief Callback function for asynchronous accept calls. + /// + /// It stops the IO service upon error or when the number of accepted + /// connections reaches the max_connections_ value. Otherwise it calls + /// accept() to start accepting next connections. + /// + /// @param ec Error code. void acceptHandler(const boost::system::error_code& ec) { if (ec) { ADD_FAILURE() << "error occurred while accepting connection: " << ec.message(); + io_service_.stop(); } + // We have reached the maximum number of connections - end the test. if (++connections_num_ >= max_connections_) { io_service_.stop(); @@ -151,34 +274,113 @@ public: } } + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. void timeoutHandler() { ADD_FAILURE() << "Timeout occurred while running the test!"; io_service_.stop(); } + /// @brief IO service. IOService io_service_; - TCPAcceptor acceptor_; + + /// @brief TCPAcceptor under test. + TestTCPAcceptor acceptor_; + + /// @brief Server endpoint. + boost::asio::ip::tcp::endpoint asio_endpoint_; + + /// @brief asiolink server endpont (uses asio_endpoint_). + TCPEndpoint endpoint_; + + /// @brief Asynchronous timer service to detect timeouts. IntervalTimer test_timer_; + + /// @brief List of connections on the server side. std::list connections_; + + /// @brief List of client connections. std::list clients_; + + /// @brief Current number of established connections. unsigned int connections_num_; + + /// @brief Connections limit. unsigned int max_connections_; }; +// Test TCPAcceptor::asyncAccept. TEST_F(TCPAcceptorTest, asyncAccept) { + // Establish up to 10 connections. setMaxConnections(10); - TCPEndpoint endpoint(IOAddress("127.0.0.1"), 18123); - acceptor_.open(endpoint); - acceptor_.bind(endpoint); + + // Initialize acceptor. + acceptor_.open(endpoint_); + acceptor_.bind(endpoint_); acceptor_.listen(); + // Start accepting new connections. accept(); + // Create 10 new TCP connections (client side). for (unsigned int i = 0; i < 10; ++i) { connect(); } + // Run the IO service until we have accepted 10 connections, an error + // or test timeout occurred. io_service_.run(); + + // Make sure that all accepted connections have been recorded. + EXPECT_EQ(10, connections_num_); + EXPECT_EQ(10, connections_.size()); +} + +// Check that it is possible to set SO_REUSEADDR flag for the TCPAcceptor. +TEST_F(TCPAcceptorTest, reuseAddress) { + // We need at least two acceptors using common address. Let's create the + // second endpoint which has the same address but different port. + boost::asio::ip::tcp::endpoint asio_endpoint2(createSiblingEndpoint(asio_endpoint_)); + TCPEndpoint endpoint2(asio_endpoint2); + + // Create and open two acceptors. + TestTCPAcceptor acceptor1(io_service_); + TestTCPAcceptor acceptor2(io_service_); + ASSERT_NO_THROW(acceptor1.open(endpoint_)); + ASSERT_NO_THROW(acceptor2.open(endpoint2)); + + // Set SO_REUSEADDR socket option so as acceptors can bind to the + /// same address. + ASSERT_NO_THROW( + acceptor1.setOption(TestTCPAcceptor::ReuseAddress(true)) + ); + ASSERT_NO_THROW( + acceptor2.setOption(TestTCPAcceptor::ReuseAddress(true)) + ); + ASSERT_NO_THROW(acceptor1.bind(endpoint_)); + ASSERT_NO_THROW(acceptor2.bind(endpoint2)); + + // Create third acceptor, but don't set the SO_REUSEADDR. It should + // refuse to bind. + TCPEndpoint endpoint3(createSiblingEndpoint(asio_endpoint2)); + TestTCPAcceptor acceptor3(io_service_); + ASSERT_NO_THROW(acceptor3.open(endpoint3)); + EXPECT_THROW(acceptor3.bind(endpoint_), boost::system::system_error); +} + +// Test that TCPAcceptor::getProtocol returns IPPROTO_TCP. +TEST_F(TCPAcceptorTest, getProtocol) { + EXPECT_EQ(IPPROTO_TCP, acceptor_.getProtocol()); +} + +// Test that TCPAcceptor::getNative returns valid socket descriptor. +TEST_F(TCPAcceptorTest, getNative) { + // Initially the descriptor should be invalid (negative). + ASSERT_LT(acceptor_.getNative(), 0); + // Now open the socket and make sure the returned descriptor is now valid. + ASSERT_NO_THROW(acceptor_.open(endpoint_)); + EXPECT_GE(acceptor_.getNative(), 0); } }