From e2bf8aaaf4997be7217ed5d0bde8daa88e45c4a2 Mon Sep 17 00:00:00 2001 From: Adam Gu Date: Mon, 9 Apr 2018 16:53:47 +0800 Subject: [PATCH] Add timeout control to http server (draft) --- CMakeLists.txt | 7 +- src/demo/rest/book_client/book_client.cc | 2 - src/demo/rest/book_client/book_client.h | 6 -- src/demo/rest/book_client/main.cc | 1 - src/demo/rest/book_server/book_services.cc | 42 +++++++++--- src/demo/rest/book_server/main.cc | 5 ++ src/webcc/http_server.cc | 15 +++-- src/webcc/http_server.h | 14 ++-- src/webcc/http_session.cc | 75 +++++++++++++++++++--- src/webcc/http_session.h | 20 ++++-- src/webcc/rest_server.cc | 8 +-- src/webcc/rest_server.h | 7 +- src/webcc/soap_server.cc | 8 +-- src/webcc/soap_server.h | 7 +- 14 files changed, 161 insertions(+), 56 deletions(-) delete mode 100644 src/demo/rest/book_client/book_client.cc delete mode 100644 src/demo/rest/book_client/book_client.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d389f8..7848473 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,7 +65,12 @@ find_package(Threads REQUIRED) # Boost version: 1.66+ set(Boost_USE_STATIC_LIBS ON) set(Boost_USE_MULTITHREADED ON) -find_package(Boost 1.66.0 REQUIRED COMPONENTS system thread) +if(WIN32) + # TODO: Fix COMPONENTS on Windows. + find_package(Boost 1.66.0 REQUIRED) +else() + find_package(Boost 1.66.0 REQUIRED COMPONENTS system thread) +endif() if(Boost_FOUND) include_directories(${Boost_INCLUDE_DIRS}) link_directories(${Boost_LIBRARY_DIRS}) diff --git a/src/demo/rest/book_client/book_client.cc b/src/demo/rest/book_client/book_client.cc deleted file mode 100644 index f46eb3c..0000000 --- a/src/demo/rest/book_client/book_client.cc +++ /dev/null @@ -1,2 +0,0 @@ -#include "book_client.h" - diff --git a/src/demo/rest/book_client/book_client.h b/src/demo/rest/book_client/book_client.h deleted file mode 100644 index bcba55b..0000000 --- a/src/demo/rest/book_client/book_client.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef BOOK_CLIENT_H_ -#define BOOK_CLIENT_H_ - -#include - -#endif // BOOK_CLIENT_H_ diff --git a/src/demo/rest/book_client/main.cc b/src/demo/rest/book_client/main.cc index c52b9f6..565ce76 100644 --- a/src/demo/rest/book_client/main.cc +++ b/src/demo/rest/book_client/main.cc @@ -1,5 +1,4 @@ #include -#include "boost/lexical_cast.hpp" #include "webcc/http_client.h" #include "webcc/http_request.h" diff --git a/src/demo/rest/book_server/book_services.cc b/src/demo/rest/book_server/book_services.cc index 85c90e2..30cbca3 100644 --- a/src/demo/rest/book_server/book_services.cc +++ b/src/demo/rest/book_server/book_services.cc @@ -1,7 +1,10 @@ #include "book_services.h" #include + #include "boost/lexical_cast.hpp" +#include "boost/thread.hpp" +#include "boost/date_time/posix_time/posix_time.hpp" //////////////////////////////////////////////////////////////////////////////// @@ -21,6 +24,7 @@ static const Book kNullBook{}; class BookStore { public: BookStore() { + // Prepare test data. books_.push_back({ "1", "Title1", 11.1 }); books_.push_back({ "2", "Title2", 22.2 }); books_.push_back({ "3", "Title3", 33.3 }); @@ -77,8 +81,8 @@ static BookStore g_book_store; //////////////////////////////////////////////////////////////////////////////// -// Naively create JSON object string for a book. -// You should use real JSON library in your product code. +// Naively create JSON object for a book. +// You should use real JSON library in real product. static std::string CreateBookJson(const Book& book) { std::string json = "{ "; json += "\"id\": " + book.id + ", "; @@ -88,17 +92,36 @@ static std::string CreateBookJson(const Book& book) { return json; } +// Naively create JSON array object for a list of books. +// You should use real JSON library in real product. +static std::string CreateBookListJson(const std::list& books) { + std::string json = "[ "; + + for (const Book& book : books) { + json += CreateBookJson(book); + json += ","; + } + + // Remove last ','. + if (!books.empty()) { + json[json.size() - 1] = ' '; + } + + json += "]"; + + return json; +} + bool BookListService::Handle(const std::string& http_method, const std::vector& url_sub_matches, const std::string& request_content, std::string* response_content) { if (http_method == webcc::kHttpGet) { - *response_content = "{ "; - for (const Book& book : g_book_store.books()) { - *response_content += CreateBookJson(book); - *response_content += ","; - } - *response_content += " }"; + *response_content = CreateBookListJson(g_book_store.books()); + + // Sleep for testing timeout control. + boost::this_thread::sleep(boost::posix_time::seconds(2)); + return true; } @@ -126,6 +149,9 @@ bool BookDetailService::Handle(const std::string& http_method, *response_content = CreateBookJson(book); + // Sleep for testing timeout control. + //boost::this_thread::sleep(boost::posix_time::seconds(2)); + return true; } else if (http_method == webcc::kHttpPost) { diff --git a/src/demo/rest/book_server/main.cc b/src/demo/rest/book_server/main.cc index 937f1f7..3d540dc 100644 --- a/src/demo/rest/book_server/main.cc +++ b/src/demo/rest/book_server/main.cc @@ -27,6 +27,11 @@ int main(int argc, char* argv[]) { server.RegisterService(std::make_shared(), "/books/(\\d+)"); + // For test purpose. + // Timeout like 60s makes more sense in a real product. + // Leave it as default (0) for no timeout control. + server.set_timeout_seconds(1); + server.Run(); } catch (std::exception& e) { diff --git a/src/webcc/http_server.cc b/src/webcc/http_server.cc index 226de10..56c29d4 100644 --- a/src/webcc/http_server.cc +++ b/src/webcc/http_server.cc @@ -16,7 +16,8 @@ namespace webcc { HttpServer::HttpServer(unsigned short port, std::size_t workers) : signals_(io_context_) - , workers_(workers) { + , workers_(workers) + , timeout_seconds_(0) { // Register to handle the signals that indicate when the server should exit. // It is safe to register for the same signal multiple times in a program, @@ -47,7 +48,7 @@ HttpServer::~HttpServer() { } void HttpServer::Run() { - assert(request_handler_ != NULL); + assert(GetRequestHandler() != NULL); #if WEBCC_DEBUG_OUTPUT boost::thread::id thread_id = boost::this_thread::get_id(); @@ -55,7 +56,7 @@ void HttpServer::Run() { #endif // Start worker threads. - request_handler_->Start(workers_); + GetRequestHandler()->Start(workers_); // The io_context::run() call will block until all asynchronous operations // have finished. While the server is running, there is always at least one @@ -74,10 +75,10 @@ void HttpServer::DoAccept() { } if (!ec) { - HttpSessionPtr conn{ - new HttpSession(std::move(socket), request_handler_) + HttpSessionPtr session{ + new HttpSession(std::move(socket), GetRequestHandler()) }; - conn->Start(); + session->Start(timeout_seconds_); } DoAccept(); @@ -91,7 +92,7 @@ void HttpServer::DoAwaitStop() { // operations. Once all operations have finished the io_context::run() // call will exit. acceptor_->close(); - request_handler_->Stop(); + GetRequestHandler()->Stop(); }); } diff --git a/src/webcc/http_server.h b/src/webcc/http_server.h index 490ceb9..a78c827 100644 --- a/src/webcc/http_server.h +++ b/src/webcc/http_server.h @@ -28,6 +28,10 @@ public: virtual ~HttpServer(); + void set_timeout_seconds(long seconds) { + timeout_seconds_ = seconds; + } + // Run the server's io_service loop. void Run(); @@ -38,10 +42,8 @@ private: // Wait for a request to stop the server. void DoAwaitStop(); -protected: - // The handler for all incoming requests. - // TODO: Replace with virtual GetRequestHandler()? - HttpRequestHandler* request_handler_; + // Get the handler for incoming requests. + virtual HttpRequestHandler* GetRequestHandler() = 0; private: // The number of worker threads. @@ -55,6 +57,10 @@ private: // Acceptor used to listen for incoming connections. boost::scoped_ptr acceptor_; + + // Timeout in seconds for socket connection. + // Default is 0 which means no timeout. + long timeout_seconds_; }; } // namespace webcc diff --git a/src/webcc/http_session.cc b/src/webcc/http_session.cc index 839aa42..d81f5cd 100644 --- a/src/webcc/http_session.cc +++ b/src/webcc/http_session.cc @@ -6,6 +6,7 @@ #endif #include "boost/asio/write.hpp" +#include "boost/date_time/posix_time/posix_time.hpp" #include "webcc/http_request_handler.h" @@ -18,12 +19,30 @@ HttpSession::HttpSession(boost::asio::ip::tcp::socket socket, , request_parser_(&request_) { } -void HttpSession::Start() { +HttpSession::~HttpSession() { +} + +void HttpSession::Start(long timeout_seconds) { + if (timeout_seconds > 0) { + // Create timer only necessary. + boost::asio::io_context& ioc = socket_.get_executor().context(); + timer_.reset(new boost::asio::deadline_timer(ioc)); + + timer_->expires_from_now(boost::posix_time::seconds(timeout_seconds)); + + timer_->async_wait(std::bind(&HttpSession::HandleTimer, + shared_from_this(), + std::placeholders::_1)); + } + DoRead(); } void HttpSession::Stop() { - socket_.close(); + CancelTimer(); + + boost::system::error_code ignored_ec; + socket_.close(ignored_ec); } void HttpSession::SetResponseContent(const std::string& content_type, @@ -59,6 +78,8 @@ void HttpSession::HandleRead(boost::system::error_code ec, if (ec) { if (ec != boost::asio::error::operation_aborted) { Stop(); + } else { + CancelTimer(); } return; } @@ -89,20 +110,58 @@ void HttpSession::HandleRead(boost::system::error_code ec, // io_context.run), even though DoWrite() is invoked by worker threads. This is // ensured by Asio. void HttpSession::HandleWrite(boost::system::error_code ec, - size_t length) { + std::size_t length) { #if WEBCC_DEBUG_OUTPUT boost::thread::id thread_id = boost::this_thread::get_id(); - std::cout << "Response has been sent back (thread: " << thread_id << ")\n"; #endif if (!ec) { + CancelTimer(); + // Initiate graceful connection closure. - boost::system::error_code ignored_ec; - socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignored_ec); + boost::system::error_code ec; + socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); + +#if WEBCC_DEBUG_OUTPUT + std::cout << "Response has been sent back (thread: " << thread_id << ")\n"; +#endif + } else { +#if WEBCC_DEBUG_OUTPUT + std::cout << "(thread: " << thread_id << ") Sending response error: " + << ec.message() + << std::endl; +#endif + + if (ec == boost::asio::error::operation_aborted) { + CancelTimer(); + } else { + Stop(); + } } +} + +void HttpSession::HandleTimer(boost::system::error_code ec) { + std::cout << "HandleTimer: "; + + if (!ec) { + if (socket_.is_open()) { + std::cout << "socket is open, close it.\n"; + socket_.close(); + } else { + std::cout << "socket is not open.\n"; + } + } else { + if (ec == boost::asio::error::operation_aborted) { + std::cout << "Timer aborted\n"; + } + } +} - if (ec != boost::asio::error::operation_aborted) { - Stop(); +void HttpSession::CancelTimer() { + if (timer_) { + // The wait handler will be invoked with the operation_aborted error code. + boost::system::error_code ec; + timer_->cancel(ec); } } diff --git a/src/webcc/http_session.h b/src/webcc/http_session.h index 73e78da..a94460b 100644 --- a/src/webcc/http_session.h +++ b/src/webcc/http_session.h @@ -5,6 +5,7 @@ #include #include "boost/asio/ip/tcp.hpp" // for ip::tcp::socket +#include "boost/asio/deadline_timer.hpp" #include "webcc/common.h" #include "webcc/http_request.h" @@ -17,19 +18,20 @@ class HttpRequestHandler; class HttpSession : public std::enable_shared_from_this { public: - friend class HttpRequestHandler; - HttpSession(const HttpSession&) = delete; HttpSession& operator=(const HttpSession&) = delete; HttpSession(boost::asio::ip::tcp::socket socket, HttpRequestHandler* handler); + ~HttpSession(); + const HttpRequest& request() const { return request_; } - void Start(); + // Start the session with an optional timeout. + void Start(long timeout_seconds = 0); void Stop(); @@ -49,16 +51,20 @@ private: void DoWrite(); - void HandleRead(boost::system::error_code ec, - std::size_t length); + void HandleRead(boost::system::error_code ec, std::size_t length); + void HandleWrite(boost::system::error_code ec, std::size_t length); - void HandleWrite(boost::system::error_code ec, - std::size_t length); + void HandleTimer(boost::system::error_code ec); + + void CancelTimer(); private: // Socket for the connection. boost::asio::ip::tcp::socket socket_; + // Timeout timer (optional). + std::unique_ptr timer_; + // The handler used to process the incoming request. HttpRequestHandler* request_handler_; diff --git a/src/webcc/rest_server.cc b/src/webcc/rest_server.cc index b8e76e5..c115e31 100644 --- a/src/webcc/rest_server.cc +++ b/src/webcc/rest_server.cc @@ -104,18 +104,16 @@ HttpStatus::Enum RestRequestHandler::HandleSession(HttpSessionPtr session) { RestServer::RestServer(unsigned short port, std::size_t workers) : HttpServer(port, workers) - , rest_request_handler_(new RestRequestHandler()) { - request_handler_ = rest_request_handler_; + , request_handler_(new RestRequestHandler()) { } RestServer::~RestServer() { - request_handler_ = NULL; - delete rest_request_handler_; + delete request_handler_; } bool RestServer::RegisterService(RestServicePtr service, const std::string& url) { - return rest_request_handler_->RegisterService(service, url); + return request_handler_->RegisterService(service, url); } } // namespace webcc diff --git a/src/webcc/rest_server.h b/src/webcc/rest_server.h index 2e2499c..ede2324 100644 --- a/src/webcc/rest_server.h +++ b/src/webcc/rest_server.h @@ -97,7 +97,12 @@ public: bool RegisterService(RestServicePtr service, const std::string& url); private: - RestRequestHandler* rest_request_handler_; + HttpRequestHandler* GetRequestHandler() override { + return request_handler_; + } + +private: + RestRequestHandler* request_handler_; }; } // namespace webcc diff --git a/src/webcc/soap_server.cc b/src/webcc/soap_server.cc index b24fbca..a46a6a2 100644 --- a/src/webcc/soap_server.cc +++ b/src/webcc/soap_server.cc @@ -72,18 +72,16 @@ SoapServicePtr SoapRequestHandler::GetServiceByUrl(const std::string& url) { SoapServer::SoapServer(unsigned short port, std::size_t workers) : HttpServer(port, workers) - , soap_request_handler_(new SoapRequestHandler()) { - request_handler_ = soap_request_handler_; + , request_handler_(new SoapRequestHandler()) { } SoapServer::~SoapServer() { - request_handler_ = NULL; - delete soap_request_handler_; + delete request_handler_; } bool SoapServer::RegisterService(SoapServicePtr service, const std::string& url) { - return soap_request_handler_->RegisterService(service, url); + return request_handler_->RegisterService(service, url); } } // namespace webcc diff --git a/src/webcc/soap_server.h b/src/webcc/soap_server.h index 8b121a0..3a32d75 100644 --- a/src/webcc/soap_server.h +++ b/src/webcc/soap_server.h @@ -44,7 +44,12 @@ public: bool RegisterService(SoapServicePtr service, const std::string& url); private: - SoapRequestHandler* soap_request_handler_; + HttpRequestHandler* GetRequestHandler() override { + return request_handler_; + } + +private: + SoapRequestHandler* request_handler_; }; } // namespace webcc