Refine HTTP request and response.

master
Adam Gu 7 years ago
parent 88511caff6
commit cbf3efbcba

@ -2,8 +2,8 @@
#include "boost/asio/io_context.hpp" #include "boost/asio/io_context.hpp"
#include "webcc/logger.h"
#include "webcc/async_http_client.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 // 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: // simply "Hello, World!", then start a HTTP server with Python 3:
@ -11,13 +11,12 @@
// The default port number should be 8000. // The default port number should be 8000.
void Test(boost::asio::io_context& ioc) { 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_method(webcc::kHttpGet);
request->set_url("/index.html"); request->set_url("/index.html");
request->SetHost("localhost", "8000"); request->SetHost("localhost", "8000");
request->UpdateStartLine();
request->Build();
webcc::HttpAsyncClientPtr client(new webcc::AsyncHttpClient(ioc)); webcc::HttpAsyncClientPtr client(new webcc::AsyncHttpClient(ioc));

@ -1,7 +1,7 @@
#include <iostream> #include <iostream>
#include "webcc/logger.h"
#include "webcc/http_client.h" #include "webcc/http_client.h"
#include "webcc/logger.h"
// In order to test this client, create a file index.html whose content is // 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: // simply "Hello, World!", then start a HTTP server with Python 3:
@ -10,21 +10,21 @@
void Test() { void Test() {
webcc::HttpRequest request; webcc::HttpRequest request;
request.set_method(webcc::kHttpGet); request.set_method(webcc::kHttpGet);
request.set_url("/index.html"); request.set_url("/index.html");
request.SetHost("localhost", "8000"); request.SetHost("localhost", "8000");
request.UpdateStartLine();
request.Build();
webcc::HttpResponse response;
webcc::HttpClient client; webcc::HttpClient client;
if (!client.Request(request)) { if (client.Request(request)) {
return; 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() { int main() {

@ -2,8 +2,8 @@
#include "json/json.h" #include "json/json.h"
#include "webcc/logger.h"
#include "webcc/async_rest_client.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) { if (error == webcc::kNoError) {
std::cout << response->content() << std::endl; std::cout << response->content() << std::endl;
} else { } 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->SetContent(content);
} }
request->Build(); request->UpdateStartLine();
HttpAsyncClientPtr http_client(new AsyncHttpClient(io_context_)); HttpAsyncClientPtr http_client(new AsyncHttpClient(io_context_));

@ -17,6 +17,8 @@ namespace webcc {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Constants // Constants
const char* const CRLF = "\r\n";
// Default buffer size for socket reading. // Default buffer size for socket reading.
const std::size_t kBufferSize = 1024; const std::size_t kBufferSize = 1024;

@ -24,8 +24,8 @@ void HttpConnection::Start() {
} }
void HttpConnection::Close() { void HttpConnection::Close() {
boost::system::error_code ec; boost::system::error_code ignored_ec;
socket_.close(ec); socket_.close(ignored_ec);
} }
void HttpConnection::SetResponseContent(std::string&& content, void HttpConnection::SetResponseContent(std::string&& content,
@ -36,6 +36,7 @@ void HttpConnection::SetResponseContent(std::string&& content,
void HttpConnection::SendResponse(HttpStatus::Enum status) { void HttpConnection::SendResponse(HttpStatus::Enum status) {
response_.set_status(status); response_.set_status(status);
response_.UpdateStartLine();
AsyncWrite(); AsyncWrite();
} }
@ -58,6 +59,7 @@ void HttpConnection::ReadHandler(boost::system::error_code ec,
if (!request_parser_.Parse(buffer_.data(), length)) { if (!request_parser_.Parse(buffer_.data(), length)) {
// Bad request. // Bad request.
LOG_ERRO("Failed to parse HTTP request.");
response_ = HttpResponse::Fault(HttpStatus::kBadRequest); response_ = HttpResponse::Fault(HttpStatus::kBadRequest);
AsyncWrite(); AsyncWrite();
return; return;
@ -69,13 +71,16 @@ void HttpConnection::ReadHandler(boost::system::error_code ec,
return; return;
} }
LOG_VERB("HTTP request:\n%s", request_.Dump(4, "> ").c_str());
// Enqueue this connection. // Enqueue this connection.
// Some worker thread will handle it later. // Some worker thread will handle it later.
// And DoWrite() will be called in the worker thread.
request_handler_->Enqueue(shared_from_this()); request_handler_->Enqueue(shared_from_this());
} }
void HttpConnection::AsyncWrite() { void HttpConnection::AsyncWrite() {
LOG_VERB("HTTP response:\n%s", response_.Dump(4, "> ").c_str());
boost::asio::async_write(socket_, boost::asio::async_write(socket_,
response_.ToBuffers(), response_.ToBuffers(),
std::bind(&HttpConnection::WriteHandler, std::bind(&HttpConnection::WriteHandler,
@ -86,8 +91,8 @@ void HttpConnection::AsyncWrite() {
// NOTE: // NOTE:
// This write handler will be called from main thread (the thread calling // 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 // io_context.run), even though AsyncWrite() is invoked by worker threads.
// ensured by Asio. // This is ensured by Asio.
void HttpConnection::WriteHandler(boost::system::error_code ec, void HttpConnection::WriteHandler(boost::system::error_code ec,
std::size_t length) { std::size_t length) {
if (!ec) { if (!ec) {

@ -6,6 +6,17 @@
namespace webcc { 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) { void HttpMessage::SetHeader(const std::string& name, const std::string& value) {
for (HttpHeader& h : headers_) { for (HttpHeader& h : headers_) {
if (h.name == name) { if (h.name == name) {
@ -17,6 +28,30 @@ void HttpMessage::SetHeader(const std::string& name, const std::string& value) {
headers_.push_back({ name, 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, void HttpMessage::Dump(std::ostream& os, std::size_t indent,
const std::string& prefix) const { const std::string& prefix) const {
std::string indent_str; std::string indent_str;
@ -38,7 +73,7 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent,
os << content_ << std::endl; os << content_ << std::endl;
} else { } else {
std::vector<std::string> splitted; 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) { for (const std::string& line : splitted) {
os << indent_str << line << std::endl; os << indent_str << line << std::endl;
} }

@ -6,6 +6,8 @@
#include <utility> // for move() #include <utility> // for move()
#include <vector> #include <vector>
#include "boost/asio/buffer.hpp" // for const_buffer
#include "webcc/globals.h" #include "webcc/globals.h"
namespace webcc { namespace webcc {
@ -18,6 +20,8 @@ struct HttpHeader {
// Base class for HTTP request and response messages. // Base class for HTTP request and response messages.
class HttpMessage { class HttpMessage {
public: public:
HttpMessage() : content_length_(kInvalidLength) {}
virtual ~HttpMessage() = default; virtual ~HttpMessage() = default;
const std::string& start_line() const { return start_line_; } const std::string& start_line() const { return start_line_; }
@ -47,6 +51,15 @@ class HttpMessage {
SetContentLength(content_.size()); 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. // Dump to output stream.
void Dump(std::ostream& os, std::size_t indent = 0, void Dump(std::ostream& os, std::size_t indent = 0,
const std::string& prefix = "") const; const std::string& prefix = "") const;
@ -61,10 +74,10 @@ class HttpMessage {
SetHeader(kContentLength, std::to_string(content_length)); SetHeader(kContentLength, std::to_string(content_length));
} }
// Start line with trailing "\r\n". // Start line with trailing CRLF.
std::string start_line_; std::string start_line_;
std::size_t content_length_ = kInvalidLength; std::size_t content_length_;
std::vector<HttpHeader> headers_; std::vector<HttpHeader> headers_;

@ -33,7 +33,7 @@ bool HttpParser::Parse(const char* data, std::size_t length) {
std::size_t off = 0; std::size_t off = 0;
while (true) { 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) { if (pos == std::string::npos) {
break; break;
} }
@ -48,6 +48,7 @@ bool HttpParser::Parse(const char* data, std::size_t length) {
if (!start_line_parsed_) { if (!start_line_parsed_) {
start_line_parsed_ = true; start_line_parsed_ = true;
message_->set_start_line(line + CRLF);
if (!ParseStartLine(line)) { if (!ParseStartLine(line)) {
return false; return false;
} }
@ -111,7 +112,7 @@ void HttpParser::ParseContentLength(const std::string& line) {
try { try {
content_length_ = static_cast<std::size_t>(std::stoul(value)); 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()); LOG_ERRO("Invalid content length: %s.", value.c_str());
} }

@ -2,17 +2,6 @@
namespace webcc { 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) { void HttpRequest::SetHost(const std::string& host, const std::string& port) {
host_ = host; host_ = host;
port_ = port; port_ = port;
@ -24,37 +13,12 @@ void HttpRequest::SetHost(const std::string& host, const std::string& port) {
} }
} }
void HttpRequest::Build() { void HttpRequest::UpdateStartLine() {
if (start_line_.empty()) {
start_line_ = method_; start_line_ = method_;
start_line_ += " "; start_line_ += " ";
start_line_ += url_; start_line_ += url_;
start_line_ += " HTTP/1.1\r\n"; start_line_ += " HTTP/1.1";
} start_line_ += CRLF;
}
// 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;
} }
} // namespace webcc } // namespace webcc

@ -3,9 +3,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "boost/asio/buffer.hpp" // for const_buffer
#include "webcc/http_message.h" #include "webcc/http_message.h"
@ -14,9 +11,6 @@ namespace webcc {
class HttpRequest : public HttpMessage { class HttpRequest : public HttpMessage {
public: public:
HttpRequest() = default; HttpRequest() = default;
HttpRequest(const HttpRequest&) = default;
HttpRequest& operator=(const HttpRequest&) = default;
virtual ~HttpRequest() = default; virtual ~HttpRequest() = default;
const std::string& method() const { return method_; } 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. // 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); void SetHost(const std::string& host, const std::string& port);
// Compose start line, etc. // Set start line according to HTTP method, URL, etc.
// Must be called before ToBuffers()! void UpdateStartLine() override;
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;
private: private:
// HTTP method. // HTTP method.

@ -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 NOT_IMPLEMENTED = "HTTP/1.1 501 Not Implemented\r\n";
const std::string SERVICE_UNAVAILABLE = "HTTP/1.1 503 Service Unavailable\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) { switch (status) {
case HttpStatus::kOK: case HttpStatus::kOK:
return boost::asio::buffer(OK); return OK;
case HttpStatus::kCreated: case HttpStatus::kCreated:
return boost::asio::buffer(CREATED); return CREATED;
case HttpStatus::kAccepted: case HttpStatus::kAccepted:
return boost::asio::buffer(ACCEPTED); return ACCEPTED;
case HttpStatus::kNoContent: case HttpStatus::kNoContent:
return boost::asio::buffer(NO_CONTENT); return NO_CONTENT;
case HttpStatus::kNotModified: case HttpStatus::kNotModified:
return boost::asio::buffer(NOT_MODIFIED); return NOT_MODIFIED;
case HttpStatus::kBadRequest: case HttpStatus::kBadRequest:
return boost::asio::buffer(BAD_REQUEST); return BAD_REQUEST;
case HttpStatus::kNotFound: case HttpStatus::kNotFound:
return boost::asio::buffer(NOT_FOUND); return NOT_FOUND;
case HttpStatus::InternalServerError: case HttpStatus::InternalServerError:
return boost::asio::buffer(INTERNAL_SERVER_ERROR); return INTERNAL_SERVER_ERROR;
case HttpStatus::kNotImplemented: case HttpStatus::kNotImplemented:
return boost::asio::buffer(NOT_IMPLEMENTED); return NOT_IMPLEMENTED;
case HttpStatus::kServiceUnavailable: case HttpStatus::kServiceUnavailable:
return boost::asio::buffer(SERVICE_UNAVAILABLE); return SERVICE_UNAVAILABLE;
default: default:
return boost::asio::buffer(NOT_IMPLEMENTED); return NOT_IMPLEMENTED;
} }
} }
} // namespace status_strings } // namespace status_strings
namespace misc_strings { void HttpResponse::UpdateStartLine() {
start_line_ = status_strings::ToString(status_);
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;
} }
HttpResponse HttpResponse::Fault(HttpStatus::Enum status) { HttpResponse HttpResponse::Fault(HttpStatus::Enum status) {
@ -92,6 +64,7 @@ HttpResponse HttpResponse::Fault(HttpStatus::Enum status) {
HttpResponse response; HttpResponse response;
response.set_status(status); response.set_status(status);
response.UpdateStartLine();
return response; return response;
} }

@ -3,9 +3,6 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector>
#include "boost/asio/buffer.hpp" // for const_buffer
#include "webcc/http_message.h" #include "webcc/http_message.h"
@ -13,20 +10,19 @@ namespace webcc {
class HttpResponse : public HttpMessage { class HttpResponse : public HttpMessage {
public: public:
HttpResponse() : status_(HttpStatus::kOK) { HttpResponse() : status_(HttpStatus::kOK) {}
}
~HttpResponse() override = default; ~HttpResponse() override = default;
int status() const { return status_; } int status() const { return status_; }
void set_status(int status) { status_ = status; } void set_status(int status) { status_ = status; }
// Convert the response into a vector of buffers. The buffers do not own the // Set start line according to status code.
// underlying memory blocks, therefore the response object must remain valid void UpdateStartLine() override;
// and not be changed until the write operation has completed.
std::vector<boost::asio::const_buffer> ToBuffers() const;
// Get a fault response when HTTP status is not OK. // Get a fault response when HTTP status is not OK.
// TODO: Avoid copy.
static HttpResponse Fault(HttpStatus::Enum status); static HttpResponse Fault(HttpStatus::Enum status);
private: private:

@ -10,8 +10,6 @@ HttpResponseParser::HttpResponseParser(HttpResponse* response)
} }
bool HttpResponseParser::ParseStartLine(const std::string& line) { bool HttpResponseParser::ParseStartLine(const std::string& line) {
response_->set_start_line(line + "\r\n");
std::size_t off = 0; std::size_t off = 0;
std::size_t pos = line.find(' '); std::size_t pos = line.find(' ');

@ -31,7 +31,7 @@ bool RestClient::Request(const std::string& method,
request.SetContent(content); request.SetContent(content);
} }
request.Build(); request.UpdateStartLine();
HttpClient http_client; HttpClient http_client;

@ -44,7 +44,7 @@ Error SoapClient::Call(const std::string& operation,
http_request.SetContent(std::move(http_content)); http_request.SetContent(std::move(http_content));
http_request.SetHost(host_, port_); http_request.SetHost(host_, port_);
http_request.SetHeader(kSoapAction, operation); http_request.SetHeader(kSoapAction, operation);
http_request.Build(); http_request.UpdateStartLine();
HttpResponse http_response; HttpResponse http_response;

Loading…
Cancel
Save