]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[5448] Added support for persistent connections and keep alives.
authorMarcin Siodelski <marcin@isc.org>
Wed, 13 Dec 2017 13:20:37 +0000 (14:20 +0100)
committerMarcin Siodelski <marcin@isc.org>
Wed, 13 Dec 2017 13:20:37 +0000 (14:20 +0100)
src/bin/agent/ca_process.cc
src/lib/http/connection.cc
src/lib/http/connection.h
src/lib/http/http_messages.mes
src/lib/http/listener.cc
src/lib/http/listener.h
src/lib/http/tests/connection_pool_unittests.cc
src/lib/http/tests/listener_unittests.cc

index e1fd8dac61a7d47bdd23b9a750a7cd7a513e246f..2f2e157bbaed361a44c217bc0d12f02a5eeb81a0 100644 (file)
@@ -26,6 +26,8 @@ namespace {
 
 const long REQUEST_TIMEOUT = 10000;
 
+const long IDLE_TIMEOUT = 30000;
+
 }
 
 namespace isc {
@@ -151,10 +153,11 @@ CtrlAgentProcess::configure(isc::data::ConstElementPtr config_set,
 
             // Create http listener. It will open up a TCP socket and be
             // prepared to accept incoming connection.
-            HttpListenerPtr http_listener(new HttpListener(*getIoService(),
-                                                           server_address,
-                                                           server_port, rcf,
-                                                           REQUEST_TIMEOUT));
+            HttpListenerPtr
+                http_listener(new HttpListener(*getIoService(), server_address,
+                                               server_port, rcf,
+                                               HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                                               HttpListener::IdleTimeout(IDLE_TIMEOUT)));
 
             // Instruct the http listener to actually open socket, install
             // callback and start listening.
index 1e5f5cb2ecf409327350fdb8de2c4fa914e4b723..418715ea8a7680ac6f0246ef1f1f0eac3df025d5 100644 (file)
@@ -30,9 +30,12 @@ HttpConnection:: HttpConnection(asiolink::IOService& io_service,
                                 HttpConnectionPool& connection_pool,
                                 const HttpResponseCreatorPtr& response_creator,
                                 const HttpAcceptorCallback& callback,
-                                const long request_timeout)
+                                const long request_timeout,
+                                const long idle_timeout)
     : request_timer_(io_service),
+      request_timer_setup_(false),
       request_timeout_(request_timeout),
+      idle_timeout_(idle_timeout),
       socket_(io_service),
       acceptor_(acceptor),
       connection_pool_(connection_pool),
@@ -50,6 +53,7 @@ HttpConnection::~HttpConnection() {
 
 void
 HttpConnection::close() {
+    request_timer_setup_ = false;
     request_timer_.cancel();
     socket_.close();
 }
@@ -117,7 +121,13 @@ HttpConnection::doWrite() {
                               output_buf_.length(),
                               cb);
         } else {
-            stopThisConnection();
+            if (!request_->isPersistent()) {
+                stopThisConnection();
+
+            } else {
+                reinitProcessingState();
+                doRead();
+            }
         }
     } catch (const std::exception& ex) {
         stopThisConnection();
@@ -148,13 +158,8 @@ HttpConnection::acceptorCallback(const boost::system::error_code& ec) {
                   HTTP_REQUEST_RECEIVE_START)
             .arg(getRemoteEndpointAddressAsText())
             .arg(static_cast<unsigned>(request_timeout_/1000));
-        // Pass raw pointer rather than shared_ptr to this object,
-        // because IntervalTimer already passes shared pointer to the
-        // IntervalTimerImpl to make sure that the callback remains
-        // valid.
-        request_timer_.setup(boost::bind(&HttpConnection::requestTimeoutCallback,
-                                         this),
-                             request_timeout_, IntervalTimer::ONE_SHOT);
+
+        setupRequestTimer();
         doRead();
     }
 }
@@ -241,10 +246,47 @@ HttpConnection::socketWriteCallback(boost::system::error_code ec, size_t length)
 
     } else {
         output_buf_.clear();
-        stopThisConnection();
+
+        if (!request_->isPersistent()) {
+            stopThisConnection();
+
+        } else {
+            reinitProcessingState();
+            doRead();
+        }
     }
 }
 
+void
+HttpConnection::reinitProcessingState() {
+    request_ = response_creator_->createNewHttpRequest();
+    parser_.reset(new HttpRequestParser(*request_));
+    parser_->initModel();
+    setupIdleTimer();
+}
+
+void
+HttpConnection::setupRequestTimer() {
+    // Pass raw pointer rather than shared_ptr to this object,
+    // because IntervalTimer already passes shared pointer to the
+    // IntervalTimerImpl to make sure that the callback remains
+    // valid.
+    if (!request_timer_setup_) {
+        request_timer_setup_ = true;
+        request_timer_.setup(boost::bind(&HttpConnection::requestTimeoutCallback,
+                                         this),
+                             request_timeout_, IntervalTimer::ONE_SHOT);
+    }
+}
+
+void
+HttpConnection::setupIdleTimer() {
+    request_timer_setup_ = false;
+    request_timer_.setup(boost::bind(&HttpConnection::idleTimeoutCallback,
+                                     this),
+                         idle_timeout_, IntervalTimer::ONE_SHOT);
+}
+
 void
 HttpConnection::requestTimeoutCallback() {
     LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL,
@@ -256,6 +298,14 @@ HttpConnection::requestTimeoutCallback() {
     asyncSendResponse(response);
 }
 
+void
+HttpConnection::idleTimeoutCallback() {
+    LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL,
+              HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED)
+        .arg(getRemoteEndpointAddressAsText());
+    stopThisConnection();
+}
+
 std::string
 HttpConnection::getRemoteEndpointAddressAsText() const {
     try {
index 6305d5515912cbd2355e831a6c819531ec31c01f..fd387244e98d36ce7f9a9cc04d5dbf7cddb57626 100644 (file)
@@ -92,12 +92,15 @@ public:
     /// create HTTP response from the HTTP request received.
     /// @param callback Callback invoked when new connection is accepted.
     /// @param request_timeout Configured timeout for a HTTP request.
+    /// @param idle_timeout Timeout after which persistent HTTP connection is
+    /// closed by the server.
     HttpConnection(asiolink::IOService& io_service,
                    HttpAcceptor& acceptor,
                    HttpConnectionPool& connection_pool,
                    const HttpResponseCreatorPtr& response_creator,
                    const HttpAcceptorCallback& callback,
-                   const long request_timeout);
+                   const long request_timeout,
+                   const long idle_timeout);
 
     /// @brief Destructor.
     ///
@@ -166,12 +169,28 @@ private:
     void socketWriteCallback(boost::system::error_code ec,
                              size_t length);
 
+    /// @brief Reinitializes request processing state after sending a response.
+    ///
+    /// This method is only called for persistent connections, when the response
+    /// to a previous command has been sent. It initializes the state machine to
+    /// be able to process the next request. It also sets the persistent connection
+    /// idle timer to monitor the connection timeout.
+    void reinitProcessingState();
+
+    /// @brief Reset timer for detecting request timeouts.
+    void setupRequestTimer();
+
+    /// @brief Reset timer for detecing idle timeout in persistent connections.
+    void setupIdleTimer();
+
     /// @brief Callback invoked when the HTTP Request Timeout occurs.
     ///
     /// This callback creates HTTP response with Request Timeout error code
     /// and sends it to the client.
     void requestTimeoutCallback();
 
+    void idleTimeoutCallback();
+
     /// @brief Stops current connection.
     void stopThisConnection();
 
@@ -181,9 +200,15 @@ private:
     /// @brief Timer used to detect Request Timeout.
     asiolink::IntervalTimer request_timer_;
 
+    bool request_timer_setup_;
+
     /// @brief Configured Request Timeout in milliseconds.
     long request_timeout_;
 
+    /// @brief Timeout after which the persistent HTTP connection is closed
+    /// by the server.
+    long idle_timeout_;
+
     /// @brief Socket used by this connection.
     asiolink::TCPSocket<SocketCallback> socket_;
 
index ad317fbf0b961669b984dd3ced79e8fcb0a66117..414eab53df298593d374152a7a91b7d404df6c1d 100644 (file)
@@ -22,6 +22,10 @@ of the request. The first argument specifies the amount of received data.
 The second argument specifies an address of the remote endpoint which
 produced the data.
 
+% HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED closing persistent connection with %1 as a result of a timeout
+This debug message is issued when the persistent HTTP connection is being
+closed as a result of being idle.
+
 % HTTP_REQUEST_RECEIVED received HTTP request from %1
 This debug message is issued when the server finished receiving a HTTP
 request from the remote endpoint. The address of the remote endpoint is
index b0e9d2c0613660b5e2e4453141d591e12d33c749..643376942fad62390adc4d1ee0c4700376c75611 100644 (file)
@@ -37,6 +37,8 @@ public:
     /// create @ref HttpResponseCreator instances.
     /// @param request_timeout Timeout after which the HTTP Request Timeout
     /// is generated.
+    /// @param idle_timeout Timeout after which an idle persistent HTTP
+    /// connection is closed by the server.
     ///
     /// @throw HttpListenerError when any of the specified parameters is
     /// invalid.
@@ -44,7 +46,8 @@ public:
                      const asiolink::IOAddress& server_address,
                      const unsigned short server_port,
                      const HttpResponseCreatorFactoryPtr& creator_factory,
-                     const long request_timeout);
+                     const long request_timeout,
+                     const long idle_timeout);
 
     /// @brief Returns reference to the current listener endpoint.
     const TCPEndpoint& getEndpoint() const;
@@ -97,16 +100,21 @@ private:
 
     /// @brief Timeout for HTTP Request Timeout desired.
     long request_timeout_;
+
+    /// @brief Timeout after which idle persistent connection is closed by
+    /// the server.
+    long idle_timeout_;
 };
 
 HttpListenerImpl::HttpListenerImpl(IOService& io_service,
                                    const asiolink::IOAddress& server_address,
                                    const unsigned short server_port,
                                    const HttpResponseCreatorFactoryPtr& creator_factory,
-                                   const long request_timeout)
+                                   const long request_timeout,
+                                   const long idle_timeout)
     : io_service_(io_service), acceptor_(io_service),
       endpoint_(), creator_factory_(creator_factory),
-      request_timeout_(request_timeout) {
+      request_timeout_(request_timeout), idle_timeout_(idle_timeout) {
     // Try creating an endpoint. This may cause exceptions.
     try {
         endpoint_.reset(new TCPEndpoint(server_address, server_port));
@@ -127,6 +135,12 @@ HttpListenerImpl::HttpListenerImpl(IOService& io_service,
         isc_throw(HttpListenerError, "Invalid desired HTTP request timeout "
                   << request_timeout_);
     }
+
+    // Idle persistent connection timeout is signed and must be greater than 0.
+    if (idle_timeout_ <= 0) {
+        isc_throw(HttpListenerError, "Invalid desired HTTP idle persistent connection"
+                  " timeout " << idle_timeout_);
+    }
 }
 
 const TCPEndpoint&
@@ -169,7 +183,8 @@ HttpListenerImpl::accept() {
                                               connections_,
                                               response_creator,
                                               acceptor_callback,
-                                              request_timeout_));
+                                              request_timeout_,
+                                              idle_timeout_));
     // Add this new connection to the pool.
     connections_.start(conn);
 }
@@ -185,9 +200,11 @@ HttpListener::HttpListener(IOService& io_service,
                            const asiolink::IOAddress& server_address,
                            const unsigned short server_port,
                            const HttpResponseCreatorFactoryPtr& creator_factory,
-                           const long request_timeout)
+                           const HttpListener::RequestTimeout& request_timeout,
+                           const HttpListener::IdleTimeout& idle_timeout)
     : impl_(new HttpListenerImpl(io_service, server_address, server_port,
-                                 creator_factory, request_timeout)) {
+                                 creator_factory, request_timeout.value_,
+                                 idle_timeout.value_)) {
 }
 
 HttpListener::~HttpListener() {
index 1659d8868bb279d1114936e2e0dfb5c365c1807c..7c41bc200774991ec993db66224fd1d699316649 100644 (file)
@@ -51,6 +51,28 @@ class HttpListenerImpl;
 class HttpListener {
 public:
 
+    /// @brief HTTP request timeout value.
+    struct RequestTimeout {
+        /// @brief Constructor.
+        ///
+        /// @param value Request timeout value in milliseconds.
+        explicit RequestTimeout(long value)
+            : value_(value) {
+        }
+        long value_; ///< Request timeout value specified.
+    };
+
+    /// @brief Idle connection timeout.
+    struct IdleTimeout {
+        /// @brief Constructor.
+        ///
+        /// @param value Connection idle timeout value in milliseconds.
+        explicit IdleTimeout(long value)
+            : value_(value) {
+        }
+        long value_; ///< Connection idle timeout value specified.
+    };
+
     /// @brief Constructor.
     ///
     /// This constructor creates new server endpoint using the specified IP
@@ -67,6 +89,8 @@ public:
     /// create @ref HttpResponseCreator instances.
     /// @param request_timeout Timeout after which the HTTP Request Timeout
     /// is generated.
+    /// @param idle_timeout Timeout after which an idle persistent HTTP
+    /// connection is closed by the server.
     ///
     /// @throw HttpListenerError when any of the specified parameters is
     /// invalid.
@@ -74,7 +98,8 @@ public:
                  const asiolink::IOAddress& server_address,
                  const unsigned short server_port,
                  const HttpResponseCreatorFactoryPtr& creator_factory,
-                 const long request_timeout);
+                 const RequestTimeout& request_timeout,
+                 const IdleTimeout& idle_timeout);
 
     /// @brief Destructor.
     ///
index 3cf2797e202268545bc193d0b319401be6538c80..42fda5ff227c039a1de007664bf303e3f81c997e 100644 (file)
@@ -30,6 +30,12 @@ typedef TestHttpResponseBase<HttpResponseJson> Response;
 /// @brief Pointer to test HTTP response.
 typedef boost::shared_ptr<Response> ResponsePtr;
 
+/// @brief Request timeout used in tests.
+const long CONN_REQUEST_TIMEOUT = 1000;
+
+/// @brief Idle connecion timeout used in tests.
+const long CONN_IDLE_TIMEOUT = 1000;
+
 /// @brief Implementation of the @ref HttpResponseCreator.
 class TestHttpResponseCreator : public HttpResponseCreator {
 public:
@@ -114,12 +120,14 @@ TEST_F(HttpConnectionPoolTest, startStop) {
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_,
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     // The pool should be initially empty.
     TestHttpConnectionPool pool;
     ASSERT_TRUE(pool.connections_.empty());
@@ -152,12 +160,14 @@ TEST_F(HttpConnectionPoolTest, stopAll) {
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_,
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     TestHttpConnectionPool pool;
     ASSERT_NO_THROW(pool.start(conn1));
     ASSERT_NO_THROW(pool.start(conn2));
@@ -176,12 +186,14 @@ TEST_F(HttpConnectionPoolTest, stopInvalid) {
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_,
                                                connection_pool_,
                                                response_creator_,
                                                HttpAcceptorCallback(),
-                                               1000));
+                                               CONN_REQUEST_TIMEOUT,
+                                               CONN_IDLE_TIMEOUT));
     TestHttpConnectionPool pool;
     ASSERT_NO_THROW(pool.start(conn1));
     ASSERT_NO_THROW(pool.stop(conn2));
index 3b6aa025ce2823dabc8ebd424ccb195608a0ab74..28f7e8a1c3a30c9a4e2458bfc9281b33f47d72ee 100644 (file)
@@ -7,6 +7,7 @@
 #include <config.h>
 #include <asiolink/asio_wrapper.h>
 #include <asiolink/interval_timer.h>
+#include <http/http_types.h>
 #include <http/listener.h>
 #include <http/post_request_json.h>
 #include <http/response_creator.h>
@@ -18,6 +19,7 @@
 #include <boost/bind.hpp>
 #include <gtest/gtest.h>
 #include <list>
+#include <sstream>
 #include <string>
 
 using namespace boost::asio::ip;
@@ -36,6 +38,9 @@ const unsigned short SERVER_PORT = 18123;
 /// @brief Request Timeout used in most of the tests (ms).
 const long REQUEST_TIMEOUT = 10000;
 
+/// @brief Persistent connection idle timeout used in most of the tests (ms).
+const long IDLE_TIMEOUT = 10000;
+
 /// @brief Test timeout (ms).
 const long TEST_TIMEOUT = 10000;
 
@@ -244,6 +249,36 @@ public:
         });
     }
 
+    /// @brief Checks if the TCP connection is still open.
+    ///
+    /// Tests the TCP connection by trying to read from the socket.
+    ///
+    /// @return true if the TCP connection is open.
+    bool isConnectionAlive() {
+        // Remember the current non blocking setting.
+        const bool non_blocking_orig = socket_.non_blocking();
+        // Set the socket to non blocking mode. We're going to test if the socket
+        // returns would_block status on the attempt to read from it.
+        socket_.non_blocking(true);
+
+        // We need to provide a buffer for a call to read.
+        char data[2];
+        boost::system::error_code ec;
+        boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec);
+
+        // Revert the original non_blocking flag on the socket.
+        socket_.non_blocking(non_blocking_orig);
+
+        // If the connection is alive we'd typically get would_block status code.
+        // If there are any data that haven't been read we may also get success
+        // status. We're guessing that try_again may also be returned by some
+        // implementations in some situations. Any other error code indicates a
+        // problem with the connection so we assume that the connection has been
+        // closed.
+        return (!ec || (ec.value() == boost::asio::error::try_again) ||
+                (ec.value() == boost::asio::error::would_block));
+    }
+
     /// @brief Close connection.
     void close() {
         socket_.close();
@@ -280,8 +315,8 @@ public:
     /// Starts test timer which detects timeouts.
     HttpListenerTest()
         : io_service_(), factory_(new TestHttpResponseCreatorFactory()),
-          test_timer_(io_service_), clients_() {
-        test_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler, this),
+          test_timer_(io_service_), run_io_service_timer_(io_service_), clients_() {
+        test_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler, this, true),
                           TEST_TIMEOUT, IntervalTimer::ONE_SHOT);
     }
 
@@ -299,6 +334,8 @@ public:
     ///
     /// This method creates HttpClient instance and retains it in the clients_
     /// list.
+    ///
+    /// @param request String containing the HTTP request to be sent.
     void startRequest(const std::string& request) {
         HttpClientPtr client(new HttpClient(io_service_));
         clients_.push_back(client);
@@ -308,11 +345,45 @@ 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!";
+    ///
+    /// @param fail_on_timeout Specifies if test failure should be reported.
+    void timeoutHandler(const bool fail_on_timeout) {
+        if (fail_on_timeout) {
+            ADD_FAILURE() << "Timeout occurred while running the test!";
+        }
         io_service_.stop();
     }
 
+    /// @brief Runs IO service with optional timeout.
+    ///
+    /// @param timeout Optional value specifying for how long the io service
+    /// should be ran.
+    void runIOService(long timeout = 0) {
+        if (timeout > 0) {
+            run_io_service_timer_.setup(boost::bind(&HttpListenerTest::timeoutHandler,
+                                                    this, false),
+                                        timeout, IntervalTimer::ONE_SHOT);
+        }
+        io_service_.run();
+        io_service_.get_io_service().reset();
+        io_service_.poll();
+    }
+
+    /// @brief Returns HTTP OK response expected by unit tests.
+    ///
+    /// @param http_version HTTP version.
+    ///
+    /// @return HTTP OK response expected by unit tests.
+    std::string httpOk(const HttpVersion& http_version) {
+        std::ostringstream s;
+        s << "HTTP/" << http_version.major_ << "." << http_version.minor_ << " 200 OK\r\n"
+            "Content-Length: 0\r\n"
+            "Content-Type: application/json\r\n"
+            "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n"
+            "\r\n";
+        return (s.str());
+    }
+
     /// @brief IO service used in the tests.
     IOService io_service_;
 
@@ -322,6 +393,10 @@ public:
     /// @brief Asynchronous timer service to detect timeouts.
     IntervalTimer test_timer_;
 
+    /// @brief Asynchronous timer for running IO service for a specified amount
+    /// of time.
+    IntervalTimer run_io_service_timer_;
+
     /// @brief List of client connections.
     std::list<HttpClientPtr> clients_;
 };
@@ -335,21 +410,231 @@ TEST_F(HttpListenerTest, listen) {
         "{ }";
 
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
-                          factory_, REQUEST_TIMEOUT);
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
     ASSERT_NO_THROW(listener.start());
     ASSERT_EQ(SERVER_ADDRESS, listener.getLocalAddress().toText());
     ASSERT_EQ(SERVER_PORT, listener.getLocalPort());
     ASSERT_NO_THROW(startRequest(request));
-    ASSERT_NO_THROW(io_service_.run());
+    ASSERT_NO_THROW(runIOService());
     ASSERT_EQ(1, clients_.size());
     HttpClientPtr client = *clients_.begin();
     ASSERT_TRUE(client);
-    EXPECT_EQ("HTTP/1.1 200 OK\r\n"
-              "Content-Length: 0\r\n"
-              "Content-Type: application/json\r\n"
-              "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n"
-              "\r\n",
-              client->getResponse());
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
+
+    listener.stop();
+    io_service_.poll();
+}
+
+// This test verifies that persistent HTTP connection can be established when
+// "Conection: Keep-Alive" header value is specified.
+TEST_F(HttpListenerTest, keepAlive) {
+
+    // The first request contains the keep-alive header which instructs the server
+    // to maintain the TCP connection after sending a response.
+    std::string request = "POST /foo/bar HTTP/1.0\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n"
+        "Connection: Keep-Alive\r\n\r\n"
+        "{ }";
+
+    HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
+
+    ASSERT_NO_THROW(listener.start());
+
+    // Send the request with the keep-alive header.
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    HttpClientPtr client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
+
+    // We have sent keep-alive header so we expect that the connection with
+    // the server remains active.
+    ASSERT_TRUE(client->isConnectionAlive());
+
+    // Test that we can send another request via the same connection. This time
+    // it lacks the keep-alive header, so the server should close the connection
+    // after sending the response.
+    request = "POST /foo/bar HTTP/1.0\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n\r\n"
+        "{ }";
+
+    // Send request reusing the existing connection.
+    ASSERT_NO_THROW(client->sendRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
+
+    // Connection should have been closed by the server.
+    EXPECT_FALSE(client->isConnectionAlive());
+
+    listener.stop();
+    io_service_.poll();
+}
+
+// This test verifies that persistent HTTP connection is established by default
+// when HTTP/1.1 is in use.
+TEST_F(HttpListenerTest, persistentConnection) {
+
+    // The HTTP/1.1 requests are by default persistent.
+    std::string request = "POST /foo/bar HTTP/1.1\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n\r\n"
+        "{ }";
+
+    HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
+
+    ASSERT_NO_THROW(listener.start());
+
+    // Send the first request.
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    HttpClientPtr client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
+
+    // HTTP/1.1 connection is persistent by default.
+    ASSERT_TRUE(client->isConnectionAlive());
+
+    // Test that we can send another request via the same connection. This time
+    // it includes the "Connection: close" header which instructs the server to
+    // close the connection after responding.
+    request = "POST /foo/bar HTTP/1.1\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n"
+        "Connection: close\r\n\r\n"
+        "{ }";
+
+    // Send request reusing the existing connection.
+    ASSERT_NO_THROW(client->sendRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
+
+    // Connection should have been closed by the server.
+    EXPECT_FALSE(client->isConnectionAlive());
+
+    listener.stop();
+    io_service_.poll();
+}
+
+// This test verifies that "keep-alive" connection is closed by the server after
+// an idle time.
+TEST_F(HttpListenerTest, keepAliveTimeout) {
+
+    // The first request contains the keep-alive header which instructs the server
+    // to maintain the TCP connection after sending a response.
+    std::string request = "POST /foo/bar HTTP/1.0\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n"
+        "Connection: Keep-Alive\r\n\r\n"
+        "{ }";
+
+    // Specify the idle timeout of 500ms.
+    HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(500));
+
+    ASSERT_NO_THROW(listener.start());
+
+    // Send the request with the keep-alive header.
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    HttpClientPtr client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
+
+    // We have sent keep-alive header so we expect that the connection with
+    // the server remains active.
+    ASSERT_TRUE(client->isConnectionAlive());
+
+    // Run IO service for 1000ms. The idle time is set to 500ms, so the connection
+    // should be closed by the server while we wait here.
+    runIOService(1000);
+
+    // Make sure the connection has been closed.
+    EXPECT_FALSE(client->isConnectionAlive());
+
+    // Check if we can re-establish the connection and send another request.
+    clients_.clear();
+    request = "POST /foo/bar HTTP/1.0\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n\r\n"
+        "{ }";
+
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse());
+
+    EXPECT_FALSE(client->isConnectionAlive());
+
+    listener.stop();
+    io_service_.poll();
+}
+
+// This test verifies that persistent connection is closed by the server after
+// an idle time.
+TEST_F(HttpListenerTest, persistentConnectionTimeout) {
+
+    // The HTTP/1.1 requests are by default persistent.
+    std::string request = "POST /foo/bar HTTP/1.1\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n"
+        "Connection: Keep-Alive\r\n\r\n"
+        "{ }";
+
+    // Specify the idle timeout of 500ms.
+    HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(500));
+
+    ASSERT_NO_THROW(listener.start());
+
+    // Send the request.
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    HttpClientPtr client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
+
+    // The connection should remain active.
+    ASSERT_TRUE(client->isConnectionAlive());
+
+    // Run IO service for 1000ms. The idle time is set to 500ms, so the connection
+    // should be closed by the server while we wait here.
+    runIOService(1000);
+
+    // Make sure the connection has been closed.
+    EXPECT_FALSE(client->isConnectionAlive());
+
+    // Check if we can re-establish the connection and send another request.
+    clients_.clear();
+    request = "POST /foo/bar HTTP/1.1\r\n"
+        "Content-Type: application/json\r\n"
+        "Content-Length: 3\r\n"
+        "Connection: close\r\n\r\n"
+        "{ }";
+
+    ASSERT_NO_THROW(startRequest(request));
+    ASSERT_NO_THROW(runIOService());
+    ASSERT_EQ(1, clients_.size());
+    client = *clients_.begin();
+    ASSERT_TRUE(client);
+    EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse());
+
+    EXPECT_FALSE(client->isConnectionAlive());
+
     listener.stop();
     io_service_.poll();
 }
@@ -357,7 +642,8 @@ TEST_F(HttpListenerTest, listen) {
 // This test verifies that the HTTP listener can't be started twice.
 TEST_F(HttpListenerTest, startTwice) {
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
-                          factory_, REQUEST_TIMEOUT);
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
     ASSERT_NO_THROW(listener.start());
     EXPECT_THROW(listener.start(), HttpListenerError);
 }
@@ -372,10 +658,11 @@ TEST_F(HttpListenerTest, badRequest) {
         "{ }";
 
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
-                          factory_, REQUEST_TIMEOUT);
+                          factory_, HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
     ASSERT_NO_THROW(listener.start());
     ASSERT_NO_THROW(startRequest(request));
-    ASSERT_NO_THROW(io_service_.run());
+    ASSERT_NO_THROW(runIOService());
     ASSERT_EQ(1, clients_.size());
     HttpClientPtr client = *clients_.begin();
     ASSERT_TRUE(client);
@@ -393,7 +680,8 @@ TEST_F(HttpListenerTest, badRequest) {
 TEST_F(HttpListenerTest, invalidFactory) {
     EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS),
                               SERVER_PORT, HttpResponseCreatorFactoryPtr(),
-                              REQUEST_TIMEOUT),
+                              HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                              HttpListener::IdleTimeout(IDLE_TIMEOUT)),
                  HttpListenerError);
 }
 
@@ -401,7 +689,18 @@ TEST_F(HttpListenerTest, invalidFactory) {
 // Request Timeout.
 TEST_F(HttpListenerTest, invalidRequestTimeout) {
     EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS),
-                              SERVER_PORT, factory_, 0),
+                              SERVER_PORT, factory_, HttpListener::RequestTimeout(0),
+                              HttpListener::IdleTimeout(IDLE_TIMEOUT)),
+                 HttpListenerError);
+}
+
+// This test verifies that the timeout of 0 can't be specified for the
+// idle persistent connection timeout.
+TEST_F(HttpListenerTest, invalidIdleTimeout) {
+    EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS),
+                              SERVER_PORT, factory_,
+                              HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                              HttpListener::IdleTimeout(0)),
                  HttpListenerError);
 }
 
@@ -419,7 +718,9 @@ TEST_F(HttpListenerTest, addressInUse) {
     // Listener should report an error when we try to start it because another
     // acceptor is bound to that port and address.
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS),
-                          SERVER_PORT + 1, factory_, REQUEST_TIMEOUT);
+                          SERVER_PORT + 1, factory_,
+                          HttpListener::RequestTimeout(REQUEST_TIMEOUT),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
     EXPECT_THROW(listener.start(), HttpListenerError);
 }
 
@@ -435,10 +736,11 @@ TEST_F(HttpListenerTest, requestTimeout) {
     // Open the listener with the Request Timeout of 1 sec and post the
     // partial request.
     HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT,
-                          factory_, 1000);
+                          factory_, HttpListener::RequestTimeout(1000),
+                          HttpListener::IdleTimeout(IDLE_TIMEOUT));
     ASSERT_NO_THROW(listener.start());
     ASSERT_NO_THROW(startRequest(request));
-    ASSERT_NO_THROW(io_service_.run());
+    ASSERT_NO_THROW(runIOService());
     ASSERT_EQ(1, clients_.size());
     HttpClientPtr client = *clients_.begin();
     ASSERT_TRUE(client);