Update readme

This commit is contained in:
Adam Gu
2018-05-09 17:07:58 +08:00
parent 662b7c4112
commit 81ed82a302
27 changed files with 438 additions and 238 deletions
+7
View File
@@ -18,6 +18,13 @@ const std::string kTextXmlUtf8 = "text/xml; charset=utf-8";
const std::string kTextJsonUtf8 = "text/json; charset=utf-8";
const std::string kHttpHead = "HEAD";
const std::string kHttpGet = "GET";
const std::string kHttpPost = "POST";
const std::string kHttpPatch = "PATCH";
const std::string kHttpPut = "PUT";
const std::string kHttpDelete = "DELETE";
////////////////////////////////////////////////////////////////////////////////
const char* GetErrorMessage(Error error) {
+30 -30
View File
@@ -26,6 +26,36 @@ extern const std::string kTextJsonUtf8;
////////////////////////////////////////////////////////////////////////////////
// HTTP methods (verbs) in string ("HEAD", "GET", etc.).
// NOTE: Don't use enum to avoid converting back and forth.
extern const std::string kHttpHead;
extern const std::string kHttpGet;
extern const std::string kHttpPost;
extern const std::string kHttpPatch;
extern const std::string kHttpPut;
extern const std::string kHttpDelete;
// HTTP response status.
// This is not a full list.
// Full list: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
// NTOE: Don't use enum class because we want to convert to/from int easily.
struct HttpStatus {
enum Enum {
kOK = 200,
kCreated = 201,
kAccepted = 202,
kNoContent = 204,
kNotModified = 304,
kBadRequest = 400,
kNotFound = 404,
InternalServerError = 500,
kNotImplemented = 501,
kServiceUnavailable = 503,
};
};
////////////////////////////////////////////////////////////////////////////////
// Error codes.
enum Error {
kNoError = 0, // OK
@@ -55,36 +85,6 @@ const char* GetErrorMessage(Error error);
////////////////////////////////////////////////////////////////////////////////
// HTTP methods (verbs).
// NOTE: Don't use enum to avoid converting back and forth.
const std::string kHttpHead = "HEAD";
const std::string kHttpGet = "GET";
const std::string kHttpPost = "POST";
const std::string kHttpPatch = "PATCH";
const std::string kHttpPut = "PUT";
const std::string kHttpDelete = "DELETE";
// HTTP response status.
// This is not a full list.
// Full list: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
// NTOE: Don't use enum class because we want to convert to/from int easily.
struct HttpStatus {
enum Enum {
kOK = 200,
kCreated = 201,
kAccepted = 202,
kNoContent = 204,
kNotModified = 304,
kBadRequest = 400,
kNotFound = 404,
InternalServerError = 500,
kNotImplemented = 501,
kServiceUnavailable = 503,
};
};
////////////////////////////////////////////////////////////////////////////////
// XML namespace name/url pair.
// E.g., { "soap", "http://schemas.xmlsoap.org/soap/envelope/" }
// TODO: Rename (add soap prefix)
+149 -76
View File
@@ -1,62 +1,73 @@
#include "webcc/http_client.h"
#include "boost/date_time/posix_time/posix_time.hpp"
#include "boost/lambda/bind.hpp"
#include "boost/lambda/lambda.hpp"
#if 0
#include "boost/asio.hpp"
#else
#include "boost/asio/connect.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "boost/asio/read.hpp"
#include "boost/asio/write.hpp"
#endif
#include "webcc/logger.h"
#include "webcc/http_response_parser.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
// NOTE:
// The timeout control is inspired by the following Asio example:
// example\cpp03\timeouts\blocking_tcp_client.cpp
namespace webcc {
////////////////////////////////////////////////////////////////////////////////
// See https://stackoverflow.com/a/9079092
static void SetTimeout(boost::asio::ip::tcp::socket& socket,
int timeout_seconds) {
#if defined _WINDOWS
int ms = timeout_seconds * 1000;
const char* optval = reinterpret_cast<const char*>(&ms);
std::size_t optlen = sizeof(ms);
setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, optval, optlen);
setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, optval, optlen);
#else // POSIX
// TODO: This doesn't work! Consider to control timeout in server side.
struct timeval tv;
tv.tv_sec = timeout_seconds;
tv.tv_usec = 0;
setsockopt(socket.native_handle(), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(socket.native_handle(), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
#endif
}
static const int kConnectMaxSeconds = 10;
static const int kSendMaxSeconds = 10;
static const int kReceiveMaxSeconds = 30;
////////////////////////////////////////////////////////////////////////////////
HttpClient::HttpClient()
: timeout_seconds_(15) {
: socket_(io_context_)
, timeout_seconds_(kReceiveMaxSeconds)
, deadline_timer_(io_context_) {
deadline_timer_.expires_at(boost::posix_time::pos_infin);
// Start the persistent actor that checks for deadline expiry.
CheckDeadline();
}
Error HttpClient::SendRequest(const HttpRequest& request,
Error HttpClient::MakeRequest(const HttpRequest& request,
HttpResponse* response) {
assert(response != NULL);
using boost::asio::ip::tcp;
Error error = kNoError;
tcp::socket socket(io_context_);
if ((error = Connect(request)) != kNoError) {
return error;
}
// Send HTTP request.
if ((error = SendReqeust(request)) != kNoError) {
return error;
}
// Read and parse HTTP response.
parser_ = std::make_unique<HttpResponseParser>(response);
error = ReadResponse(response);
return error;
}
Error HttpClient::Connect(const HttpRequest& request) {
using boost::asio::ip::tcp;
tcp::resolver resolver(io_context_);
@@ -66,7 +77,6 @@ Error HttpClient::SendRequest(const HttpRequest& request,
}
boost::system::error_code ec;
// tcp::resolver::results_type
auto endpoints = resolver.resolve(tcp::v4(), request.host(), port, ec);
if (ec) {
@@ -77,58 +87,121 @@ Error HttpClient::SendRequest(const HttpRequest& request,
return kHostResolveError;
}
boost::asio::connect(socket, endpoints, ec);
if (ec) {
return kEndpointConnectError;
}
deadline_timer_.expires_from_now(
boost::posix_time::seconds(kConnectMaxSeconds));
SetTimeout(socket, timeout_seconds_);
ec = boost::asio::error::would_block;
// Send HTTP request.
boost::asio::async_connect(socket_,
endpoints,
boost::lambda::var(ec) = boost::lambda::_1);
LOG_VERB("http request:\n{\n%s}", request.Dump().c_str());
// Block until the asynchronous operation has completed.
do {
io_context_.run_one();
} while (ec == boost::asio::error::would_block);
try {
boost::asio::write(socket, request.ToBuffers());
} catch (boost::system::system_error&) {
return kSocketWriteError;
}
// Read and parse HTTP response.
HttpResponseParser parser(response);
// NOTE:
// We must stop trying to read once all content has been received,
// because some servers will block extra call to read_some().
while (!parser.finished()) {
// read_some() will block until one or more bytes of data has been
// read successfully, or until an error occurs.
std::size_t length = socket.read_some(boost::asio::buffer(buffer_), ec);
if (length == 0 || ec) {
#if defined _WINDOWS
if (ec.value() == WSAETIMEDOUT) {
return kSocketTimeoutError;
}
#endif
return kSocketReadError;
}
// Parse the response piece just read.
// If the content has been fully received, next time flag "finished_"
// will be set.
Error error = parser.Parse(buffer_.data(), length);
if (error != kNoError) {
LOG_ERRO("failed to parse http response.");
return error;
// Determine whether a connection was successfully established. The
// deadline actor may have had a chance to run and close our socket, even
// though the connect operation notionally succeeded. Therefore we must
// check whether the socket is still open before deciding if we succeeded
// or failed.
if (ec || !socket_.is_open()) {
if (ec) {
return kEndpointConnectError;
} else {
return kSocketTimeoutError;
}
}
LOG_VERB("http response:\n{\n%s}", response->Dump().c_str());
return kNoError;
}
Error HttpClient::SendReqeust(const HttpRequest& request) {
LOG_VERB("http request:\n{\n%s}", request.Dump().c_str());
deadline_timer_.expires_from_now(boost::posix_time::seconds(kSendMaxSeconds));
boost::system::error_code ec = boost::asio::error::would_block;
boost::asio::async_write(socket_,
request.ToBuffers(),
boost::lambda::var(ec) = boost::lambda::_1);
// Block until the asynchronous operation has completed.
do {
io_context_.run_one();
} while (ec == boost::asio::error::would_block);
if (ec) {
return kSocketWriteError;
}
return kNoError;
}
Error HttpClient::ReadResponse(HttpResponse* response) {
deadline_timer_.expires_from_now(
boost::posix_time::seconds(timeout_seconds_));
boost::system::error_code ec = boost::asio::error::would_block;
Error error = kNoError;
socket_.async_read_some(
boost::asio::buffer(buffer_),
[this, &ec, &error, response](boost::system::error_code inner_ec,
std::size_t length) {
ec = inner_ec;
if (inner_ec || length == 0) {
error = kSocketReadError;
} else {
// Parse the response piece just read.
// If the content has been fully received, next time flag "finished_"
// will be set.
error = parser_->Parse(buffer_.data(), length);
if (error != kNoError) {
LOG_ERRO("failed to parse http response.");
return;
}
if (parser_->finished()) {
// Stop trying to read once all content has been received,
// because some servers will block extra call to read_some().
return;
}
ReadResponse(response);
}
});
// Block until the asynchronous operation has completed.
do {
io_context_.run_one();
} while (ec == boost::asio::error::would_block);
if (error == kNoError) {
LOG_VERB("http response:\n{\n%s}", response->Dump().c_str());
}
return error;
}
void HttpClient::CheckDeadline() {
if (deadline_timer_.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.
boost::system::error_code ignored_ec;
socket_.close(ignored_ec);
deadline_timer_.expires_at(boost::posix_time::pos_infin);
}
// Put the actor back to sleep.
deadline_timer_.async_wait(std::bind(&HttpClient::CheckDeadline, this));
}
} // namespace webcc
+35 -6
View File
@@ -2,10 +2,14 @@
#define WEBCC_HTTP_CLIENT_H_
#include <array>
#include <memory>
#include "boost/asio/deadline_timer.hpp"
#include "boost/asio/ip/tcp.hpp"
#include "boost/asio/io_context.hpp"
#include "webcc/common.h"
#include "webcc/http_response_parser.h"
namespace webcc {
@@ -16,19 +20,44 @@ class HttpClient {
public:
HttpClient();
// Set socket send & recv timeout.
void set_timeout_seconds(int seconds) {
timeout_seconds_ = seconds;
~HttpClient() = default;
HttpClient(const HttpClient&) = delete;
HttpClient& operator=(const HttpClient&) = delete;
void set_timeout_seconds(int timeout_seconds) {
timeout_seconds_ = timeout_seconds;
}
// Send an HTTP request, wait until the response is received.
Error SendRequest(const HttpRequest& request,
HttpResponse* response);
// Make a HTTP request.
// Connect to the server, send the request, wait until the response is
// received.
Error MakeRequest(const HttpRequest& request, HttpResponse* response);
private:
Error Connect(const HttpRequest& request);
Error SendReqeust(const HttpRequest& request);
Error ReadResponse(HttpResponse* response);
void CheckDeadline();
private:
boost::asio::io_context io_context_;
boost::asio::ip::tcp::socket socket_;
std::array<char, kBufferSize> buffer_;
std::unique_ptr<HttpResponseParser> parser_;
// Maximum seconds to wait before the client cancels the operation.
// Only for receiving response from server.
int timeout_seconds_;
// Timer for the timeout control.
boost::asio::deadline_timer deadline_timer_;
};
} // namespace webcc
+2 -1
View File
@@ -13,7 +13,8 @@ class HttpParser {
public:
explicit HttpParser(HttpMessage* message);
~HttpParser() = default;
virtual ~HttpParser() = default;
HttpParser(const HttpParser&) = delete;
HttpParser& operator=(const HttpParser&) = delete;
+1 -1
View File
@@ -39,7 +39,7 @@ private:
void WorkerRoutine();
// Called by the worker routine.
virtual HttpStatus::Enum HandleSession(HttpSessionPtr session) = 0;
virtual void HandleSession(HttpSessionPtr session) = 0;
private:
Queue<HttpSessionPtr> queue_;
+2
View File
@@ -11,6 +11,8 @@ class HttpRequestParser : public HttpParser {
public:
explicit HttpRequestParser(HttpRequest* request);
~HttpRequestParser() override = default;
private:
Error ParseStartLine(const std::string& line) override;
+2 -2
View File
@@ -16,8 +16,8 @@ class HttpResponse : public HttpMessage {
const HttpResponse& response);
public:
HttpResponse() {
}
HttpResponse() = default;
~HttpResponse() override = default;
int status() const {
return status_;
+2
View File
@@ -11,6 +11,8 @@ class HttpResponseParser : public HttpParser {
public:
explicit HttpResponseParser(HttpResponse* response);
~HttpResponseParser() override = default;
private:
// Parse HTTP start line; E.g., "HTTP/1.1 200 OK".
Error ParseStartLine(const std::string& line) override;
+16 -15
View File
@@ -29,31 +29,32 @@ void HttpSession::Stop() {
socket_.close(ec);
}
void HttpSession::SetResponseContent(const std::string& content_type,
std::size_t content_length,
std::string&& content) {
response_.SetContentType(content_type);
void HttpSession::SetResponseContent(std::string&& content,
const std::string& content_type) {
response_.SetContent(std::move(content));
response_.SetContentType(content_type);
}
void HttpSession::SendResponse() {
void HttpSession::SendResponse(int status) {
response_.set_status(status);
DoWrite();
}
void HttpSession::DoRead() {
auto handler = std::bind(&HttpSession::HandleRead,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2);
socket_.async_read_some(boost::asio::buffer(buffer_), handler);
socket_.async_read_some(boost::asio::buffer(buffer_),
std::bind(&HttpSession::HandleRead,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
}
void HttpSession::DoWrite() {
auto handler = std::bind(&HttpSession::HandleWrite,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2);
boost::asio::async_write(socket_, response_.ToBuffers(), handler);
boost::asio::async_write(socket_,
response_.ToBuffers(),
std::bind(&HttpSession::HandleWrite,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
}
void HttpSession::HandleRead(boost::system::error_code ec,
+4 -9
View File
@@ -33,16 +33,11 @@ public:
void Stop();
void SetResponseStatus(int status) {
response_.set_status(status);
}
void SetResponseContent(std::string&& content,
const std::string& content_type);
void SetResponseContent(const std::string& content_type,
std::size_t content_length,
std::string&& content);
// Write response back to the client.
void SendResponse();
// Write response back to the client with the given HTTP status.
void SendResponse(int status);
private:
void DoRead();
+13 -13
View File
@@ -23,9 +23,11 @@ void LogInit(int level, int modes);
void LogWrite(int level, const char* file, int line, const char* format, ...);
} // namespace webcc
// Initialize the logger with a level.
// E.g., LOG_INIT(ERRO, FLUSH)
#define LOG_INIT(level, modes) LogInit(level, modes);
#define LOG_INIT(level, modes) webcc::LogInit(level, modes);
#if (defined(WIN32) || defined(_WIN64))
@@ -33,19 +35,19 @@ void LogWrite(int level, const char* file, int line, const char* format, ...);
#define __FILENAME__ strrchr("\\" __FILE__, '\\') + 1
#define LOG_VERB(format, ...) \
LogWrite(VERB, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
webcc::LogWrite(webcc::VERB, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
#define LOG_INFO(format, ...) \
LogWrite(INFO, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
webcc::LogWrite(webcc::INFO, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
#define LOG_WARN(format, ...) \
LogWrite(WARN, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
webcc::LogWrite(webcc::WARN, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
#define LOG_ERRO(format, ...) \
LogWrite(ERRO, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
webcc::LogWrite(webcc::ERRO, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
#define LOG_FATA(format, ...) \
LogWrite(FATA, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
webcc::LogWrite(webcc::FATA, __FILENAME__, __LINE__, format, ##__VA_ARGS__);
#else
@@ -53,19 +55,19 @@ void LogWrite(int level, const char* file, int line, const char* format, ...);
#define __FILENAME__ strrchr("/" __FILE__, '/') + 1
#define LOG_VERB(format, args...) \
LogWrite(VERB, __FILENAME__, __LINE__, format, ##args);
webcc::LogWrite(webcc::VERB, __FILENAME__, __LINE__, format, ##args);
#define LOG_INFO(format, args...) \
LogWrite(INFO, __FILENAME__, __LINE__, format, ##args);
webcc::LogWrite(webcc::INFO, __FILENAME__, __LINE__, format, ##args);
#define LOG_WARN(format, args...) \
LogWrite(WARN, __FILENAME__, __LINE__, format, ##args);
webcc::LogWrite(webcc::WARN, __FILENAME__, __LINE__, format, ##args);
#define LOG_ERRO(format, args...) \
LogWrite(ERRO, __FILENAME__, __LINE__, format, ##args);
webcc::LogWrite(webcc::ERRO, __FILENAME__, __LINE__, format, ##args);
#define LOG_FATA(format, args...) \
LogWrite(FATA, __FILENAME__, __LINE__, format, ##args);
webcc::LogWrite(webcc::FATA, __FILENAME__, __LINE__, format, ##args);
#endif // defined(WIN32) || defined(_WIN64)
@@ -89,6 +91,4 @@ void LogWrite(int level, const char* file, int line, const char* format, ...);
#endif // WEBCC_ENABLE_LOG
} // namespace webcc
#endif // WEBCC_LOGGER_H_
+17 -20
View File
@@ -59,42 +59,39 @@ bool RestRequestHandler::RegisterService(RestServicePtr service,
return service_manager_.AddService(service, url);
}
HttpStatus::Enum RestRequestHandler::HandleSession(HttpSessionPtr session) {
void RestRequestHandler::HandleSession(HttpSessionPtr session) {
Url url(session->request().url());
if (!url.IsValid()) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
session->SendResponse(HttpStatus::kBadRequest);
return;
}
std::vector<std::string> sub_matches;
RestServicePtr service = service_manager_.GetService(url.path(), &sub_matches);
if (!service) {
LOG_WARN("No service matches the URL: %s", url.path().c_str());
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
session->SendResponse(HttpStatus::kBadRequest);
return;
}
// TODO: Only for GET?
Url::Query query = Url::SplitQuery(url.query());
// TODO: Error handling.
std::string content;
service->Handle(session->request().method(),
sub_matches,
query,
session->request().content(),
&content);
bool ok = service->Handle(session->request().method(),
sub_matches,
query,
session->request().content(),
&content);
if (!ok) {
// TODO: Could be other than kBadRequest.
session->SendResponse(HttpStatus::kBadRequest);
return;
}
session->SetResponseStatus(HttpStatus::kOK);
session->SetResponseContent(kTextJsonUtf8,
content.length(),
std::move(content));
session->SendResponse();
return HttpStatus::kOK;
session->SetResponseContent(std::move(content), kTextJsonUtf8);
session->SendResponse(HttpStatus::kOK);
}
////////////////////////////////////////////////////////////////////////////////
+1 -1
View File
@@ -75,7 +75,7 @@ public:
bool RegisterService(RestServicePtr service, const std::string& url);
private:
HttpStatus::Enum HandleSession(HttpSessionPtr session) override;
void HandleSession(HttpSessionPtr session) override;
private:
RestServiceManager service_manager_;
+6 -1
View File
@@ -48,7 +48,12 @@ Error SoapClient::Call(const std::string& operation,
HttpResponse http_response;
HttpClient http_client;
Error error = http_client.SendRequest(http_request, &http_response);
if (timeout_seconds_ != -1) {
http_client.set_timeout_seconds(timeout_seconds_);
}
Error error = http_client.MakeRequest(http_request, &http_response);
if (error != kNoError) {
return error;
+5 -4
View File
@@ -13,12 +13,10 @@ namespace webcc {
//
class SoapClient {
public:
virtual ~SoapClient() {
}
virtual ~SoapClient() = default;
protected:
SoapClient() {
}
SoapClient() = default;
// A generic wrapper to make a call.
// NOTE: The parameters should be movable.
@@ -27,6 +25,9 @@ protected:
std::string* result);
protected:
// -1 means default timeout (normally 30s) will be used.
int timeout_seconds_ = -1;
Namespace soapenv_ns_; // SOAP envelope namespace.
Namespace service_ns_; // Namespace for your web service.
+11 -17
View File
@@ -16,36 +16,30 @@ bool SoapRequestHandler::RegisterService(SoapServicePtr service,
return true;
}
HttpStatus::Enum SoapRequestHandler::HandleSession(HttpSessionPtr session) {
void SoapRequestHandler::HandleSession(HttpSessionPtr session) {
SoapServicePtr service = GetServiceByUrl(session->request().url());
if (!service) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
session->SendResponse(HttpStatus::kBadRequest);
return;
}
// Parse the SOAP request XML.
SoapRequest soap_request;
if (!soap_request.FromXml(session->request().content())) {
session->SetResponseStatus(HttpStatus::kBadRequest);
session->SendResponse();
return HttpStatus::kBadRequest;
session->SendResponse(HttpStatus::kBadRequest);
return;
}
// TODO: Error handling.
SoapResponse soap_response;
service->Handle(soap_request, &soap_response);
if (!service->Handle(soap_request, &soap_response)) {
session->SendResponse(HttpStatus::kBadRequest);
return;
}
std::string content;
soap_response.ToXml(&content);
session->SetResponseStatus(HttpStatus::kOK);
session->SetResponseContent(kTextXmlUtf8,
content.length(),
std::move(content));
session->SendResponse();
return HttpStatus::kOK;
session->SetResponseContent(std::move(content), kTextXmlUtf8);
session->SendResponse(HttpStatus::kOK);
}
SoapServicePtr SoapRequestHandler::GetServiceByUrl(const std::string& url) {
+1 -1
View File
@@ -24,7 +24,7 @@ public:
bool RegisterService(SoapServicePtr service, const std::string& url);
private:
HttpStatus::Enum HandleSession(HttpSessionPtr session) override;
void HandleSession(HttpSessionPtr session) override;
SoapServicePtr GetServiceByUrl(const std::string& url);
+4
View File
@@ -2,6 +2,7 @@
#define WEBCC_SOAP_SERVICE_H_
#include <memory>
#include "webcc/common.h"
namespace webcc {
@@ -17,6 +18,9 @@ public:
// Handle SOAP request, output the response.
virtual bool Handle(const SoapRequest& soap_request,
SoapResponse* soap_response) = 0;
protected:
HttpStatus::Enum http_status_ = HttpStatus::kOK;
};
typedef std::shared_ptr<SoapService> SoapServicePtr;