|
@@ -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 <boost/shared_ptr.hpp>
|
|
|
#include <gtest/gtest.h>
|
|
|
#include <list>
|
|
|
+#include <netinet/in.h>
|
|
|
#include <string>
|
|
|
|
|
|
-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<TCPClient> 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_) {
|
|
|
}
|
|
|
|
|
|
+ /// @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() {
|
|
|
- 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();
|
|
|
+ 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<TCPClient> TCPClientPtr;
|
|
|
|
|
|
+/// @brief A signature of the function implementing callback for the
|
|
|
+/// TCPAcceptor.
|
|
|
typedef boost::function<void(const boost::system::error_code&)> TCPAcceptorCallback;
|
|
|
|
|
|
+/// @brief TCPAcceptor using TCPAcceptorCallback.
|
|
|
+typedef TCPAcceptor<TCPAcceptorCallback> 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<TCPAcceptorCallback>& 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<SocketCallback> socket_;
|
|
|
- TCPAcceptor<TCPAcceptorCallback>& 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<Acceptor> 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<TCPAcceptorCallback> 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<AcceptorPtr> connections_;
|
|
|
+
|
|
|
+ /// @brief List of client connections.
|
|
|
std::list<TCPClientPtr> 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);
|
|
|
}
|
|
|
|
|
|
}
|