Add a base class for http async clients; refine examples.
parent
9ecec5de0b
commit
43eaf90621
@ -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;
|
||||||
|
}
|
@ -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_
|
@ -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_
|
Loading…
Reference in New Issue