From 33f2b84f8c32e58e28ca381e7756f03b2ad01b62 Mon Sep 17 00:00:00 2001 From: Adam Gu Date: Mon, 23 Jul 2018 15:40:58 +0800 Subject: [PATCH] Replace boost thread with std thread; update rest book example; move soap tutorials to wiki. --- README.md | 2 + doc/SoapClientTutorial.md | 101 -------------- doc/SoapClientTutorial_zh-CN.md | 153 ---------------------- doc/SoapServerTutorial.md | 137 ------------------- example/rest_book_client/main.cc | 52 +++++--- example/rest_book_server/services.cc | 26 ++-- example/soap_calc_client_parasoft/main.cc | 3 +- example/soap_calc_server/calc_service.cc | 19 +-- example/soap_calc_server/calc_service.h | 4 - webcc/http_request_handler.cc | 8 +- webcc/http_request_handler.h | 5 +- 11 files changed, 64 insertions(+), 446 deletions(-) delete mode 100644 doc/SoapClientTutorial.md delete mode 100644 doc/SoapClientTutorial_zh-CN.md delete mode 100644 doc/SoapServerTutorial.md diff --git a/README.md b/README.md index 4abe4fc..6cc8cd7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A lightweight C++ REST and SOAP client and server library based on *Boost.Asio*. +Please turn to our [Wiki](https://github.com/sprinfall/webcc/wiki) for more tutorials and guides. + ## Quick Start ### REST Server diff --git a/doc/SoapClientTutorial.md b/doc/SoapClientTutorial.md deleted file mode 100644 index 71c47e8..0000000 --- a/doc/SoapClientTutorial.md +++ /dev/null @@ -1,101 +0,0 @@ -# SOAP Client Tutorial - -Firstly, please install **SoapUI** if you don't have it. We need SoapUI to generate sample requests for each web service operation. The open source version is good enough. - -Take the calculator web service provided by ParaSoft as an example. Download the WSDL from http://ws1.parasoft.com/glue/calculator.wsdl, create a SOAP project within SoapUI (remember to check "**Create sample requests for all operations?**"), you will see the sample request for "add" operation as the following: -```xml - - - - - 1 - 2 - - - -``` - -In order to call the "add" operation, we have to send a HTTP request with the above SOAP envelope as the content. Let's see how to do this with *webcc*. - -Firstly, create a class `CalcClient` which is derived from `webcc::SoapClient`: - -```cpp -#include -#include "webcc/soap_client.h" - -class CalcClient : public webcc::SoapClient { -public: - CalcClient() { - Init(); - } -``` - -Initialize the URL, host, port, etc. in `Init()`: -```cpp -private: - void Init() { - url_ = "/glue/calculator"; - host_ = "ws1.parasoft.com"; - port_ = ""; // Default to "80". - service_ns_ = { "cal", "http://www.parasoft.com/wsdl/calculator/" }; - result_name_ = "Result"; - } -``` - -Because four calculator operations (*add*, *subtract*, *multiply* and *divide*) all have two parameters, we create a wrapper for `SoapClient::Call()`, name is as `Calc`: -```cpp -bool Calc(const std::string& operation, - const std::string& x_name, - const std::string& y_name, - double x, - double y, - double* result) { - // Prepare parameters. - std::vector parameters{ - { x_name, x }, - { y_name, y } - }; - - // Make the call. - std::string result_str; - webcc::Error error = Call(operation, std::move(parameters), &result_str); - - // Error handling if any. - if (error != webcc::kNoError) { - std::cerr << "Error: " << error; - std::cerr << ", " << webcc::GetErrorMessage(error) << std::endl; - return false; - } - - // Convert the result from string to double. - try { - *result = boost::lexical_cast(result_str); - } catch (boost::bad_lexical_cast&) { - return false; - } - - return true; -} -``` - -Note that the local parameters are moved (with `std::move()`). This is to avoid expensive copy of long string parameters, e.g., XML strings. - -Finally, we implement the four operations simply as the following: -```cpp -bool Add(double x, double y, double* result) { - return Calc("add", "x", "y", x, y, result); -} - -bool Subtract(double x, double y, double* result) { - return Calc("subtract", "x", "y", x, y, result); -} - -bool Multiply(double x, double y, double* result) { - return Calc("multiply", "x", "y", x, y, result); -} - -bool Divide(double x, double y, double* result) { - return Calc("divide", "numerator", "denominator", x, y, result); -} -``` -See? It's not that complicated. Check folder ***demo/calculator_client*** for the full example. diff --git a/doc/SoapClientTutorial_zh-CN.md b/doc/SoapClientTutorial_zh-CN.md deleted file mode 100644 index 979fdf1..0000000 --- a/doc/SoapClientTutorial_zh-CN.md +++ /dev/null @@ -1,153 +0,0 @@ -# SOAP Client Tutorial (zh-CN) - -## 背景 - -首先,[gSoap](http://www.cs.fsu.edu/~engelen/soap.html) 肯定是个不错的选择,但是如果你的程序要调用多个 Web Services(即有多个 WSDL),gSoap 会比较麻烦。还有一个问题就是,gSoap 从 WSDL 自动生成的代码实在是太难用了。当然,这些都不是什么问题,真在的问题是许可证(License),gSoap 用在商业产品中是要收费的。 - -公司比较穷,舍不得花钱买 gSoap,但是 C++ 调 Web Service 还真没什么好办法。尝试了五六个半死不活的库后,最终锁定了 [WWSAPI](https://msdn.microsoft.com/en-us/library/windows/desktop/dd430435%28v=vs.85%29.aspx)(Windows Web Services API)。 - -WWSAPI 的官方文档经常让人摸不着头脑,没有完整的示例,给出一段代码,常常需要几经调整才能使用。WWSAPI 自动生成的代码,是纯 C 的接口,在难用程度上,较 gSoap 有过之而无不及。在消息参数上,它强制使用双字节 Unicode,我们的输入输出都是 UTF8 的 `std::string`,于是莫名地多出很多编码转换。WWSAPI 需要你手动分配堆(heap),需要你指定消息的缓冲大小,而最严重的问题是,它不够稳定,特别是在子线程里调用时,莫名其妙连接就会断掉。 - -于是,我就动手自己写了个 [webcc](https://github.com/sprinfall/webcc)。 -一开始 webcc 只支持 SOAP,名字就叫 csoap,后来支持了 REST,于是改名为 webcc,取 Web C++ 的意思。 - -## 原理 - -Webcc 没有提供从 WSDL 自动生成代码的功能,一来是因为这一过程太复杂了,二来是自动生成的代码一般都不好用。所以 webcc 最好搭配 [SoapUI](https://www.soapui.org) 一起使用。SoapUI 可以帮助我们为每一个 Web Service 操作(operation)生成请求的样例,基于请求样例,就很容易发起调用了,也避免了直接阅读 WSDL。 - -下面以 ParaSoft 提供的 [Calculator](http://ws1.parasoft.com/glue/calculator.wsdl) 为例,首先下载 WSDL,然后在 SoapUI 里创建一个 SOAP 项目,记得勾上 "Create sample requests for all operations?" 这个选项,然后就能看到下面这样的请求样例了: -```xml - - - - - 1 - 2 - - - -``` -这个操作是 "add",有两个参数:x 和 y。此外值得注意的还有 XML namespace,比如 `xmlns:cal="http://www.parasoft.com/wsdl/calculator/"`。 - -要调用这个 "add" 操作,只要发一个 HTTP 请求,并把上面这个 SOAP Envelope 作为请求的 Content。在 SoapUI 里把 Request 切换到 “Raw" 模式,就可以看到下面这样完整的 HTTP 请求: -``` -POST http://ws1.parasoft.com/glue/calculator HTTP/1.1 -Accept-Encoding: gzip,deflate -Content-Type: text/xml;charset=UTF-8 -SOAPAction: "add" -Content-Length: 300 -Host: ws1.parasoft.com -Connection: Keep-Alive -User-Agent: Apache-HttpClient/4.1.1 (java 1.5) - - - - - - 1 - 1 - - - -``` -所以 webcc 所做的,只不过是跟 `ws1.parasoft.com` 建立 TCP Socket 连接,然后发送上面这段内容而已。 - -## 用法 - -首先,创建一个类 `CalcClient`,继承自 `webcc::SoapClient`: - -```cpp -#include -#include "webcc/soap_client.h" - -class CalcClient : public webcc::SoapClient { -public: - CalcClient() { - Init(); - } -``` - -在 `Init()` 函数里,初始化 URL、host、port 等等: -```cpp -private: - void Init() { - url_ = "/glue/calculator"; - host_ = "ws1.parasoft.com"; - port_ = ""; // Default to "80". - service_ns_ = { "cal", "http://www.parasoft.com/wsdl/calculator/" }; - result_name_ = "Result"; - } -``` - -由于四个计算器操作(*add*, *subtract*, *multiply* 和 *divide*)都一致的具有两个参数,我们可以稍微封装一下,弄一个辅助函数叫 `Calc`: -```cpp -bool Calc(const std::string& operation, - const std::string& x_name, - const std::string& y_name, - double x, - double y, - double* result) { - // Prepare parameters. - std::vector parameters{ - { x_name, x }, - { y_name, y } - }; - - // Make the call. - std::string result_str; - webcc::Error error = Call(operation, std::move(parameters), &result_str); - - // Error handling if any. - if (error != webcc::kNoError) { - std::cerr << "Error: " << error; - std::cerr << ", " << webcc::GetErrorMessage(error) << std::endl; - return false; - } - - // Convert the result from string to double. - try { - *result = boost::lexical_cast(result_str); - } catch (boost::bad_lexical_cast&) { - return false; - } - - return true; -} -``` - -值得注意的是,作为局部变量的参数(parameters),利用了 C++11 的 Move 语义,避免了额外的拷贝开销。 -当参数为很长的字符串时(比如 XML string),这一点特别有用。 - -最后,四个操作就是简单的转调 `Calc` 而已: -```cpp -bool Add(double x, double y, double* result) { - return Calc("add", "x", "y", x, y, result); -} - -bool Subtract(double x, double y, double* result) { - return Calc("subtract", "x", "y", x, y, result); -} - -bool Multiply(double x, double y, double* result) { - return Calc("multiply", "x", "y", x, y, result); -} - -bool Divide(double x, double y, double* result) { - return Calc("divide", "numerator", "denominator", x, y, result); -} -``` - -## 局限 - -当然,webcc 有很多局限,比如: -- 只支持 `int`, `double`, `bool` 和 `string` 这几种参数类型; -- 只支持 UTF-8 编码的消息内容; -- 一次调用一个连接; -- 连接是同步(阻塞)模式,可以指定 timeout(缺省为 15s)。 - -## 依赖 - -在实现上,webcc 有下面这些依赖: -- Boost 1.66+; -- XML 解析和构造基于 pugixml; -- 构建系统是 CMake,应该可以很方便地集成到其他 C++ 项目中。 diff --git a/doc/SoapServerTutorial.md b/doc/SoapServerTutorial.md deleted file mode 100644 index 5322b89..0000000 --- a/doc/SoapServerTutorial.md +++ /dev/null @@ -1,137 +0,0 @@ -# SOAP Server Tutorial - -Suppose you want to provide a calculator web service just like the one from [ParaSoft](http://ws1.parasoft.com/glue/calculator.wsdl). - -Firstly, create a class `CalcService` which is derived from `webcc::SoapService`, override the `Handle` method: -```cpp -// calc_service.h - -#include "webcc/soap_service.h" - -class CalcService : public webcc::SoapService { -public: - bool Handle(const webcc::SoapRequest& soap_request, - webcc::SoapResponse* soap_response) override; -}; -``` - -The `Handle` method has two parameters, one for request (input), one for response (output). - -The implementation is quite straightforward: - -- Get operation (e.g., add) from request; -- Get parameters (e.g., x and y) from request; -- Calculate the result. -- Set namespaces, result name and so on to response. -- Set result to response. - -```cpp -// calc_service.cpp - -#include "calc_service.h" - -#include "boost/lexical_cast.hpp" - -#include "webcc/soap_request.h" -#include "webcc/soap_response.h" - -bool CalcService::Handle(const webcc::SoapRequest& soap_request, - webcc::SoapResponse* soap_response) { - try { - if (soap_request.operation() == "add") { - double x = boost::lexical_cast(soap_request.GetParameter("x")); - double y = boost::lexical_cast(soap_request.GetParameter("y")); - - double result = x + y; - - soap_response->set_soapenv_ns(webcc::kSoapEnvNamespace); - soap_response->set_service_ns({ "cal", "http://www.example.com/calculator/" }); - soap_response->set_operation(soap_request.operation()); - soap_response->set_result_name("Result"); - soap_response->set_result(std::to_string(result)); - - return true; - } - - // Other operations ... - - } catch (boost::bad_lexical_cast&) { - // ... - } - - return false; -} -``` - -Next step, create a `SoapServer` and register `CalcService` to it with a URL. - -```cpp -int main(int argc, char* argv[]) { - // Check argc and argv ... - - unsigned short port = std::atoi(argv[1]); - - // Number of worker threads. - std::size_t workers = 2; - - try { - webcc::SoapServer server(port, workers); - - server.RegisterService(std::make_shared(), "/calculator"); - - server.Run(); - - } catch (std::exception& e) { - std::cerr << "Exception: " << e.what() << std::endl; - return 1; - } - - return 0; -} -``` - -The server is created with a **port number** which will be listened on to **asynchnously** accept client connections. The connections will be firstly put into a **queue** and then processed by the **worker threads**. The number of worker threads is determined by the `workers` parameter. - -When register service, the URL is what the clients will put in the HTTP request to access your service: -``` -POST /calculator HTTP/1.1 -``` - -Registering multiple services to a server is allowed, but the URL must be unique for each service. - -To invoke the `add` operation of the calculator service, an example of the client HTTP request would be: -``` -POST /calculator HTTP/1.1 -Content-Type: text/xml; charset=utf-8 -Content-Length: 263 -Host: localhost:8080 -SOAPAction: add - - - - - - 1.000000 - 2.000000 - - - -``` - -And the HTTP response is: -``` -HTTP/1.1 200 OK -Content-Type: text/xml; charset=utf-8 -Content-Length: 262 - - - - - - 3.000000 - - - -``` - -See [example/soap_calc_server](https://github.com/sprinfall/webcc/tree/master/example/soap_calc_server) for the full example. diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index 406ffcb..edc641c 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -20,12 +20,24 @@ // ----------------------------------------------------------------------------- -// Write a JSON object to string. -std::string JsonToString(const Json::Value& json) { +static std::string JsonToString(const Json::Value& json) { Json::StreamWriterBuilder builder; return Json::writeString(builder, json); } +static Json::Value StringToJson(const std::string& str) { + Json::Value json; + + Json::CharReaderBuilder builder; + std::stringstream stream(str); + std::string errs; + if (!Json::parseFromStream(builder, stream, &json, &errs)) { + std::cerr << errs << std::endl; + } + + return json; +} + // ----------------------------------------------------------------------------- class BookClientBase { @@ -78,26 +90,25 @@ public: return true; } - bool CreateBook(const std::string& id, - const std::string& title, - double price) { + bool CreateBook(const std::string& title, double price, std::string* id) { PrintSeparateLine(); - std::cout << "CreateBook: " << id << ", " << title << ", " << price - << std::endl; + std::cout << "CreateBook: " << title << ", " << price << std::endl; - Json::Value json(Json::objectValue); - json["id"] = id; - json["title"] = title; - json["price"] = price; + Json::Value req_json(Json::objectValue); + req_json["title"] = title; + req_json["price"] = price; - if (!rest_client_.Post("/books", JsonToString(json))) { + if (!rest_client_.Post("/books", JsonToString(req_json))) { PrintError(); return false; } std::cout << rest_client_.response_status() << std::endl; - return true; + Json::Value rsp_json = StringToJson(rest_client_.response_content()); + *id = rsp_json["id"].asString(); + + return !id->empty(); } }; @@ -123,8 +134,7 @@ public: return true; } - bool UpdateBook(const std::string& id, - const std::string& title, + bool UpdateBook(const std::string& id, const std::string& title, double price) { PrintSeparateLine(); std::cout << "UpdateBook: " << id << ", " << title << ", " << price @@ -187,12 +197,14 @@ int main(int argc, char* argv[]) { BookDetailClient detail_client(host, port, timeout_seconds); list_client.ListBooks(); - list_client.CreateBook("1", "1984", 12.3); - detail_client.GetBook("1"); - detail_client.UpdateBook("1", "1Q84", 32.1); - detail_client.GetBook("1"); - detail_client.DeleteBook("1"); + std::string id; + list_client.CreateBook("1984", 12.3, &id); + + detail_client.GetBook(id); + detail_client.UpdateBook(id, "1Q84", 32.1); + detail_client.GetBook(id); + detail_client.DeleteBook(id); list_client.ListBooks(); diff --git a/example/rest_book_server/services.cc b/example/rest_book_server/services.cc index 308acee..043fb71 100644 --- a/example/rest_book_server/services.cc +++ b/example/rest_book_server/services.cc @@ -2,8 +2,8 @@ #include #include +#include -#include "boost/thread/thread.hpp" #include "json/json.h" #include "webcc/logger.h" @@ -42,11 +42,11 @@ static bool JsonToBook(const std::string& json, Book* book) { // Return all books as a JSON array. // TODO: Support query parameters. -bool BookListService::Get(const webcc::UrlQuery& /* query */, +bool BookListService::Get(const webcc::UrlQuery& /*query*/, std::string* response_content) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); - boost::this_thread::sleep_for(boost::chrono::seconds(sleep_seconds_)); + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } Json::Value root(Json::arrayValue); @@ -61,17 +61,23 @@ bool BookListService::Get(const webcc::UrlQuery& /* query */, } // Add a new book. -// No response content. bool BookListService::Post(const std::string& request_content, - std::string* /* response_content */) { + std::string* response_content) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); - boost::this_thread::sleep_for(boost::chrono::seconds(sleep_seconds_)); + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } Book book; if (JsonToBook(request_content, &book)) { - g_book_store.AddBook(book); // TODO: return ID + std::string id = g_book_store.AddBook(book); + + Json::Value root; + root["id"] = id; + + Json::StreamWriterBuilder builder; + *response_content = Json::writeString(builder, root); + return true; } @@ -85,7 +91,7 @@ bool BookDetailService::Get(const std::vector& url_sub_matches, std::string* response_content) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); - boost::this_thread::sleep_for(boost::chrono::seconds(sleep_seconds_)); + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { @@ -110,7 +116,7 @@ bool BookDetailService::Put(const std::vector& url_sub_matches, std::string* response_content) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); - boost::this_thread::sleep_for(boost::chrono::seconds(sleep_seconds_)); + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { @@ -132,7 +138,7 @@ bool BookDetailService::Delete( const std::vector& url_sub_matches) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); - boost::this_thread::sleep_for(boost::chrono::seconds(sleep_seconds_)); + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { diff --git a/example/soap_calc_client_parasoft/main.cc b/example/soap_calc_client_parasoft/main.cc index c0c086b..e912395 100644 --- a/example/soap_calc_client_parasoft/main.cc +++ b/example/soap_calc_client_parasoft/main.cc @@ -79,8 +79,7 @@ class CalcClient { int main() { WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); - // Default port 80. - CalcClient calc("ws1.parasoft.com", ""); + CalcClient calc("ws1.parasoft.com", ""); // Use default port 80 double x = 1.0; double y = 2.0; diff --git a/example/soap_calc_server/calc_service.cc b/example/soap_calc_server/calc_service.cc index ae20f70..70a0103 100644 --- a/example/soap_calc_server/calc_service.cc +++ b/example/soap_calc_server/calc_service.cc @@ -11,7 +11,11 @@ bool CalcService::Handle(const webcc::SoapRequest& soap_request, webcc::SoapResponse* soap_response) { double x = 0.0; double y = 0.0; - if (!GetParameters(soap_request, &x, &y)) { + try { + x = std::stod(soap_request.GetParameter("x")); + y = std::stod(soap_request.GetParameter("y")); + } catch (const std::exception& e) { + LOG_ERRO("SoapParameter cast error: %s", e.what()); return false; } @@ -61,16 +65,3 @@ bool CalcService::Handle(const webcc::SoapRequest& soap_request, return true; } - -bool CalcService::GetParameters(const webcc::SoapRequest& soap_request, - double* x, double* y) { - try { - *x = std::stod(soap_request.GetParameter("x")); - *y = std::stod(soap_request.GetParameter("y")); - } catch (const std::exception& e) { - LOG_ERRO("SoapParameter cast error: %s", e.what()); - return false; - } - - return true; -} diff --git a/example/soap_calc_server/calc_service.h b/example/soap_calc_server/calc_service.h index deb3321..2d767e3 100644 --- a/example/soap_calc_server/calc_service.h +++ b/example/soap_calc_server/calc_service.h @@ -10,10 +10,6 @@ class CalcService : public webcc::SoapService { bool Handle(const webcc::SoapRequest& soap_request, webcc::SoapResponse* soap_response) override; - - private: - bool GetParameters(const webcc::SoapRequest& soap_request, - double* x, double* y); }; #endif // CALC_SERVICE_H_ diff --git a/webcc/http_request_handler.cc b/webcc/http_request_handler.cc index 82a8443..4067094 100644 --- a/webcc/http_request_handler.cc +++ b/webcc/http_request_handler.cc @@ -17,7 +17,7 @@ void HttpRequestHandler::Start(std::size_t count) { assert(count > 0 && workers_.size() == 0); for (std::size_t i = 0; i < count; ++i) { - workers_.create_thread(std::bind(&HttpRequestHandler::WorkerRoutine, this)); + workers_.emplace_back(std::bind(&HttpRequestHandler::WorkerRoutine, this)); } } @@ -33,7 +33,11 @@ void HttpRequestHandler::Stop() { // Enqueue a null connection to trigger the first worker to stop. queue_.Push(HttpConnectionPtr()); - workers_.join_all(); + for (auto& worker : workers_) { + if (worker.joinable()) { + worker.join(); + } + } LOG_INFO("All workers have been stopped."); } diff --git a/webcc/http_request_handler.h b/webcc/http_request_handler.h index 1151867..61ab114 100644 --- a/webcc/http_request_handler.h +++ b/webcc/http_request_handler.h @@ -2,10 +2,9 @@ #define WEBCC_HTTP_REQUEST_HANDLER_H_ #include +#include #include -#include "boost/thread/thread.hpp" - #include "webcc/http_connection.h" #include "webcc/queue.h" #include "webcc/soap_service.h" @@ -39,7 +38,7 @@ class HttpRequestHandler { virtual void HandleConnection(HttpConnectionPtr connection) = 0; Queue queue_; - boost::thread_group workers_; + std::vector workers_; }; } // namespace webcc