Add a base class for http async clients; refine examples.

master
Chunting Gu 7 years ago
parent 9ecec5de0b
commit 43eaf90621

@ -120,8 +120,8 @@ endif()
add_subdirectory(webcc)
if(WEBCC_ENABLE_EXAMPLES)
add_subdirectory(example/http_hello_client)
add_subdirectory(example/http_hello_async_client)
add_subdirectory(example/http_client)
add_subdirectory(example/http_async_client)
# For including jsoncpp as "json/json.h".
include_directories(${THIRD_PARTY_DIR}/src/jsoncpp)
@ -143,10 +143,9 @@ if(WEBCC_ENABLE_EXAMPLES)
if(WEBCC_ENABLE_SSL)
add_subdirectory(example/http_ssl_client)
add_subdirectory(example/http_ssl_async_client)
add_subdirectory(example/github_rest_client)
endif()
add_subdirectory(example/http_bin_client)
endif()
if(WEBCC_ENABLE_UNITTEST)

@ -0,0 +1,36 @@
HttpBin (http://httpbin.org/) client example.
You request to different endpoints, and it returns information about what was in the request.
E.g., request:
```plain
GET /get HTTP/1.1
Host: httpbin.org:80
User-Agent: Webcc/0.1.0
```
Response:
```plain
HTTP/1.1 200 OK
Connection: keep-alive
Server: gunicorn/19.9.0
Content-Type: application/json
Content-Length: 191
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Via: 1.1 vegur
{
"args": {},
"headers": {
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "Webcc/0.1.0"
},
"origin": "198.55.94.81",
"url": "http://httpbin.org/get"
}
```
As you can see, the request information is returned in JSON format.

@ -0,0 +1,4 @@
add_executable(http_async_client main.cc)
target_link_libraries(http_async_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_async_client "${CMAKE_THREAD_LIBS_INIT}")

@ -0,0 +1,49 @@
#include <iostream>
#include "boost/asio/io_context.hpp"
#include "webcc/http_async_client.h"
#include "webcc/logger.h"
// TODO: The program blocks during read response.
// Only HttpBin.org has this issue.
static void Test(boost::asio::io_context& io_context) {
auto request = webcc::HttpRequest::Make(webcc::kHttpGet, "/get",
"httpbin.org");
webcc::HttpAsyncClientPtr client{
new webcc::HttpAsyncClient(io_context)
};
client->SetTimeout(3);
// Response callback.
auto callback = [](webcc::HttpResponsePtr response, webcc::Error error,
bool timed_out) {
if (error == webcc::kNoError) {
std::cout << response->content() << std::endl;
} else {
std::cout << DescribeError(error);
if (timed_out) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
};
client->Request(request, callback);
}
int main() {
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
boost::asio::io_context io_context;
//Test(io_context);
Test(io_context);
io_context.run();
return 0;
}

@ -1,4 +0,0 @@
add_executable(http_bin_client main.cc)
target_link_libraries(http_bin_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_bin_client "${CMAKE_THREAD_LIBS_INIT}")

@ -1,61 +0,0 @@
// HttpBin (http://httpbin.org/) client example.
//
// You request to different endpoints, and it returns information about what
// was in the request.
//
// E.g., request:
// > GET /get HTTP/1.1
// > Host: httpbin.org:80
// > User-Agent: Webcc/0.1.0
// >
// Response:
// > HTTP/1.1 200 OK
// > Connection: keep-alive
// > Server: gunicorn/19.9.0
// > Content-Type: application/json
// > Content-Length: 191
// > Access-Control-Allow-Origin: *
// > Access-Control-Allow-Credentials: true
// > Via: 1.1 vegur
// >
// > {
// > "args": {},
// > "headers": {
// > "Connection": "close",
// > "Host": "httpbin.org",
// > "User-Agent": "Webcc/0.1.0"
// > },
// > "origin": "198.55.94.81",
// > "url": "http://httpbin.org/get"
// > }
// >
// As you can see, the request information is returned in JSON format.
#include <iostream>
#include "webcc/http_client.h"
#include "webcc/logger.h"
void Test() {
webcc::HttpRequest request(webcc::kHttpGet, "/get", "httpbin.org"/*, "80"*/);
request.Make();
webcc::HttpClient client;
if (client.Request(request)) {
std::cout << client.response()->content() << std::endl;
} else {
std::cout << webcc::DescribeError(client.error());
if (client.timed_out()) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
}
int main() {
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
Test();
return 0;
}

@ -0,0 +1,4 @@
add_executable(http_client main.cc)
target_link_libraries(http_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_client "${CMAKE_THREAD_LIBS_INIT}")

@ -0,0 +1,29 @@
#include <iostream>
#include "webcc/http_client.h"
#include "webcc/logger.h"
static void PrintError(const webcc::HttpClient& client) {
std::cout << webcc::DescribeError(client.error());
if (client.timed_out()) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
int main() {
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
auto request = webcc::HttpRequest::Make(webcc::kHttpGet, "/get",
"httpbin.org");
webcc::HttpClient client;
if (client.Request(*request)) {
std::cout << client.response_content() << std::endl;
} else {
PrintError(client);
}
return 0;
}

@ -1,4 +0,0 @@
add_executable(http_hello_async_client main.cc)
target_link_libraries(http_hello_async_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_hello_async_client "${CMAKE_THREAD_LIBS_INIT}")

@ -1,50 +0,0 @@
#include <iostream>
#include "boost/asio/io_context.hpp"
#include "webcc/http_async_client.h"
#include "webcc/logger.h"
// In order to test this client, create a file index.html whose content is
// simply "Hello, World!", then start a HTTP server with Python 3:
// $ python -m http.server
// The default port number should be 8000.
void Test(boost::asio::io_context& io_context) {
webcc::HttpRequestPtr request = std::make_shared<webcc::HttpRequest>(
webcc::kHttpGet, "/index.html", "localhost", "8000");
request->Make();
webcc::HttpAsyncClientPtr client(new webcc::HttpAsyncClient(io_context));
// Response handler.
auto handler = [](webcc::HttpResponsePtr response, webcc::Error error,
bool timed_out) {
if (error == webcc::kNoError) {
std::cout << response->content() << std::endl;
} else {
std::cout << webcc::DescribeError(error);
if (timed_out) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
};
client->Request(request, handler);
}
int main() {
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
boost::asio::io_context io_context;
Test(io_context);
Test(io_context);
Test(io_context);
io_context.run();
return 0;
}

@ -1,4 +0,0 @@
add_executable(http_hello_client main.cc)
target_link_libraries(http_hello_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_hello_client "${CMAKE_THREAD_LIBS_INIT}")

@ -1,36 +0,0 @@
#include <iostream>
#include "webcc/http_client.h"
#include "webcc/logger.h"
// In order to test this client, create a file index.html whose content is
// simply "Hello, World!", then start a HTTP server with Python 3:
// $ python -m http.server
// The default port number should be 8000.
void Test() {
webcc::HttpRequest request(webcc::kHttpGet, "/index.html", "localhost",
"8000");
request.Make();
webcc::HttpClient client;
if (client.Request(request)) {
std::cout << client.response()->content() << std::endl;
} else {
std::cout << webcc::DescribeError(client.error());
if (client.timed_out()) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
}
int main() {
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
Test();
Test();
Test();
return 0;
}

@ -0,0 +1,11 @@
add_executable(http_ssl_async_client main.cc)
# TODO
set(SSL_LIBS ${OPENSSL_LIBRARIES})
if(WIN32)
set(SSL_LIBS ${SSL_LIBS} crypt32)
endif()
target_link_libraries(http_ssl_async_client webcc ${Boost_LIBRARIES})
target_link_libraries(http_ssl_async_client "${CMAKE_THREAD_LIBS_INIT}")
target_link_libraries(http_ssl_async_client ${SSL_LIBS})

@ -0,0 +1,58 @@
#include <iostream>
#include "boost/asio/io_context.hpp"
#include "webcc/http_ssl_async_client.h"
#include "webcc/logger.h"
int main(int argc, char* argv[]) {
std::string host;
std::string url;
if (argc != 3) {
host = "www.boost.org";
url = "/LICENSE_1_0.txt";
} else {
host = argv[1];
url = argv[2];
}
std::cout << "Host: " << host << std::endl;
std::cout << "URL: " << url << std::endl;
std::cout << std::endl;
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
boost::asio::io_context io_context;
// Leave port to default value.
auto request = webcc::HttpRequest::Make(webcc::kHttpGet, url, host);
// Verify the certificate of the peer or not.
// See HttpSslClient::Request() for more details.
bool ssl_verify = false;
webcc::HttpSslAsyncClientPtr client{
new webcc::HttpSslAsyncClient{ io_context, 2000, ssl_verify }
};
// Response callback.
auto callback = [](webcc::HttpResponsePtr response, webcc::Error error,
bool timed_out) {
if (error == webcc::kNoError) {
std::cout << response->content() << std::endl;
} else {
std::cout << DescribeError(error);
if (timed_out) {
std::cout << " (timed out)";
}
std::cout << std::endl;
}
};
client->Request(request, callback);
io_context.run();
return 0;
}

@ -24,7 +24,7 @@ int main(int argc, char* argv[]) {
// Leave port to default value.
webcc::HttpRequest request(webcc::kHttpGet, url, host);
request.Make();
request.Prepare();
// Verify the certificate of the peer or not.
// See HttpSslClient::Request() for more details.

@ -15,6 +15,7 @@ include(GNUInstallDirs)
set(HEADERS
globals.h
http_async_client_base.h
http_async_client.h
http_client_base.h
http_client.h
@ -27,7 +28,7 @@ set(HEADERS
http_response.h
http_response_parser.h
http_server.h
logger.h
http_ssl_async_client.h
queue.h
rest_async_client.h
rest_client.h
@ -42,6 +43,7 @@ set(HEADERS
set(SOURCES
globals.cc
http_async_client_base.cc
http_async_client.cc
http_client_base.cc
http_client.cc
@ -54,6 +56,7 @@ set(SOURCES
http_response.cc
http_response_parser.cc
http_server.cc
http_ssl_async_client.cc
logger.cc
rest_async_client.cc
rest_client.cc

@ -11,222 +11,25 @@ namespace webcc {
HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& io_context,
std::size_t buffer_size)
: socket_(io_context),
resolver_(io_context),
buffer_(buffer_size == 0 ? kBufferSize : buffer_size),
deadline_(io_context),
timeout_seconds_(kMaxReadSeconds),
stopped_(false),
timed_out_(false) {
: HttpAsyncClientBase(io_context, buffer_size),
socket_(io_context) {
}
void HttpAsyncClient::SetTimeout(int seconds) {
if (seconds > 0) {
timeout_seconds_ = seconds;
}
void HttpAsyncClient::SocketAsyncConnect(const Endpoints& endpoints,
ConnectHandler&& handler) {
boost::asio::async_connect(socket_, endpoints, std::move(handler));
}
void HttpAsyncClient::Request(std::shared_ptr<HttpRequest> request,
HttpResponseCallback response_callback) {
assert(request);
assert(response_callback);
response_.reset(new HttpResponse());
response_parser_.reset(new HttpResponseParser(response_.get()));
stopped_ = false;
timed_out_ = false;
LOG_VERB("HTTP request:\n%s", request->Dump(4, "> ").c_str());
request_ = request;
response_callback_ = response_callback;
resolver_.async_resolve(tcp::v4(), request->host(), request->port(kHttpPort),
std::bind(&HttpAsyncClient::OnResolve,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
}
void HttpAsyncClient::Stop() {
LOG_INFO("The user asks to cancel the request.");
DoStop();
void HttpAsyncClient::SocketAsyncWrite(WriteHandler&& handler) {
boost::asio::async_write(socket_, request_->ToBuffers(), std::move(handler));
}
void HttpAsyncClient::OnResolve(boost::system::error_code ec,
tcp::resolver::results_type endpoints) {
if (ec) {
LOG_ERRO("Host resolve error (%s): %s, %s.", ec.message().c_str(),
request_->host().c_str(), request_->port().c_str());
response_callback_(response_, kHostResolveError, timed_out_);
} else {
LOG_VERB("Connect to server...");
// ConnectHandler: void(boost::system::error_code, tcp::endpoint)
boost::asio::async_connect(socket_, endpoints,
std::bind(&HttpAsyncClient::OnConnect,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
}
void HttpAsyncClient::SocketAsyncReadSome(ReadHandler&& handler) {
socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler));
}
void HttpAsyncClient::OnConnect(boost::system::error_code ec,
tcp::endpoint endpoint) {
if (ec) {
LOG_ERRO("Socket connect error (%s).", ec.message().c_str());
DoStop();
response_callback_(response_, kEndpointConnectError, timed_out_);
return;
}
LOG_VERB("Socket connected.");
// Even though the connect operation notionally succeeded, the user could
// have stopped the operation by calling Stop(). And if we started the
// deadline timer, it could also be stopped due to timeout.
if (stopped_) {
// TODO: Use some other error.
response_callback_(response_, kEndpointConnectError, timed_out_);
return;
}
// Connection established.
DoWrite();
}
void HttpAsyncClient::DoWrite() {
// NOTE:
// It doesn't make much sense to set a timeout for socket write.
// I find that it's almost impossible to simulate a situation in the server
// side to test this timeout.
boost::asio::async_write(socket_,
request_->ToBuffers(),
std::bind(&HttpAsyncClient::OnWrite,
shared_from_this(),
std::placeholders::_1));
}
void HttpAsyncClient::OnWrite(boost::system::error_code ec) {
if (stopped_) {
// TODO: Use some other error.
response_callback_(response_, kSocketWriteError, timed_out_);
return;
}
if (ec) {
LOG_ERRO("Socket write error (%s).", ec.message().c_str());
DoStop();
response_callback_(response_, kSocketWriteError, timed_out_);
} else {
LOG_INFO("Request sent.");
LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_);
deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_));
DoWaitDeadline();
DoRead();
}
}
void HttpAsyncClient::DoRead() {
socket_.async_read_some(boost::asio::buffer(buffer_),
std::bind(&HttpAsyncClient::OnRead,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
}
void HttpAsyncClient::OnRead(boost::system::error_code ec,
std::size_t length) {
LOG_VERB("Socket async read handler.");
if (ec || length == 0) {
DoStop();
LOG_ERRO("Socket read error (%s).", ec.message().c_str());
response_callback_(response_, kSocketReadError, timed_out_);
return;
}
LOG_INFO("Read data, length: %u.", length);
// Parse the response piece just read.
// If the content has been fully received, |finished()| will be true.
if (!response_parser_->Parse(buffer_.data(), length)) {
DoStop();
LOG_ERRO("Failed to parse HTTP response.");
response_callback_(response_, kHttpError, timed_out_);
return;
}
if (response_parser_->finished()) {
DoStop();
LOG_INFO("Finished to read and parse HTTP response.");
LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str());
response_callback_(response_, kNoError, timed_out_);
return;
}
if (!stopped_) {
DoRead();
}
}
void HttpAsyncClient::DoWaitDeadline() {
deadline_.async_wait(std::bind(&HttpAsyncClient::OnDeadline,
shared_from_this(), std::placeholders::_1));
}
void HttpAsyncClient::OnDeadline(boost::system::error_code ec) {
if (stopped_) {
return;
}
LOG_VERB("OnDeadline.");
// NOTE: Can't check this:
// if (ec == boost::asio::error::operation_aborted) {
// LOG_VERB("Deadline timer canceled.");
// return;
// }
if (deadline_.expires_at() <=
boost::asio::deadline_timer::traits_type::now()) {
// The deadline has passed.
// The socket is closed so that any outstanding asynchronous operations
// are canceled.
LOG_WARN("HTTP client timed out.");
timed_out_ = true;
Stop();
return;
}
// Put the actor back to sleep.
DoWaitDeadline();
}
void HttpAsyncClient::DoStop() {
if (stopped_) {
return;
}
stopped_ = true;
LOG_INFO("Close socket...");
boost::system::error_code ec;
socket_.close(ec);
if (ec) {
LOG_ERRO("Socket close error (%s).", ec.message().c_str());
}
LOG_INFO("Cancel deadline timer...");
deadline_.cancel();
void HttpAsyncClient::SocketClose(boost::system::error_code* ec) {
socket_.close(*ec);
}
} // namespace webcc

@ -1,91 +1,37 @@
#ifndef WEBCC_HTTP_ASYNC_CLIENT_H_
#define WEBCC_HTTP_ASYNC_CLIENT_H_
#include <functional>
#include <memory>
#include <vector>
#include "boost/asio/deadline_timer.hpp"
#include "boost/asio/io_context.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "webcc/globals.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
#include "webcc/http_response_parser.h"
#include "webcc/http_async_client_base.h"
namespace webcc {
// Response callback.
typedef std::function<void(HttpResponsePtr, Error, bool)> HttpResponseCallback;
// HTTP client session in asynchronous mode.
// A request will return without waiting for the response, the callback handler
// will be invoked when the response is received or timeout occurs.
// Don't use the same HttpAsyncClient object in multiple threads.
class HttpAsyncClient : public std::enable_shared_from_this<HttpAsyncClient> {
// HTTP asynchronous client.
class HttpAsyncClient : public HttpAsyncClientBase {
public:
// The |buffer_size| is the bytes of the buffer for reading response.
// 0 means default value (e.g., 1024) will be used.
explicit HttpAsyncClient(boost::asio::io_context& io_context,
std::size_t buffer_size = 0);
WEBCC_DELETE_COPY_ASSIGN(HttpAsyncClient);
// Set the timeout seconds for reading response.
// The |seconds| is only effective when greater than 0.
void SetTimeout(int seconds);
// Asynchronously connect to the server, send the request, read the response,
// and call the |response_callback| when all these finish.
void Request(HttpRequestPtr request, HttpResponseCallback response_callback);
// Called by the user to cancel the request.
void Stop();
~HttpAsyncClient() = default;
private:
using tcp = boost::asio::ip::tcp;
void OnResolve(boost::system::error_code ec,
tcp::resolver::results_type results);
void Resolve() final {
DoResolve(kHttpPort);
}
void OnConnect(boost::system::error_code ec, tcp::endpoint endpoint);
void OnConnected() final {
DoWrite();
}
void DoWrite();
void OnWrite(boost::system::error_code ec);
void SocketAsyncConnect(const Endpoints& endpoints,
ConnectHandler&& handler) final;
void DoRead();
void OnRead(boost::system::error_code ec, std::size_t length);
void SocketAsyncWrite(WriteHandler&& handler) final;
void DoWaitDeadline();
void OnDeadline(boost::system::error_code ec);
void SocketAsyncReadSome(ReadHandler&& handler) final;
// Terminate all the actors to shut down the connection.
void DoStop();
void SocketClose(boost::system::error_code* ec) final;
tcp::resolver resolver_;
tcp::socket socket_;
std::shared_ptr<HttpRequest> request_;
std::vector<char> buffer_;
HttpResponsePtr response_;
std::unique_ptr<HttpResponseParser> response_parser_;
HttpResponseCallback response_callback_;
// Timer for the timeout control.
boost::asio::deadline_timer deadline_;
// Maximum seconds to wait before the client cancels the operation.
// Only for receiving response from server.
int timeout_seconds_;
// Request stopped due to timeout or socket error.
bool stopped_;
// If the error was caused by timeout or not.
// Will be passed to the response handler/callback.
bool timed_out_;
};
typedef std::shared_ptr<HttpAsyncClient> HttpAsyncClientPtr;

@ -0,0 +1,236 @@
#include "webcc/http_async_client_base.h"
#include "boost/asio/connect.hpp"
#include "boost/asio/read.hpp"
#include "boost/asio/write.hpp"
#include "webcc/logger.h"
#include "webcc/utility.h"
namespace webcc {
HttpAsyncClientBase::HttpAsyncClientBase(boost::asio::io_context& io_context,
std::size_t buffer_size)
: resolver_(io_context),
buffer_(buffer_size == 0 ? kBufferSize : buffer_size),
deadline_(io_context),
timeout_seconds_(kMaxReadSeconds),
stopped_(false),
timed_out_(false) {
}
void HttpAsyncClientBase::SetTimeout(int seconds) {
if (seconds > 0) {
timeout_seconds_ = seconds;
}
}
void HttpAsyncClientBase::Request(std::shared_ptr<HttpRequest> request,
HttpResponseCallback response_callback) {
assert(request);
assert(response_callback);
response_.reset(new HttpResponse());
response_parser_.reset(new HttpResponseParser(response_.get()));
stopped_ = false;
timed_out_ = false;
LOG_VERB("HTTP request:\n%s", request->Dump(4, "> ").c_str());
request_ = request;
response_callback_ = response_callback;
DoResolve(kHttpSslPort);
}
void HttpAsyncClientBase::Stop() {
LOG_INFO("The user asks to cancel the request.");
DoStop();
}
void HttpAsyncClientBase::DoResolve(const std::string& default_port) {
auto handler = std::bind(&HttpAsyncClientBase::OnResolve, shared_from_this(),
std::placeholders::_1, std::placeholders::_2);
resolver_.async_resolve(tcp::v4(), request_->host(),
request_->port(default_port), handler);
}
void HttpAsyncClientBase::OnResolve(boost::system::error_code ec,
tcp::resolver::results_type endpoints) {
if (ec) {
LOG_ERRO("Host resolve error (%s): %s, %s.", ec.message().c_str(),
request_->host().c_str(), request_->port().c_str());
response_callback_(response_, kHostResolveError, timed_out_);
} else {
LOG_VERB("Host resolved.");
DoConnect(endpoints);
}
}
void HttpAsyncClientBase::DoConnect(const Endpoints& endpoints) {
auto handler = std::bind(&HttpAsyncClientBase::OnConnect,
shared_from_this(),
std::placeholders::_1, std::placeholders::_2);
SocketAsyncConnect(endpoints, std::move(handler));
}
void HttpAsyncClientBase::OnConnect(boost::system::error_code ec,
tcp::endpoint endpoint) {
if (ec) {
LOG_ERRO("Socket connect error (%s).", ec.message().c_str());
DoStop();
response_callback_(response_, kEndpointConnectError, timed_out_);
return;
}
LOG_VERB("Socket connected.");
// Even though the connect operation notionally succeeded, the user could
// have stopped the operation by calling Stop(). And if we started the
// deadline timer, it could also be stopped due to timeout.
if (stopped_) {
// TODO: Use some other error.
response_callback_(response_, kEndpointConnectError, timed_out_);
return;
}
// Connection established.
OnConnected();
}
void HttpAsyncClientBase::DoWrite() {
// NOTE:
// It doesn't make much sense to set a timeout for socket write.
// I find that it's almost impossible to simulate a situation in the server
// side to test this timeout.
SocketAsyncWrite(std::bind(&HttpAsyncClientBase::OnWrite, shared_from_this(),
std::placeholders::_1, std::placeholders::_2));
}
void HttpAsyncClientBase::OnWrite(boost::system::error_code ec,
std::size_t /*length*/) {
if (stopped_) {
// TODO: Use some other error.
response_callback_(response_, kSocketWriteError, timed_out_);
return;
}
if (ec) {
LOG_ERRO("Socket write error (%s).", ec.message().c_str());
DoStop();
response_callback_(response_, kSocketWriteError, timed_out_);
} else {
LOG_INFO("Request sent.");
LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_);
deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_));
DoWaitDeadline();
DoRead();
}
}
void HttpAsyncClientBase::DoRead() {
auto handler = std::bind(&HttpAsyncClientBase::OnRead, shared_from_this(),
std::placeholders::_1, std::placeholders::_2);
SocketAsyncReadSome(std::move(handler));
}
void HttpAsyncClientBase::OnRead(boost::system::error_code ec,
std::size_t length) {
LOG_VERB("Socket async read handler.");
if (ec || length == 0) {
DoStop();
LOG_ERRO("Socket read error (%s).", ec.message().c_str());
response_callback_(response_, kSocketReadError, timed_out_);
return;
}
LOG_INFO("Read data, length: %u.", length);
// Parse the response piece just read.
// If the content has been fully received, |finished()| will be true.
if (!response_parser_->Parse(buffer_.data(), length)) {
DoStop();
LOG_ERRO("Failed to parse HTTP response.");
response_callback_(response_, kHttpError, timed_out_);
return;
}
if (response_parser_->finished()) {
DoStop();
LOG_INFO("Finished to read and parse HTTP response.");
LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str());
response_callback_(response_, kNoError, timed_out_);
return;
}
if (!stopped_) {
DoRead();
}
}
void HttpAsyncClientBase::DoWaitDeadline() {
deadline_.async_wait(std::bind(&HttpAsyncClientBase::OnDeadline,
shared_from_this(), std::placeholders::_1));
}
void HttpAsyncClientBase::OnDeadline(boost::system::error_code ec) {
if (stopped_) {
return;
}
LOG_VERB("OnDeadline.");
// NOTE: Can't check this:
// if (ec == boost::asio::error::operation_aborted) {
// LOG_VERB("Deadline timer canceled.");
// return;
// }
if (deadline_.expires_at() <=
boost::asio::deadline_timer::traits_type::now()) {
// The deadline has passed.
// The socket is closed so that any outstanding asynchronous operations
// are canceled.
LOG_WARN("HTTP client timed out.");
timed_out_ = true;
Stop();
return;
}
// Put the actor back to sleep.
DoWaitDeadline();
}
void HttpAsyncClientBase::DoStop() {
if (stopped_) {
return;
}
stopped_ = true;
LOG_INFO("Close socket...");
boost::system::error_code ec;
SocketClose(&ec);
if (ec) {
LOG_ERRO("Socket close error (%s).", ec.message().c_str());
}
LOG_INFO("Cancel deadline timer...");
deadline_.cancel();
}
} // namespace webcc

@ -0,0 +1,128 @@
#ifndef WEBCC_HTTP_ASYNC_CLIENT_BASE_H_
#define WEBCC_HTTP_ASYNC_CLIENT_BASE_H_
#include <functional>
#include <memory>
#include <vector>
#include "boost/asio/deadline_timer.hpp"
#include "boost/asio/io_context.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "webcc/globals.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
#include "webcc/http_response_parser.h"
namespace webcc {
// Response callback.
typedef std::function<void(HttpResponsePtr, Error, bool)> HttpResponseCallback;
// HTTP client session in asynchronous mode.
// A request will return without waiting for the response, the callback handler
// will be invoked when the response is received or timeout occurs.
// Don't use the same HttpAsyncClient object in multiple threads.
class HttpAsyncClientBase
: public std::enable_shared_from_this<HttpAsyncClientBase> {
public:
// The |buffer_size| is the bytes of the buffer for reading response.
// 0 means default value (e.g., 1024) will be used.
explicit HttpAsyncClientBase(boost::asio::io_context& io_context,
std::size_t buffer_size = 0);
WEBCC_DELETE_COPY_ASSIGN(HttpAsyncClientBase);
// Set the timeout seconds for reading response.
// The |seconds| is only effective when greater than 0.
void SetTimeout(int seconds);
// Asynchronously connect to the server, send the request, read the response,
// and call the |response_callback| when all these finish.
void Request(HttpRequestPtr request, HttpResponseCallback response_callback);
// Called by the user to cancel the request.
void Stop();
protected:
using tcp = boost::asio::ip::tcp;
typedef tcp::resolver::results_type Endpoints;
typedef std::function<void(boost::system::error_code, std::size_t)>
ReadHandler;
typedef std::function<void(boost::system::error_code, tcp::endpoint)>
ConnectHandler;
typedef std::function<void(boost::system::error_code, std::size_t)>
WriteHandler;
// To enable_shared_from_this for both parent and derived.
// See https://stackoverflow.com/q/657155/6825348
template <typename Derived>
std::shared_ptr<Derived> shared_from_base() {
return std::static_pointer_cast<Derived>(shared_from_this());
}
protected:
virtual void Resolve() = 0;
void DoResolve(const std::string& default_port);
void OnResolve(boost::system::error_code ec,
tcp::resolver::results_type results);
void DoConnect(const Endpoints& endpoints);
void OnConnect(boost::system::error_code ec, tcp::endpoint endpoint);
virtual void OnConnected() = 0;
void DoWrite();
void OnWrite(boost::system::error_code ec, std::size_t length);
void DoRead();
void OnRead(boost::system::error_code ec, std::size_t length);
void DoWaitDeadline();
void OnDeadline(boost::system::error_code ec);
// Terminate all the actors to shut down the connection.
void DoStop();
virtual void SocketAsyncConnect(const Endpoints& endpoints,
ConnectHandler&& handler) = 0;
virtual void SocketAsyncWrite(WriteHandler&& handler) = 0;
virtual void SocketAsyncReadSome(ReadHandler&& handler) = 0;
virtual void SocketClose(boost::system::error_code* ec) = 0;
tcp::resolver resolver_;
HttpRequestPtr request_;
std::vector<char> buffer_;
HttpResponsePtr response_;
std::unique_ptr<HttpResponseParser> response_parser_;
HttpResponseCallback response_callback_;
// Timer for the timeout control.
boost::asio::deadline_timer deadline_;
// Maximum seconds to wait before the client cancels the operation.
// Only for receiving response from server.
int timeout_seconds_;
// Request stopped due to timeout or socket error.
bool stopped_;
// If the error was caused by timeout or not.
// Will be passed to the response handler/callback.
bool timed_out_;
};
} // namespace webcc
#endif // WEBCC_HTTP_ASYNC_CLIENT_BASE_H_

@ -20,9 +20,8 @@ void HttpClient::SocketWrite(const HttpRequest& request,
boost::asio::write(socket_, request.ToBuffers(), *ec);
}
void HttpClient::SocketAsyncReadSome(std::vector<char>& buffer,
ReadHandler handler) {
socket_.async_read_some(boost::asio::buffer(buffer), handler);
void HttpClient::SocketAsyncReadSome(ReadHandler&& handler) {
socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler));
}
void HttpClient::SocketClose(boost::system::error_code* ec) {

@ -5,17 +5,13 @@
namespace webcc {
// HTTP client in synchronous mode.
// A request will not return until the response is received or timeout occurs.
// Don't use the same HttpClient object in multiple threads.
// HTTP synchronous client.
class HttpClient : public HttpClientBase {
public:
explicit HttpClient(std::size_t buffer_size = 0);
~HttpClient() = default;
WEBCC_DELETE_COPY_ASSIGN(HttpClient);
private:
Error Connect(const HttpRequest& request) final {
return DoConnect(request, kHttpPort);
@ -27,8 +23,7 @@ class HttpClient : public HttpClientBase {
void SocketWrite(const HttpRequest& request,
boost::system::error_code* ec) final;
void SocketAsyncReadSome(std::vector<char>& buffer,
ReadHandler handler) final;
void SocketAsyncReadSome(ReadHandler&& handler) final;
void SocketClose(boost::system::error_code* ec) final;

@ -1,8 +1,5 @@
#include "webcc/http_client_base.h"
#include "boost/asio/connect.hpp"
#include "boost/asio/read.hpp"
#include "boost/asio/write.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"
#include "webcc/logger.h"
@ -44,7 +41,7 @@ bool HttpClientBase::Request(const HttpRequest& request,
return false;
}
if ((error_ = SendReqeust(request)) != kNoError) {
if ((error_ = WriteReqeust(request)) != kNoError) {
return false;
}
@ -87,7 +84,7 @@ Error HttpClientBase::DoConnect(const HttpRequest& request,
return kNoError;
}
Error HttpClientBase::SendReqeust(const HttpRequest& request) {
Error HttpClientBase::WriteReqeust(const HttpRequest& request) {
LOG_VERB("HTTP request:\n%s", request.Dump(4, "> ").c_str());
// NOTE:
@ -130,8 +127,8 @@ Error HttpClientBase::ReadResponse() {
void HttpClientBase::DoReadResponse(Error* error) {
boost::system::error_code ec = boost::asio::error::would_block;
auto read_handler = [this, &ec, error](boost::system::error_code inner_ec,
std::size_t length) {
auto handler = [this, &ec, error](boost::system::error_code inner_ec,
std::size_t length) {
ec = inner_ec;
LOG_VERB("Socket async read handler.");
@ -168,7 +165,7 @@ void HttpClientBase::DoReadResponse(Error* error) {
}
};
SocketAsyncReadSome(buffer_, read_handler);
SocketAsyncReadSome(std::move(handler));
// Block until the asynchronous operation has completed.
do {

@ -17,6 +17,10 @@
namespace webcc {
// The base class of synchronous HTTP clients.
// In synchronous mode, a request won't return until the response is received
// or timeout occurs.
// Please don't use the same client object in multiple threads.
class HttpClientBase {
public:
// The |buffer_size| is the bytes of the buffer for reading response.
@ -38,6 +42,16 @@ class HttpClientBase {
HttpResponsePtr response() const { return response_; }
int response_status() const {
assert(response_);
return response_->status();
}
const std::string& response_content() const {
assert(response_);
return response_->content();
}
bool timed_out() const { return timed_out_; }
Error error() const { return error_; }
@ -48,12 +62,11 @@ class HttpClientBase {
typedef std::function<void(boost::system::error_code, std::size_t)>
ReadHandler;
Error DoConnect(const HttpRequest& request, const std::string& default_port);
virtual Error Connect(const HttpRequest& request) = 0;
boost::asio::io_context io_context_;
Error DoConnect(const HttpRequest& request, const std::string& default_port);
private:
Error SendReqeust(const HttpRequest& request);
Error WriteReqeust(const HttpRequest& request);
Error ReadResponse();
@ -64,19 +77,18 @@ class HttpClientBase {
void Stop();
virtual Error Connect(const HttpRequest& request) = 0;
virtual void SocketConnect(const Endpoints& endpoints,
boost::system::error_code* ec) = 0;
virtual void SocketWrite(const HttpRequest& request,
boost::system::error_code* ec) = 0;
virtual void SocketAsyncReadSome(std::vector<char>& buffer,
ReadHandler handler) = 0;
virtual void SocketAsyncReadSome(ReadHandler&& handler) = 0;
virtual void SocketClose(boost::system::error_code* ec) = 0;
boost::asio::io_context io_context_;
std::vector<char> buffer_;
HttpResponsePtr response_;

@ -46,7 +46,7 @@ class HttpMessage {
// Make the message (e.g., update start line).
// Must be called before ToBuffers()!
virtual void Make() = 0;
virtual void Prepare() = 0;
// Convert the message into a vector of buffers. The buffers do not own the
// underlying memory blocks, therefore the message object must remain valid

@ -9,7 +9,7 @@ HttpRequest::HttpRequest(const std::string& method,
: method_(method), url_(url), host_(host), port_(port) {
}
void HttpRequest::Make() {
void HttpRequest::Prepare() {
start_line_ = method_;
start_line_ += " ";
start_line_ += url_;
@ -32,4 +32,21 @@ void HttpRequest::Make() {
SetHeader(http::headers::kUserAgent, "Webcc/" WEBCC_VERSION);
}
// static
HttpRequestPtr HttpRequest::Make(const std::string& method,
const std::string& url,
const std::string& host,
const std::string& port,
bool prepare) {
HttpRequestPtr request{
new HttpRequest{ method, url, host, port }
};
if (prepare) {
request->Prepare();
}
return request;
}
} // namespace webcc

@ -8,8 +8,11 @@
namespace webcc {
class HttpRequest;
class HttpRequestParser;
typedef std::shared_ptr<HttpRequest> HttpRequestPtr;
class HttpRequest : public HttpMessage {
public:
HttpRequest() = default;
@ -36,8 +39,15 @@ class HttpRequest : public HttpMessage {
return port_.empty() ? default_port : port_;
}
// Prepare payload.
// Compose start line, set Host header, etc.
void Make() override;
void Prepare() override;
static HttpRequestPtr Make(const std::string& method,
const std::string& url,
const std::string& host,
const std::string& port = "",
bool prepare = true);
private:
friend class HttpRequestParser;
@ -57,8 +67,6 @@ class HttpRequest : public HttpMessage {
std::string port_;
};
typedef std::shared_ptr<HttpRequest> HttpRequestPtr;
} // namespace webcc
#endif // WEBCC_HTTP_REQUEST_H_

@ -57,7 +57,7 @@ const std::string& ToString(int status) {
} // namespace status_strings
void HttpResponse::Make() {
void HttpResponse::Prepare() {
start_line_ = status_strings::ToString(status_);
// NOTE: C++11 requires a space between literal and string macro.
@ -72,7 +72,7 @@ HttpResponse HttpResponse::Fault(http::Status status) {
HttpResponse response;
response.set_status(status);
response.Make();
response.Prepare();
return response;
}

@ -19,7 +19,7 @@ class HttpResponse : public HttpMessage {
void set_status(int status) { status_ = status; }
// Set start line according to status code.
void Make() override;
void Prepare() override;
// Get a fault response when HTTP status is not OK.
// TODO: Avoid copy.

@ -41,7 +41,7 @@ void HttpSession::SetResponseContent(std::string&& content,
void HttpSession::SendResponse(http::Status status) {
response_.set_status(status);
response_.Make();
response_.Prepare();
DoWrite();
}

@ -0,0 +1,71 @@
#include "webcc/http_ssl_async_client.h"
#include "boost/asio/connect.hpp"
#include "webcc/logger.h"
using tcp = boost::asio::ip::tcp;
namespace ssl = boost::asio::ssl;
namespace webcc {
HttpSslAsyncClient::HttpSslAsyncClient(boost::asio::io_context& io_context,
std::size_t buffer_size,
bool ssl_verify)
: HttpAsyncClientBase(io_context, buffer_size),
ssl_context_(ssl::context::sslv23),
ssl_socket_(io_context, ssl_context_),
ssl_verify_(ssl_verify) {
// Use the default paths for finding CA certificates.
ssl_context_.set_default_verify_paths();
}
void HttpSslAsyncClient::Resolve() {
DoResolve(kHttpSslPort);
}
void HttpSslAsyncClient::DoHandshake() {
if (ssl_verify_) {
ssl_socket_.set_verify_mode(ssl::verify_peer);
} else {
ssl_socket_.set_verify_mode(ssl::verify_none);
}
ssl_socket_.set_verify_callback(ssl::rfc2818_verification(request_->host()));
ssl_socket_.async_handshake(ssl::stream_base::client,
std::bind(&HttpSslAsyncClient::OnHandshake,
shared_from_this(),
std::placeholders::_1));
}
void HttpSslAsyncClient::OnHandshake(boost::system::error_code ec) {
if (ec) {
LOG_ERRO("Handshake error (%s).", ec.message().c_str());
response_callback_(response_, kHandshakeError, false);
return;
}
DoWrite();
}
void HttpSslAsyncClient::SocketAsyncConnect(const Endpoints& endpoints,
ConnectHandler&& handler) {
boost::asio::async_connect(ssl_socket_.lowest_layer(), endpoints,
std::move(handler));
}
void HttpSslAsyncClient::SocketAsyncWrite(WriteHandler&& handler) {
boost::asio::async_write(ssl_socket_, request_->ToBuffers(),
std::move(handler));
}
void HttpSslAsyncClient::SocketAsyncReadSome(ReadHandler&& handler) {
ssl_socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler));
}
void HttpSslAsyncClient::SocketClose(boost::system::error_code* ec) {
ssl_socket_.lowest_layer().close(*ec);
}
} // namespace webcc

@ -0,0 +1,58 @@
#ifndef WEBCC_HTTP_SSL_ASYNC_CLIENT_H_
#define WEBCC_HTTP_SSL_ASYNC_CLIENT_H_
#include "webcc/http_async_client_base.h"
#include "boost/asio/ssl.hpp"
namespace webcc {
// HTTP SSL (a.k.a., HTTPS) asynchronous client.
class HttpSslAsyncClient : public HttpAsyncClientBase {
public:
// SSL verification (|ssl_verify|) needs CA certificates to be found
// in the default verify paths of OpenSSL. On Windows, it means you need to
// set environment variable SSL_CERT_FILE properly.
explicit HttpSslAsyncClient(boost::asio::io_context& io_context,
std::size_t buffer_size = 0,
bool ssl_verify = true);
~HttpSslAsyncClient() = default;
// See https://stackoverflow.com/q/657155/6825348
std::shared_ptr<HttpSslAsyncClient> shared_from_this() {
return shared_from_base<HttpSslAsyncClient>();
}
private:
void Resolve() final;
// Override to do handshake after connected.
void OnConnected() final {
DoHandshake();
}
void DoHandshake();
void OnHandshake(boost::system::error_code ec);
void SocketAsyncConnect(const Endpoints& endpoints,
ConnectHandler&& handler) final;
void SocketAsyncWrite(WriteHandler&& handler) final;
void SocketAsyncReadSome(ReadHandler&& handler) final;
void SocketClose(boost::system::error_code* ec) final;
boost::asio::ssl::context ssl_context_;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket_;
// Verify the certificate of the peer (remote server) or not.
bool ssl_verify_;
};
typedef std::shared_ptr<HttpSslAsyncClient> HttpSslAsyncClientPtr;
} // namespace webcc
#endif // WEBCC_HTTP_SSL_ASYNC_CLIENT_H_

@ -61,9 +61,8 @@ void HttpSslClient::SocketWrite(const HttpRequest& request,
boost::asio::write(ssl_socket_, request.ToBuffers(), *ec);
}
void HttpSslClient::SocketAsyncReadSome(std::vector<char>& buffer,
ReadHandler handler) {
ssl_socket_.async_read_some(boost::asio::buffer(buffer), handler);
void HttpSslClient::SocketAsyncReadSome(ReadHandler&& handler) {
ssl_socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler));
}
void HttpSslClient::SocketClose(boost::system::error_code* ec) {

@ -7,9 +7,7 @@
namespace webcc {
// HTTP SSL (a.k.a., HTTPS) client session in synchronous mode.
// A request will not return until the response is received or timeout occurs.
// Don't use the same HttpSslClient object in multiple threads.
// HTTP SSL (a.k.a., HTTPS) synchronous client.
class HttpSslClient : public HttpClientBase {
public:
// SSL verification (|ssl_verify|) needs CA certificates to be found
@ -19,8 +17,6 @@ class HttpSslClient : public HttpClientBase {
~HttpSslClient() = default;
WEBCC_DELETE_COPY_ASSIGN(HttpSslClient);
private:
Error Handshake(const std::string& host);
@ -33,8 +29,7 @@ class HttpSslClient : public HttpClientBase {
void SocketWrite(const HttpRequest& request,
boost::system::error_code* ec) final;
void SocketAsyncReadSome(std::vector<char>& buffer,
ReadHandler handler) final;
void SocketAsyncReadSome(ReadHandler&& handler) final;
void SocketClose(boost::system::error_code* ec) final;

@ -168,7 +168,7 @@ void LogWrite(int level, const char* file, int line, const char* format, ...) {
va_list args;
va_start(args, format);
fprintf(stderr, "%s, %s, %7s, %24s, %4d, ",
fprintf(stderr, "%s, %s, %7s, %25s, %4d, ",
timestamp.c_str(), kLevelNames[level], thread_id.c_str(),
file, line);

@ -24,7 +24,7 @@ void RestAsyncClient::Request(const std::string& method,
http::charsets::kUtf8);
}
http_request->Make();
http_request->Prepare();
HttpAsyncClientPtr http_async_client{
new HttpAsyncClient(io_context_, buffer_size_)

@ -22,7 +22,7 @@ bool RestClient::Request(const std::string& method, const std::string& url,
http::charsets::kUtf8);
}
http_request.Make();
http_request.Prepare();
if (!http_client_.Request(http_request, buffer_size)) {
return false;

@ -16,9 +16,9 @@ class RestClient {
public:
// If |port| is empty, |host| will be checked to see if it contains port or
// not (separated by ':').
explicit RestClient(const std::string& host,
const std::string& port = "",
std::size_t buffer_size = 0);
explicit RestClient(const std::string& host,
const std::string& port = "",
std::size_t buffer_size = 0);
~RestClient() = default;

@ -20,7 +20,7 @@ bool RestSslClient::Request(const std::string& method, const std::string& url,
http::charsets::kUtf8);
}
http_request.Make();
http_request.Prepare();
if (!http_client_.Request(http_request, buffer_size)) {
return false;

@ -63,7 +63,7 @@ void SoapAsyncClient::Request(const std::string& operation,
}
http_request->SetHeader(kSoapAction, operation);
http_request->Make();
http_request->Prepare();
HttpAsyncClientPtr http_async_client{
new HttpAsyncClient(io_context_, buffer_size_)

@ -62,7 +62,7 @@ bool SoapClient::Request(const std::string& operation,
http_request.SetHeader(kSoapAction, operation);
http_request.Make();
http_request.Prepare();
if (!http_client_.Request(http_request, buffer_size)) {
error_ = http_client_.error();

@ -22,8 +22,7 @@ void AdjustHostPort(std::string& host, std::string& port) {
}
}
void PrintEndpoint(std::ostream& ostream,
const boost::asio::ip::tcp::endpoint& endpoint) {
void PrintEndpoint(std::ostream& ostream, const TcpEndpoint& endpoint) {
ostream << endpoint;
if (endpoint.protocol() == tcp::v4()) {
ostream << ", v4";
@ -32,8 +31,7 @@ void PrintEndpoint(std::ostream& ostream,
}
}
void PrintEndpoints(std::ostream& ostream,
const tcp::resolver::results_type& endpoints) {
void PrintEndpoints(std::ostream& ostream, const TcpEndpoints& endpoints) {
ostream << "Endpoints: " << endpoints.size() << std::endl;
tcp::resolver::results_type::iterator it = endpoints.begin();
for (; it != endpoints.end(); ++it) {
@ -43,7 +41,7 @@ void PrintEndpoints(std::ostream& ostream,
}
}
std::string EndpointToString(const boost::asio::ip::tcp::endpoint& endpoint) {
std::string EndpointToString(const TcpEndpoint& endpoint) {
std::stringstream ss;
PrintEndpoint(ss, endpoint);
return ss.str();

@ -12,14 +12,14 @@ namespace webcc {
// If |port| is empty, try to extract it from |host| (separated by ':').
void AdjustHostPort(std::string& host, std::string& port);
void PrintEndpoint(std::ostream& ostream,
const boost::asio::ip::tcp::endpoint& endpoint);
typedef boost::asio::ip::tcp::endpoint TcpEndpoint;
typedef boost::asio::ip::tcp::resolver::results_type TcpEndpoints;
void PrintEndpoints(
std::ostream& ostream,
const boost::asio::ip::tcp::resolver::results_type& endpoints);
void PrintEndpoint(std::ostream& ostream, const TcpEndpoint& endpoint);
std::string EndpointToString(const boost::asio::ip::tcp::endpoint& endpoint);
void PrintEndpoints(std::ostream& ostream, const TcpEndpoints& endpoints);
std::string EndpointToString(const TcpEndpoint& endpoint);
// Get the timestamp for HTTP Date header field.
// E.g., Wed, 21 Oct 2015 07:28:00 GMT

Loading…
Cancel
Save