Refine HTTP request and response.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "boost/asio/io_context.hpp"
|
||||
|
||||
#include "webcc/logger.h"
|
||||
#include "webcc/async_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:
|
||||
@@ -11,13 +11,12 @@
|
||||
// The default port number should be 8000.
|
||||
|
||||
void Test(boost::asio::io_context& ioc) {
|
||||
std::shared_ptr<webcc::HttpRequest> request(new webcc::HttpRequest());
|
||||
webcc::HttpRequestPtr request(new webcc::HttpRequest());
|
||||
|
||||
request->set_method(webcc::kHttpGet);
|
||||
request->set_url("/index.html");
|
||||
request->SetHost("localhost", "8000");
|
||||
|
||||
request->Build();
|
||||
request->UpdateStartLine();
|
||||
|
||||
webcc::HttpAsyncClientPtr client(new webcc::AsyncHttpClient(ioc));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "webcc/logger.h"
|
||||
#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:
|
||||
@@ -10,21 +10,21 @@
|
||||
|
||||
void Test() {
|
||||
webcc::HttpRequest request;
|
||||
|
||||
request.set_method(webcc::kHttpGet);
|
||||
request.set_url("/index.html");
|
||||
request.SetHost("localhost", "8000");
|
||||
|
||||
request.Build();
|
||||
|
||||
webcc::HttpResponse response;
|
||||
request.UpdateStartLine();
|
||||
|
||||
webcc::HttpClient client;
|
||||
if (!client.Request(request)) {
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
std::cout << response.content() << std::endl;
|
||||
}
|
||||
|
||||
int main() {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "json/json.h"
|
||||
|
||||
#include "webcc/logger.h"
|
||||
#include "webcc/async_rest_client.h"
|
||||
#include "webcc/logger.h"
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -118,7 +118,11 @@ int main(int argc, char* argv[]) {
|
||||
if (error == webcc::kNoError) {
|
||||
std::cout << response->content() << std::endl;
|
||||
} else {
|
||||
std::cout << webcc::DescribeError(error) << std::endl;
|
||||
std::cout << webcc::DescribeError(error);
|
||||
if (timed_out) {
|
||||
std::cout << " (timed out)";
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ void AsyncRestClient::Request(const std::string& method,
|
||||
request->SetContent(content);
|
||||
}
|
||||
|
||||
request->Build();
|
||||
request->UpdateStartLine();
|
||||
|
||||
HttpAsyncClientPtr http_client(new AsyncHttpClient(io_context_));
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace webcc {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Constants
|
||||
|
||||
const char* const CRLF = "\r\n";
|
||||
|
||||
// Default buffer size for socket reading.
|
||||
const std::size_t kBufferSize = 1024;
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ void HttpConnection::Start() {
|
||||
}
|
||||
|
||||
void HttpConnection::Close() {
|
||||
boost::system::error_code ec;
|
||||
socket_.close(ec);
|
||||
boost::system::error_code ignored_ec;
|
||||
socket_.close(ignored_ec);
|
||||
}
|
||||
|
||||
void HttpConnection::SetResponseContent(std::string&& content,
|
||||
@@ -36,6 +36,7 @@ void HttpConnection::SetResponseContent(std::string&& content,
|
||||
|
||||
void HttpConnection::SendResponse(HttpStatus::Enum status) {
|
||||
response_.set_status(status);
|
||||
response_.UpdateStartLine();
|
||||
AsyncWrite();
|
||||
}
|
||||
|
||||
@@ -58,6 +59,7 @@ void HttpConnection::ReadHandler(boost::system::error_code ec,
|
||||
|
||||
if (!request_parser_.Parse(buffer_.data(), length)) {
|
||||
// Bad request.
|
||||
LOG_ERRO("Failed to parse HTTP request.");
|
||||
response_ = HttpResponse::Fault(HttpStatus::kBadRequest);
|
||||
AsyncWrite();
|
||||
return;
|
||||
@@ -69,13 +71,16 @@ void HttpConnection::ReadHandler(boost::system::error_code ec,
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_VERB("HTTP request:\n%s", request_.Dump(4, "> ").c_str());
|
||||
|
||||
// Enqueue this connection.
|
||||
// Some worker thread will handle it later.
|
||||
// And DoWrite() will be called in the worker thread.
|
||||
request_handler_->Enqueue(shared_from_this());
|
||||
}
|
||||
|
||||
void HttpConnection::AsyncWrite() {
|
||||
LOG_VERB("HTTP response:\n%s", response_.Dump(4, "> ").c_str());
|
||||
|
||||
boost::asio::async_write(socket_,
|
||||
response_.ToBuffers(),
|
||||
std::bind(&HttpConnection::WriteHandler,
|
||||
@@ -86,8 +91,8 @@ void HttpConnection::AsyncWrite() {
|
||||
|
||||
// NOTE:
|
||||
// This write handler will be called from main thread (the thread calling
|
||||
// io_context.run), even though DoWrite() is invoked by worker threads. This is
|
||||
// ensured by Asio.
|
||||
// io_context.run), even though AsyncWrite() is invoked by worker threads.
|
||||
// This is ensured by Asio.
|
||||
void HttpConnection::WriteHandler(boost::system::error_code ec,
|
||||
std::size_t length) {
|
||||
if (!ec) {
|
||||
|
||||
+36
-1
@@ -6,6 +6,17 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace misc_strings {
|
||||
|
||||
const char NAME_VALUE_SEPARATOR[] = { ':', ' ' };
|
||||
const char CRLF[] = { '\r', '\n' };
|
||||
|
||||
} // misc_strings
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void HttpMessage::SetHeader(const std::string& name, const std::string& value) {
|
||||
for (HttpHeader& h : headers_) {
|
||||
if (h.name == name) {
|
||||
@@ -17,6 +28,30 @@ void HttpMessage::SetHeader(const std::string& name, const std::string& value) {
|
||||
headers_.push_back({ name, value });
|
||||
}
|
||||
|
||||
// ATTENTION: The buffers don't hold the memory!
|
||||
std::vector<boost::asio::const_buffer> HttpMessage::ToBuffers() const {
|
||||
assert(!start_line_.empty());
|
||||
|
||||
std::vector<boost::asio::const_buffer> buffers;
|
||||
|
||||
buffers.push_back(boost::asio::buffer(start_line_));
|
||||
|
||||
for (const HttpHeader& h : headers_) {
|
||||
buffers.push_back(boost::asio::buffer(h.name));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR));
|
||||
buffers.push_back(boost::asio::buffer(h.value));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
}
|
||||
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
|
||||
if (content_length_ > 0) {
|
||||
buffers.push_back(boost::asio::buffer(content_));
|
||||
}
|
||||
|
||||
return buffers;
|
||||
}
|
||||
|
||||
void HttpMessage::Dump(std::ostream& os, std::size_t indent,
|
||||
const std::string& prefix) const {
|
||||
std::string indent_str;
|
||||
@@ -38,7 +73,7 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent,
|
||||
os << content_ << std::endl;
|
||||
} else {
|
||||
std::vector<std::string> splitted;
|
||||
boost::split(splitted, content_, boost::is_any_of("\r\n"));
|
||||
boost::split(splitted, content_, boost::is_any_of(CRLF));
|
||||
for (const std::string& line : splitted) {
|
||||
os << indent_str << line << std::endl;
|
||||
}
|
||||
|
||||
+15
-2
@@ -6,6 +6,8 @@
|
||||
#include <utility> // for move()
|
||||
#include <vector>
|
||||
|
||||
#include "boost/asio/buffer.hpp" // for const_buffer
|
||||
|
||||
#include "webcc/globals.h"
|
||||
|
||||
namespace webcc {
|
||||
@@ -18,6 +20,8 @@ struct HttpHeader {
|
||||
// Base class for HTTP request and response messages.
|
||||
class HttpMessage {
|
||||
public:
|
||||
HttpMessage() : content_length_(kInvalidLength) {}
|
||||
|
||||
virtual ~HttpMessage() = default;
|
||||
|
||||
const std::string& start_line() const { return start_line_; }
|
||||
@@ -47,6 +51,15 @@ class HttpMessage {
|
||||
SetContentLength(content_.size());
|
||||
}
|
||||
|
||||
// Set start line according to other informations.
|
||||
// Must be called before ToBuffers()!
|
||||
virtual void UpdateStartLine() = 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
|
||||
// and not be changed until the write operation has completed.
|
||||
std::vector<boost::asio::const_buffer> ToBuffers() const;
|
||||
|
||||
// Dump to output stream.
|
||||
void Dump(std::ostream& os, std::size_t indent = 0,
|
||||
const std::string& prefix = "") const;
|
||||
@@ -61,10 +74,10 @@ class HttpMessage {
|
||||
SetHeader(kContentLength, std::to_string(content_length));
|
||||
}
|
||||
|
||||
// Start line with trailing "\r\n".
|
||||
// Start line with trailing CRLF.
|
||||
std::string start_line_;
|
||||
|
||||
std::size_t content_length_ = kInvalidLength;
|
||||
std::size_t content_length_;
|
||||
|
||||
std::vector<HttpHeader> headers_;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ bool HttpParser::Parse(const char* data, std::size_t length) {
|
||||
std::size_t off = 0;
|
||||
|
||||
while (true) {
|
||||
std::size_t pos = pending_data_.find("\r\n", off);
|
||||
std::size_t pos = pending_data_.find(CRLF, off);
|
||||
if (pos == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
@@ -48,6 +48,7 @@ bool HttpParser::Parse(const char* data, std::size_t length) {
|
||||
|
||||
if (!start_line_parsed_) {
|
||||
start_line_parsed_ = true;
|
||||
message_->set_start_line(line + CRLF);
|
||||
if (!ParseStartLine(line)) {
|
||||
return false;
|
||||
}
|
||||
@@ -111,7 +112,7 @@ void HttpParser::ParseContentLength(const std::string& line) {
|
||||
|
||||
try {
|
||||
content_length_ = static_cast<std::size_t>(std::stoul(value));
|
||||
} catch (const std::exception& e) {
|
||||
} catch (const std::exception&) {
|
||||
LOG_ERRO("Invalid content length: %s.", value.c_str());
|
||||
}
|
||||
|
||||
|
||||
+3
-39
@@ -2,17 +2,6 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace misc_strings {
|
||||
|
||||
const char NAME_VALUE_SEPARATOR[] = { ':', ' ' };
|
||||
const char CRLF[] = { '\r', '\n' };
|
||||
|
||||
} // misc_strings
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void HttpRequest::SetHost(const std::string& host, const std::string& port) {
|
||||
host_ = host;
|
||||
port_ = port;
|
||||
@@ -24,37 +13,12 @@ void HttpRequest::SetHost(const std::string& host, const std::string& port) {
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequest::Build() {
|
||||
if (start_line_.empty()) {
|
||||
void HttpRequest::UpdateStartLine() {
|
||||
start_line_ = method_;
|
||||
start_line_ += " ";
|
||||
start_line_ += url_;
|
||||
start_line_ += " HTTP/1.1\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
// ATTENTION: The buffers don't hold the memory!
|
||||
std::vector<boost::asio::const_buffer> HttpRequest::ToBuffers() const {
|
||||
assert(!start_line_.empty());
|
||||
|
||||
std::vector<boost::asio::const_buffer> buffers;
|
||||
|
||||
buffers.push_back(boost::asio::buffer(start_line_));
|
||||
|
||||
for (const HttpHeader& h : headers_) {
|
||||
buffers.push_back(boost::asio::buffer(h.name));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR));
|
||||
buffers.push_back(boost::asio::buffer(h.value));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
}
|
||||
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
|
||||
if (content_length_ > 0) {
|
||||
buffers.push_back(boost::asio::buffer(content_));
|
||||
}
|
||||
|
||||
return buffers;
|
||||
start_line_ += " HTTP/1.1";
|
||||
start_line_ += CRLF;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+2
-14
@@ -3,9 +3,6 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "boost/asio/buffer.hpp" // for const_buffer
|
||||
|
||||
#include "webcc/http_message.h"
|
||||
|
||||
@@ -14,9 +11,6 @@ namespace webcc {
|
||||
class HttpRequest : public HttpMessage {
|
||||
public:
|
||||
HttpRequest() = default;
|
||||
HttpRequest(const HttpRequest&) = default;
|
||||
HttpRequest& operator=(const HttpRequest&) = default;
|
||||
|
||||
virtual ~HttpRequest() = default;
|
||||
|
||||
const std::string& method() const { return method_; }
|
||||
@@ -33,14 +27,8 @@ class HttpRequest : public HttpMessage {
|
||||
// a numeric number (e.g., "9000") and "80" will be used if it's empty.
|
||||
void SetHost(const std::string& host, const std::string& port);
|
||||
|
||||
// Compose start line, etc.
|
||||
// Must be called before ToBuffers()!
|
||||
void Build();
|
||||
|
||||
// Convert the response into a vector of buffers. The buffers do not own the
|
||||
// underlying memory blocks, therefore the request object must remain valid
|
||||
// and not be changed until the write operation has completed.
|
||||
std::vector<boost::asio::const_buffer> ToBuffers() const;
|
||||
// Set start line according to HTTP method, URL, etc.
|
||||
void UpdateStartLine() override;
|
||||
|
||||
private:
|
||||
// HTTP method.
|
||||
|
||||
+15
-42
@@ -16,75 +16,47 @@ const std::string INTERNAL_SERVER_ERROR =
|
||||
const std::string NOT_IMPLEMENTED = "HTTP/1.1 501 Not Implemented\r\n";
|
||||
const std::string SERVICE_UNAVAILABLE = "HTTP/1.1 503 Service Unavailable\r\n";
|
||||
|
||||
boost::asio::const_buffer ToBuffer(int status) {
|
||||
const std::string& ToString(int status) {
|
||||
switch (status) {
|
||||
case HttpStatus::kOK:
|
||||
return boost::asio::buffer(OK);
|
||||
return OK;
|
||||
|
||||
case HttpStatus::kCreated:
|
||||
return boost::asio::buffer(CREATED);
|
||||
return CREATED;
|
||||
|
||||
case HttpStatus::kAccepted:
|
||||
return boost::asio::buffer(ACCEPTED);
|
||||
return ACCEPTED;
|
||||
|
||||
case HttpStatus::kNoContent:
|
||||
return boost::asio::buffer(NO_CONTENT);
|
||||
return NO_CONTENT;
|
||||
|
||||
case HttpStatus::kNotModified:
|
||||
return boost::asio::buffer(NOT_MODIFIED);
|
||||
return NOT_MODIFIED;
|
||||
|
||||
case HttpStatus::kBadRequest:
|
||||
return boost::asio::buffer(BAD_REQUEST);
|
||||
return BAD_REQUEST;
|
||||
|
||||
case HttpStatus::kNotFound:
|
||||
return boost::asio::buffer(NOT_FOUND);
|
||||
return NOT_FOUND;
|
||||
|
||||
case HttpStatus::InternalServerError:
|
||||
return boost::asio::buffer(INTERNAL_SERVER_ERROR);
|
||||
return INTERNAL_SERVER_ERROR;
|
||||
|
||||
case HttpStatus::kNotImplemented:
|
||||
return boost::asio::buffer(NOT_IMPLEMENTED);
|
||||
return NOT_IMPLEMENTED;
|
||||
|
||||
case HttpStatus::kServiceUnavailable:
|
||||
return boost::asio::buffer(SERVICE_UNAVAILABLE);
|
||||
return SERVICE_UNAVAILABLE;
|
||||
|
||||
default:
|
||||
return boost::asio::buffer(NOT_IMPLEMENTED);
|
||||
return NOT_IMPLEMENTED;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace status_strings
|
||||
|
||||
namespace misc_strings {
|
||||
|
||||
const char NAME_VALUE_SEPARATOR[] = { ':', ' ' };
|
||||
const char CRLF[] = { '\r', '\n' };
|
||||
|
||||
} // misc_strings
|
||||
|
||||
// ATTENTION: The buffers don't hold the memory!
|
||||
std::vector<boost::asio::const_buffer> HttpResponse::ToBuffers() const {
|
||||
std::vector<boost::asio::const_buffer> buffers;
|
||||
|
||||
// Status line
|
||||
buffers.push_back(status_strings::ToBuffer(status_));
|
||||
|
||||
// Header fields (optional)
|
||||
for (const HttpHeader& h : headers_) {
|
||||
buffers.push_back(boost::asio::buffer(h.name));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::NAME_VALUE_SEPARATOR));
|
||||
buffers.push_back(boost::asio::buffer(h.value));
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
}
|
||||
|
||||
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
|
||||
|
||||
// Content (optional)
|
||||
if (!content_.empty()) {
|
||||
buffers.push_back(boost::asio::buffer(content_));
|
||||
}
|
||||
|
||||
return buffers;
|
||||
void HttpResponse::UpdateStartLine() {
|
||||
start_line_ = status_strings::ToString(status_);
|
||||
}
|
||||
|
||||
HttpResponse HttpResponse::Fault(HttpStatus::Enum status) {
|
||||
@@ -92,6 +64,7 @@ HttpResponse HttpResponse::Fault(HttpStatus::Enum status) {
|
||||
|
||||
HttpResponse response;
|
||||
response.set_status(status);
|
||||
response.UpdateStartLine();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "boost/asio/buffer.hpp" // for const_buffer
|
||||
|
||||
#include "webcc/http_message.h"
|
||||
|
||||
@@ -13,20 +10,19 @@ namespace webcc {
|
||||
|
||||
class HttpResponse : public HttpMessage {
|
||||
public:
|
||||
HttpResponse() : status_(HttpStatus::kOK) {
|
||||
}
|
||||
HttpResponse() : status_(HttpStatus::kOK) {}
|
||||
|
||||
~HttpResponse() override = default;
|
||||
|
||||
int status() const { return status_; }
|
||||
|
||||
void set_status(int status) { status_ = status; }
|
||||
|
||||
// Convert the response into a vector of buffers. The buffers do not own the
|
||||
// underlying memory blocks, therefore the response object must remain valid
|
||||
// and not be changed until the write operation has completed.
|
||||
std::vector<boost::asio::const_buffer> ToBuffers() const;
|
||||
// Set start line according to status code.
|
||||
void UpdateStartLine() override;
|
||||
|
||||
// Get a fault response when HTTP status is not OK.
|
||||
// TODO: Avoid copy.
|
||||
static HttpResponse Fault(HttpStatus::Enum status);
|
||||
|
||||
private:
|
||||
|
||||
@@ -10,8 +10,6 @@ HttpResponseParser::HttpResponseParser(HttpResponse* response)
|
||||
}
|
||||
|
||||
bool HttpResponseParser::ParseStartLine(const std::string& line) {
|
||||
response_->set_start_line(line + "\r\n");
|
||||
|
||||
std::size_t off = 0;
|
||||
|
||||
std::size_t pos = line.find(' ');
|
||||
|
||||
@@ -31,7 +31,7 @@ bool RestClient::Request(const std::string& method,
|
||||
request.SetContent(content);
|
||||
}
|
||||
|
||||
request.Build();
|
||||
request.UpdateStartLine();
|
||||
|
||||
HttpClient http_client;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Error SoapClient::Call(const std::string& operation,
|
||||
http_request.SetContent(std::move(http_content));
|
||||
http_request.SetHost(host_, port_);
|
||||
http_request.SetHeader(kSoapAction, operation);
|
||||
http_request.Build();
|
||||
http_request.UpdateStartLine();
|
||||
|
||||
HttpResponse http_response;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user