Support data streaming on server side.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#if 0
|
||||
// HTTP GET request parser test fixture.
|
||||
class GetRequestParserTest : public testing::Test {
|
||||
protected:
|
||||
@@ -231,3 +232,4 @@ TEST_F(MultipartRequestParserTest, ParseByteWise) {
|
||||
|
||||
CheckResult();
|
||||
}
|
||||
#endif // 0
|
||||
+3
-3
@@ -12,14 +12,14 @@ using boost::asio::ip::tcp;
|
||||
namespace webcc {
|
||||
|
||||
Connection::Connection(tcp::socket socket, ConnectionPool* pool,
|
||||
Queue<ConnectionPtr>* queue)
|
||||
Queue<ConnectionPtr>* queue, ViewMatcher&& view_matcher)
|
||||
: socket_(std::move(socket)), pool_(pool), queue_(queue),
|
||||
buffer_(kBufferSize) {
|
||||
view_matcher_(std::move(view_matcher)), buffer_(kBufferSize) {
|
||||
}
|
||||
|
||||
void Connection::Start() {
|
||||
request_.reset(new Request{});
|
||||
request_parser_.Init(request_.get());
|
||||
request_parser_.Init(request_.get(), view_matcher_);
|
||||
DoRead();
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -24,7 +24,7 @@ using ConnectionPtr = std::shared_ptr<Connection>;
|
||||
class Connection : public std::enable_shared_from_this<Connection> {
|
||||
public:
|
||||
Connection(boost::asio::ip::tcp::socket socket, ConnectionPool* pool,
|
||||
Queue<ConnectionPtr>* queue);
|
||||
Queue<ConnectionPtr>* queue, ViewMatcher&& view_matcher);
|
||||
|
||||
~Connection() = default;
|
||||
|
||||
@@ -71,6 +71,10 @@ private:
|
||||
// The connection queue.
|
||||
Queue<ConnectionPtr>* queue_;
|
||||
|
||||
// A function for matching view once the headers of a request has been
|
||||
// received.
|
||||
ViewMatcher view_matcher_;
|
||||
|
||||
// The buffer for incoming data.
|
||||
std::vector<char> buffer_;
|
||||
|
||||
|
||||
+26
-4
@@ -65,22 +65,24 @@ bool StringBodyHandler::Finish() {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
FileBodyHandler::FileBodyHandler(Message* message) : BodyHandler(message) {
|
||||
bool FileBodyHandler::OpenFile() {
|
||||
try {
|
||||
temp_path_ = bfs::temp_directory_path() / bfs::unique_path();
|
||||
LOG_VERB("Generate a temp path for streaming: %s",
|
||||
temp_path_.string().c_str());
|
||||
} catch (const bfs::filesystem_error&) {
|
||||
LOG_ERRO("Failed to generate temp path: %s", temp_path_.string().c_str());
|
||||
throw Error{ Error::kFileError };
|
||||
return false;
|
||||
}
|
||||
|
||||
ofstream_.open(temp_path_, std::ios::binary);
|
||||
|
||||
if (ofstream_.fail()) {
|
||||
LOG_ERRO("Failed to open the temp file: %s", temp_path_.string().c_str());
|
||||
throw Error{ Error::kFileError };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileBodyHandler::AddContent(const char* data, std::size_t count) {
|
||||
@@ -136,12 +138,17 @@ bool Parser::Parse(const char* data, std::size_t length) {
|
||||
|
||||
LOG_INFO("HTTP headers just ended.");
|
||||
|
||||
if (!OnHeadersEnd()) {
|
||||
// Only request parser can reach here when no view matches the request.
|
||||
// Data streaming or not is also determined for request parser.
|
||||
return false;
|
||||
}
|
||||
|
||||
CreateBodyHandler();
|
||||
|
||||
if (!body_handler_) {
|
||||
// The only reason to reach here is that it was failed to generate the temp
|
||||
// file for streaming. Normally, it shouldn't happen.
|
||||
// TODO: Keep a member |error_| for the user to query.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -152,6 +159,7 @@ bool Parser::Parse(const char* data, std::size_t length) {
|
||||
void Parser::Reset() {
|
||||
message_ = nullptr;
|
||||
body_handler_.reset();
|
||||
stream_ = false;
|
||||
|
||||
pending_data_.clear();
|
||||
|
||||
@@ -202,6 +210,20 @@ bool Parser::ParseHeaders() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Parser::CreateBodyHandler() {
|
||||
if (stream_) {
|
||||
auto file_body_handler = new FileBodyHandler{ message_ };
|
||||
if (!file_body_handler->OpenFile()) {
|
||||
body_handler_.reset();
|
||||
delete file_body_handler;
|
||||
} else {
|
||||
body_handler_.reset(file_body_handler);
|
||||
}
|
||||
} else {
|
||||
body_handler_.reset(new StringBodyHandler{ message_ });
|
||||
}
|
||||
}
|
||||
|
||||
bool Parser::GetNextLine(std::size_t off, std::string* line, bool erase) {
|
||||
std::size_t pos = pending_data_.find(kCRLF, off);
|
||||
|
||||
|
||||
+14
-2
@@ -63,10 +63,14 @@ private:
|
||||
class FileBodyHandler : public BodyHandler {
|
||||
public:
|
||||
// NOTE: Might throw Error::kFileError.
|
||||
explicit FileBodyHandler(Message* message);
|
||||
explicit FileBodyHandler(Message* message) : BodyHandler(message) {
|
||||
}
|
||||
|
||||
~FileBodyHandler() override = default;
|
||||
|
||||
// Open a temp file for data streaming.
|
||||
bool OpenFile();
|
||||
|
||||
void AddContent(const char* data, std::size_t count) override;
|
||||
void AddContent(const std::string& data) override;
|
||||
|
||||
@@ -108,7 +112,11 @@ protected:
|
||||
// Return false only on syntax errors.
|
||||
bool ParseHeaders();
|
||||
|
||||
virtual void CreateBodyHandler() = 0;
|
||||
// Called when headers just parsed.
|
||||
// Return false if something is wrong.
|
||||
virtual bool OnHeadersEnd() = 0;
|
||||
|
||||
void CreateBodyHandler();
|
||||
|
||||
// Get next line (using delimiter CRLF) from the pending data.
|
||||
// The line will not contain a trailing CRLF.
|
||||
@@ -134,8 +142,12 @@ protected:
|
||||
|
||||
protected:
|
||||
Message* message_;
|
||||
|
||||
std::unique_ptr<BodyHandler> body_handler_;
|
||||
|
||||
// Data streaming or not.
|
||||
bool stream_;
|
||||
|
||||
// Data waiting to be parsed.
|
||||
std::string pending_data_;
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ public:
|
||||
}
|
||||
|
||||
// Use the file content as body.
|
||||
// NOTE: Error::kFileError might be thrown.
|
||||
RequestBuilder& File(const Path& path, bool infer_media_type = true,
|
||||
std::size_t chunk_size = 1024);
|
||||
|
||||
|
||||
+15
-4
@@ -13,14 +13,25 @@ namespace webcc {
|
||||
RequestParser::RequestParser() : request_(nullptr) {
|
||||
}
|
||||
|
||||
void RequestParser::Init(Request* request) {
|
||||
void RequestParser::Init(Request* request, ViewMatcher view_matcher) {
|
||||
assert(view_matcher);
|
||||
|
||||
Parser::Init(request);
|
||||
|
||||
request_ = request;
|
||||
view_matcher_ = view_matcher;
|
||||
}
|
||||
|
||||
// TODO
|
||||
void RequestParser::CreateBodyHandler() {
|
||||
body_handler_.reset(new StringBodyHandler{ message_ });
|
||||
bool RequestParser::OnHeadersEnd() {
|
||||
bool matched = view_matcher_(request_->method(), request_->url().path(),
|
||||
&stream_);
|
||||
|
||||
if (!matched) {
|
||||
LOG_WARN("No view matches the request: %s %s", request_->method().c_str(),
|
||||
request_->url().path().c_str());
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
bool RequestParser::ParseStartLine(const std::string& line) {
|
||||
|
||||
+14
-2
@@ -1,12 +1,16 @@
|
||||
#ifndef WEBCC_REQUEST_PARSER_H_
|
||||
#define WEBCC_REQUEST_PARSER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "webcc/parser.h"
|
||||
|
||||
namespace webcc {
|
||||
|
||||
using ViewMatcher =
|
||||
std::function<bool(const std::string&, const std::string&, bool*)>;
|
||||
|
||||
class Request;
|
||||
|
||||
class RequestParser : public Parser {
|
||||
@@ -15,10 +19,14 @@ public:
|
||||
|
||||
~RequestParser() override = default;
|
||||
|
||||
void Init(Request* request);
|
||||
void Init(Request* request, ViewMatcher view_matcher);
|
||||
|
||||
private:
|
||||
void CreateBodyHandler() override;
|
||||
// Override to match the URL against views and check if the matched view
|
||||
// asks for data streaming.
|
||||
bool OnHeadersEnd() override;
|
||||
|
||||
bool Stream() const;
|
||||
|
||||
bool ParseStartLine(const std::string& line) override;
|
||||
|
||||
@@ -39,6 +47,10 @@ private:
|
||||
private:
|
||||
Request* request_;
|
||||
|
||||
// A function for matching view once the headers of a request has been
|
||||
// received. The parsing will stop and fail if no view can be matched.
|
||||
ViewMatcher view_matcher_;
|
||||
|
||||
// Form data parsing step.
|
||||
enum Step {
|
||||
kStart,
|
||||
|
||||
@@ -45,18 +45,6 @@ void ResponseParser::Init(Response* response, bool stream) {
|
||||
stream_ = stream;
|
||||
}
|
||||
|
||||
void ResponseParser::CreateBodyHandler() {
|
||||
if (stream_) {
|
||||
try {
|
||||
body_handler_.reset(new FileBodyHandler{ message_ });
|
||||
} catch (const Error&) {
|
||||
body_handler_.reset();
|
||||
}
|
||||
} else {
|
||||
body_handler_.reset(new StringBodyHandler{ message_ });
|
||||
}
|
||||
}
|
||||
|
||||
bool ResponseParser::ParseStartLine(const std::string& line) {
|
||||
std::vector<std::string> parts;
|
||||
SplitStartLine(line, &parts);
|
||||
|
||||
@@ -21,7 +21,9 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
void CreateBodyHandler() override;
|
||||
bool OnHeadersEnd() override {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse HTTP start line; E.g., "HTTP/1.1 200 OK".
|
||||
bool ParseStartLine(const std::string& line) override;
|
||||
@@ -33,9 +35,6 @@ private:
|
||||
// The result response message.
|
||||
Response* response_ = nullptr;
|
||||
|
||||
// Data streaming or not.
|
||||
bool stream_ = false;
|
||||
|
||||
// The response for HEAD request could also have `Content-Length` header,
|
||||
// set this flag to ignore it.
|
||||
bool ignroe_body_ = false;
|
||||
|
||||
+66
-22
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "boost/algorithm/string.hpp"
|
||||
#include "boost/filesystem/fstream.hpp"
|
||||
#include "boost/filesystem/operations.hpp"
|
||||
|
||||
#include "webcc/body.h"
|
||||
#include "webcc/logger.h"
|
||||
@@ -193,8 +194,11 @@ void Server::AsyncAccept() {
|
||||
if (!ec) {
|
||||
LOG_INFO("Accepted a connection.");
|
||||
|
||||
using namespace std::placeholders;
|
||||
auto view_matcher = std::bind(&Server::MatchView, this, _1, _2, _3);
|
||||
|
||||
auto connection = std::make_shared<Connection>(
|
||||
std::move(socket), &pool_, &queue_);
|
||||
std::move(socket), &pool_, &queue_, std::move(view_matcher));
|
||||
|
||||
pool_.Start(connection);
|
||||
}
|
||||
@@ -275,17 +279,28 @@ void Server::Handle(ConnectionPtr connection) {
|
||||
auto request = connection->request();
|
||||
|
||||
const Url& url = request->url();
|
||||
UrlArgs args;
|
||||
|
||||
LOG_INFO("Request URL path: %s", url.path().c_str());
|
||||
|
||||
UrlArgs args;
|
||||
auto view = FindView(request->method(), url.path(), &args);
|
||||
|
||||
if (!view) {
|
||||
LOG_WARN("No view matches the URL path: %s", url.path().c_str());
|
||||
if (!ServeStatic(connection)) {
|
||||
LOG_WARN("No view matches the request: %s %s", request->method().c_str(),
|
||||
url.path().c_str());
|
||||
|
||||
if (request->method() == methods::kGet) {
|
||||
// Try to serve static files for GET request.
|
||||
auto response = ServeStatic(request);
|
||||
if (!response) {
|
||||
// Static file not found.
|
||||
connection->SendResponse(Status::kNotFound);
|
||||
} else {
|
||||
connection->SendResponse(response);
|
||||
}
|
||||
} else {
|
||||
connection->SendResponse(Status::kBadRequest);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,7 +314,7 @@ void Server::Handle(ConnectionPtr connection) {
|
||||
if (response) {
|
||||
connection->SendResponse(response);
|
||||
} else {
|
||||
connection->SendResponse(Status::kNotImplemented);
|
||||
connection->SendResponse(Status::kBadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,41 +350,70 @@ ViewPtr Server::FindView(const std::string& method, const std::string& url,
|
||||
return ViewPtr();
|
||||
}
|
||||
|
||||
bool Server::ServeStatic(ConnectionPtr connection) {
|
||||
if (doc_root_.empty()) {
|
||||
LOG_INFO("The doc root was not specified.");
|
||||
bool Server::MatchView(const std::string& method, const std::string& url,
|
||||
bool* stream) {
|
||||
assert(stream != nullptr);
|
||||
*stream = false;
|
||||
|
||||
for (auto& route : routes_) {
|
||||
if (std::find(route.methods.begin(), route.methods.end(), method) ==
|
||||
route.methods.end()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route.url.empty()) {
|
||||
std::smatch match;
|
||||
|
||||
if (std::regex_match(url, match, route.url_regex)) {
|
||||
*stream = route.view->Stream(method);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (boost::iequals(route.url, url)) {
|
||||
*stream = route.view->Stream(method);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match a static file.
|
||||
if (method == methods::kGet && !doc_root_.empty()) {
|
||||
Path path = doc_root_ / url;
|
||||
if (!bfs::is_directory(path) && bfs::exists(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto request = connection->request();
|
||||
std::string path = request->url().path();
|
||||
ResponsePtr Server::ServeStatic(RequestPtr request) {
|
||||
assert(request->method() == methods::kGet);
|
||||
|
||||
// If path ends in slash (i.e. is a directory) then add "index.html".
|
||||
if (path[path.size() - 1] == '/') {
|
||||
path += "index.html"; // TODO
|
||||
if (doc_root_.empty()) {
|
||||
LOG_INFO("The doc root was not specified.");
|
||||
return {};
|
||||
}
|
||||
|
||||
Path p = doc_root_ / path;
|
||||
Path path = doc_root_ / request->url().path();
|
||||
|
||||
try {
|
||||
auto body = std::make_shared<FileBody>(p, file_chunk_size_);
|
||||
// NOTE: FileBody might throw Error::kFileError.
|
||||
auto body = std::make_shared<FileBody>(path, file_chunk_size_);
|
||||
|
||||
auto response = std::make_shared<Response>(Status::kOK);
|
||||
|
||||
std::string extension = p.extension().string();
|
||||
std::string extension = path.extension().string();
|
||||
response->SetContentType(media_types::FromExtension(extension), "");
|
||||
|
||||
// NOTE: Gzip compression is not supported.
|
||||
response->SetBody(body, true);
|
||||
|
||||
// Send response back to client.
|
||||
connection->SendResponse(response);
|
||||
|
||||
return true;
|
||||
return response;
|
||||
|
||||
} catch (const Error& error) {
|
||||
LOG_ERRO("File error: %s.", error.message().c_str());
|
||||
return false;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -93,12 +93,18 @@ private:
|
||||
// request comes, this connection will be put back to the queue again.
|
||||
virtual void Handle(ConnectionPtr connection);
|
||||
|
||||
// Find the view by HTTP method and URL.
|
||||
// Find the view by HTTP method and URL (path).
|
||||
ViewPtr FindView(const std::string& method, const std::string& url,
|
||||
UrlArgs* args);
|
||||
|
||||
// Match the view by HTTP method and URL (path).
|
||||
// Return if a view or static file is matched or not.
|
||||
// If the view asks for data streaming, |stream| will be set to true.
|
||||
bool MatchView(const std::string& method, const std::string& url,
|
||||
bool* stream);
|
||||
|
||||
// Serve static files from the doc root.
|
||||
bool ServeStatic(ConnectionPtr connection);
|
||||
ResponsePtr ServeStatic(RequestPtr request);
|
||||
|
||||
private:
|
||||
struct RouteInfo {
|
||||
|
||||
@@ -13,6 +13,13 @@ public:
|
||||
virtual ~View() = default;
|
||||
|
||||
virtual ResponsePtr Handle(RequestPtr request) = 0;
|
||||
|
||||
// Return true if you want the request data of the given method to be streamed
|
||||
// to a temp file. Data streaming is useful for receiving large data, e.g.,
|
||||
// a JPEG image, posted from the client.
|
||||
virtual bool Stream(const std::string& /*method*/) {
|
||||
return false; // No streaming by default
|
||||
}
|
||||
};
|
||||
|
||||
using ViewPtr = std::shared_ptr<View>;
|
||||
|
||||
Reference in New Issue
Block a user