diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b8065e2..09169fb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,4 +14,6 @@ if(WEBCC_ENABLE_DEMO) add_subdirectory(demo/soap/calc_client) add_subdirectory(demo/soap/calc_server) endif() + add_subdirectory(demo/rest/book_client) + add_subdirectory(demo/rest/book_server) endif() \ No newline at end of file diff --git a/src/demo/rest/book_client/CMakeLists.txt b/src/demo/rest/book_client/CMakeLists.txt new file mode 100644 index 0000000..a435983 --- /dev/null +++ b/src/demo/rest/book_client/CMakeLists.txt @@ -0,0 +1,5 @@ +file(GLOB SRCS *.cc *.h) + +add_executable(rest_book_client ${SRCS}) + +target_link_libraries(rest_book_client webcc) diff --git a/src/demo/rest/book_client/book_client.cc b/src/demo/rest/book_client/book_client.cc new file mode 100644 index 0000000..f46eb3c --- /dev/null +++ b/src/demo/rest/book_client/book_client.cc @@ -0,0 +1,2 @@ +#include "book_client.h" + diff --git a/src/demo/rest/book_client/book_client.h b/src/demo/rest/book_client/book_client.h new file mode 100644 index 0000000..bcba55b --- /dev/null +++ b/src/demo/rest/book_client/book_client.h @@ -0,0 +1,6 @@ +#ifndef BOOK_CLIENT_H_ +#define BOOK_CLIENT_H_ + +#include + +#endif // BOOK_CLIENT_H_ diff --git a/src/demo/rest/book_client/main.cc b/src/demo/rest/book_client/main.cc new file mode 100644 index 0000000..c52b9f6 --- /dev/null +++ b/src/demo/rest/book_client/main.cc @@ -0,0 +1,88 @@ +#include +#include "boost/lexical_cast.hpp" + +#include "webcc/http_client.h" +#include "webcc/http_request.h" +#include "webcc/http_response.h" + +class BookListClient { +public: + BookListClient() { + host_ = "localhost"; + port_ = "8080"; + } + + bool ListBooks() { + webcc::HttpRequest http_request; + + http_request.set_method(webcc::kHttpGet); + http_request.set_url("/books"); + http_request.SetHost(host_, port_); + + http_request.Build(); + + webcc::HttpResponse http_response; + + webcc::HttpClient http_client; + webcc::Error error = http_client.SendRequest(http_request, &http_response); + + if (error != webcc::kNoError) { + return false; + } + + std::cout << "Book list: " << std::endl + << http_response.content() << std::endl; + + return true; + } + +private: + std::string host_; + std::string port_; +}; + +class BookDetailClient { +public: + BookDetailClient() { + host_ = "localhost"; + port_ = "8080"; + } + + bool GetBook(const std::string& id) { + webcc::HttpRequest http_request; + + http_request.set_method(webcc::kHttpGet); + http_request.set_url("/books/" + id); + http_request.SetHost(host_, port_); + + http_request.Build(); + + webcc::HttpResponse http_response; + + webcc::HttpClient http_client; + webcc::Error error = http_client.SendRequest(http_request, &http_response); + + if (error != webcc::kNoError) { + return false; + } + + std::cout << "Book: " << id << std::endl + << http_response.content() << std::endl; + + return true; + } + +private: + std::string host_; + std::string port_; +}; + +int main() { + BookListClient book_list_client; + book_list_client.ListBooks(); + + BookDetailClient book_detail_client; + book_detail_client.GetBook("1"); + + return 0; +} diff --git a/src/demo/rest/book_server/CMakeLists.txt b/src/demo/rest/book_server/CMakeLists.txt new file mode 100644 index 0000000..457a575 --- /dev/null +++ b/src/demo/rest/book_server/CMakeLists.txt @@ -0,0 +1,5 @@ +file(GLOB SRCS *.cc *.h) + +add_executable(rest_book_server ${SRCS}) + +target_link_libraries(rest_book_server webcc) diff --git a/src/demo/rest/book_server/book_services.cc b/src/demo/rest/book_server/book_services.cc new file mode 100644 index 0000000..85c90e2 --- /dev/null +++ b/src/demo/rest/book_server/book_services.cc @@ -0,0 +1,138 @@ +#include "book_services.h" + +#include +#include "boost/lexical_cast.hpp" + +//////////////////////////////////////////////////////////////////////////////// + +class Book { +public: + std::string id; + std::string title; + double price; + + bool IsNull() const { + return id.empty(); + } +}; + +static const Book kNullBook{}; + +class BookStore { +public: + BookStore() { + books_.push_back({ "1", "Title1", 11.1 }); + books_.push_back({ "2", "Title2", 22.2 }); + books_.push_back({ "3", "Title3", 33.3 }); + } + + const std::list& books() const { + return books_; + } + + const Book& GetBook(const std::string& id) const { + auto it = std::find_if(books_.begin(), + books_.end(), + [&id](const Book& book) { return book.id == id; }); + + if (it == books_.end()) { + return kNullBook; + } + + return *it; + } + + bool AddBook(const Book& new_book) { + auto it = std::find_if( + books_.begin(), + books_.end(), + [&new_book](const Book& book) { return book.id == new_book.id; }); + + if (it != books_.end()) { + return false; + } + + books_.push_back(new_book); + return true; + } + + bool DeleteBook(const std::string& id) { + auto it = std::find_if(books_.begin(), + books_.end(), + [&id](const Book& book) { return book.id == id; }); + + if (it == books_.end()) { + return false; + } + + books_.erase(it); + return true; + } + +private: + std::list books_; +}; + +static BookStore g_book_store; + +//////////////////////////////////////////////////////////////////////////////// + +// Naively create JSON object string for a book. +// You should use real JSON library in your product code. +static std::string CreateBookJson(const Book& book) { + std::string json = "{ "; + json += "\"id\": " + book.id + ", "; + json += "\"title\": " + book.title + ", "; + json += "\"price\": " + std::to_string(book.price); + json += " }"; + return json; +} + +bool BookListService::Handle(const std::string& http_method, + const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) { + if (http_method == webcc::kHttpGet) { + *response_content = "{ "; + for (const Book& book : g_book_store.books()) { + *response_content += CreateBookJson(book); + *response_content += ","; + } + *response_content += " }"; + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// + +bool BookDetailService::Handle(const std::string& http_method, + const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) { + if (url_sub_matches.size() != 1) { + return false; + } + + const std::string& book_id = url_sub_matches[0]; + + if (http_method == webcc::kHttpGet) { + const Book& book = g_book_store.GetBook(book_id); + + if (book.IsNull()) { + return false; + } + + *response_content = CreateBookJson(book); + + return true; + + } else if (http_method == webcc::kHttpPost) { + + } else if (http_method == webcc::kHttpDelete) { + + } + + return false; +} diff --git a/src/demo/rest/book_server/book_services.h b/src/demo/rest/book_server/book_services.h new file mode 100644 index 0000000..d9bbb13 --- /dev/null +++ b/src/demo/rest/book_server/book_services.h @@ -0,0 +1,44 @@ +#ifndef BOOK_SERVICES_H_ +#define BOOK_SERVICES_H_ + +#include "webcc/rest_service.h" + +// NOTE: +// XxxListService and XxxDetailService are similar to the XxxListView +// and XxxDetailView in Django (a Python web framework). + +// List Service handles the HTTP GET and returns the book list based on +// query parameters specified in the URL. +// The URL should be like: +// - /books +// - /books?name={BookName} +// The query parameters could be regular expressions. +class BookListService : public webcc::RestService { +public: + BookListService() = default; + ~BookListService() override = default; + + bool Handle(const std::string& http_method, + const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) override; +}; + +// Detail Service handles the following HTTP methods: +// - GET +// - PUT +// - PATCH +// - DELETE +// The URL should be like: /books/{BookID}. +class BookDetailService : public webcc::RestService { +public: + BookDetailService() = default; + ~BookDetailService() override = default; + + bool Handle(const std::string& http_method, + const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) override; +}; + +#endif // BOOK_SERVICE_H_ diff --git a/src/demo/rest/book_server/main.cc b/src/demo/rest/book_server/main.cc new file mode 100644 index 0000000..937f1f7 --- /dev/null +++ b/src/demo/rest/book_server/main.cc @@ -0,0 +1,38 @@ +#include +#include "webcc/rest_server.h" +#include "book_services.h" + +static void Help(const char* argv0) { + std::cout << "Usage: " << argv0 << " " << std::endl; + std::cout << " E.g.," << std::endl; + std::cout << " " << argv0 << " 8080" << std::endl; +} + +int main(int argc, char* argv[]) { + if (argc != 2) { + Help(argv[0]); + return 1; + } + + unsigned short port = std::atoi(argv[1]); + + std::size_t workers = 2; + + try { + webcc::RestServer server(port, workers); + + server.RegisterService(std::make_shared(), + "/books"); + + server.RegisterService(std::make_shared(), + "/books/(\\d+)"); + + server.Run(); + + } catch (std::exception& e) { + std::cerr << "Exception: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/src/webcc/http_message.h b/src/webcc/http_message.h index 1b2ee7e..5056ee5 100644 --- a/src/webcc/http_message.h +++ b/src/webcc/http_message.h @@ -28,7 +28,7 @@ public: start_line_ = start_line; } - size_t content_length() const { + std::size_t content_length() const { return content_length_; } @@ -43,7 +43,7 @@ public: SetHeader(kContentType, content_type); } - void SetContentLength(size_t content_length) { + void SetContentLength(std::size_t content_length) { content_length_ = content_length; SetHeader(kContentLength, std::to_string(content_length)); } @@ -53,7 +53,7 @@ public: content_ = std::move(content); } - void AppendContent(const char* data, size_t count) { + void AppendContent(const char* data, std::size_t count) { content_.append(data, count); } @@ -62,8 +62,7 @@ public: } bool IsContentFull() const { - assert(IsContentLengthValid()); - return content_.length() >= content_length_; + return IsContentLengthValid() && content_.length() >= content_length_; } bool IsContentLengthValid() const { diff --git a/src/webcc/http_parser.cc b/src/webcc/http_parser.cc index 53268e9..8e62c0a 100644 --- a/src/webcc/http_parser.cc +++ b/src/webcc/http_parser.cc @@ -10,11 +10,12 @@ namespace webcc { HttpParser::HttpParser(HttpMessage* message) : message_(message) , start_line_parsed_(false) + , content_length_parsed_(false) , header_parsed_(false) , finished_(false) { } -Error HttpParser::Parse(const char* data, size_t len) { +Error HttpParser::Parse(const char* data, std::size_t len) { if (header_parsed_) { // Add the data to the content. message_->AppendContent(data, len); @@ -28,10 +29,10 @@ Error HttpParser::Parse(const char* data, size_t len) { } pending_data_.append(data, len); - size_t off = 0; + std::size_t off = 0; while (true) { - size_t pos = pending_data_.find("\r\n", off); + std::size_t pos = pending_data_.find("\r\n", off); if (pos == std::string::npos) { break; } @@ -52,8 +53,8 @@ Error HttpParser::Parse(const char* data, size_t len) { } } else { // Currently, only Content-Length is important to us. - // Other fields are ignored. - if (!message_->IsContentLengthValid()) { + // Other header fields are ignored. + if (!content_length_parsed_) { ParseContentLength(line); } } @@ -64,9 +65,16 @@ Error HttpParser::Parse(const char* data, size_t len) { if (header_parsed_) { // Headers just ended. - if (!message_->IsContentLengthValid()) { - // No Content-Length? - return kHttpContentLengthError; + if (!content_length_parsed_) { + // No Content-Length, no content. + message_->SetContentLength(0); + finished_ = true; + return kNoError; + } else { + // Invalid Content-Length in the request. + if (!message_->IsContentLengthValid()) { + return kHttpContentLengthError; + } } message_->AppendContent(pending_data_.substr(off)); @@ -84,7 +92,7 @@ Error HttpParser::Parse(const char* data, size_t len) { } void HttpParser::ParseContentLength(const std::string& line) { - size_t pos = line.find(':'); + std::size_t pos = line.find(':'); if (pos == std::string::npos) { return; } @@ -92,6 +100,8 @@ void HttpParser::ParseContentLength(const std::string& line) { std::string name = line.substr(0, pos); if (boost::iequals(name, kContentLength)) { + content_length_parsed_ = true; + ++pos; // Skip ':'. while (line[pos] == ' ') { // Skip spaces. ++pos; @@ -100,9 +110,9 @@ void HttpParser::ParseContentLength(const std::string& line) { std::string value = line.substr(pos); try { - message_->SetContentLength(boost::lexical_cast(value)); + std::size_t length = boost::lexical_cast(value); + message_->SetContentLength(length); } catch (boost::bad_lexical_cast&) { - // TODO } } } diff --git a/src/webcc/http_parser.h b/src/webcc/http_parser.h index c50529d..9a846c2 100644 --- a/src/webcc/http_parser.h +++ b/src/webcc/http_parser.h @@ -17,7 +17,7 @@ public: return finished_; } - Error Parse(const char* data, size_t len); + Error Parse(const char* data, std::size_t len); protected: // Parse HTTP start line. @@ -36,6 +36,7 @@ protected: // Parsing helper flags. bool start_line_parsed_; + bool content_length_parsed_; bool header_parsed_; bool finished_; }; diff --git a/src/webcc/http_request.cc b/src/webcc/http_request.cc index 005c9af..75d5097 100644 --- a/src/webcc/http_request.cc +++ b/src/webcc/http_request.cc @@ -29,7 +29,7 @@ void HttpRequest::SetHost(const std::string& host, const std::string& port) { } } -void HttpRequest::MakeStartLine() { +void HttpRequest::Build() { if (start_line_.empty()) { start_line_ = method_; start_line_ += " "; @@ -48,7 +48,6 @@ const char CRLF[] = { '\r', '\n' }; // ATTENTION: The buffers don't hold the memory! std::vector HttpRequest::ToBuffers() const { assert(!start_line_.empty()); - assert(IsContentLengthValid()); std::vector buffers; @@ -63,7 +62,9 @@ std::vector HttpRequest::ToBuffers() const { buffers.push_back(boost::asio::buffer(misc_strings::CRLF)); - buffers.push_back(boost::asio::buffer(content_)); + if (content_length_ > 0) { + buffers.push_back(boost::asio::buffer(content_)); + } return buffers; } diff --git a/src/webcc/http_request.h b/src/webcc/http_request.h index 22025e7..f7232a5 100644 --- a/src/webcc/http_request.h +++ b/src/webcc/http_request.h @@ -51,13 +51,13 @@ public: // \param port Numeric port number, "80" will be used if it's empty. void SetHost(const std::string& host, const std::string& port); - // Compose start line from method, url, etc. - void MakeStartLine(); + // 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. - // NOTE: Please call MakeStartLine() before. std::vector ToBuffers() const; private: diff --git a/src/webcc/rest_server.cc b/src/webcc/rest_server.cc index c0b454c..b8e76e5 100644 --- a/src/webcc/rest_server.cc +++ b/src/webcc/rest_server.cc @@ -87,6 +87,7 @@ HttpStatus::Enum RestRequestHandler::HandleSession(HttpSessionPtr session) { // TODO: Error handling. std::string content; service->Handle(session->request().method(), + sub_matches, session->request().content(), &content); diff --git a/src/webcc/rest_service.h b/src/webcc/rest_service.h index 0c32dad..0a69bfc 100644 --- a/src/webcc/rest_service.h +++ b/src/webcc/rest_service.h @@ -1,8 +1,9 @@ #ifndef WEBCC_REST_SERVICE_H_ #define WEBCC_REST_SERVICE_H_ -#include #include +#include +#include #include "webcc/common.h" @@ -18,8 +19,9 @@ public: // Both the request and response parameters should be JSON. // TODO: Query parameters. virtual bool Handle(const std::string& http_method, - const std::string& request, - std::string* response) = 0; + const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) = 0; }; typedef std::shared_ptr RestServicePtr;