diff --git a/README.md b/README.md index 736fb28..0f29f0b 100644 --- a/README.md +++ b/README.md @@ -1,214 +1,21 @@ -# csoap +# webcc -A lightweight C++ SOAP client & server library based on Boost.Asio. +A lightweight C++ REST and SOAP client and server library based on Boost.Asio. -[中文教程](doc/ClientTutorial_zh-CN.md) +## Tutorials -## Client Usage +**SOAP:** +- [SOAP Client Tutorial](doc/SoapClientTutorial.md) +- [SOAP Server Tutorial](doc/SoapServerTutorial.md) -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. +- [SOAP 客户端教程](doc/SoapClientTutorial_zh-CN.md) -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 *csoap*. - -Firstly, create a class `CalculatorClient` which is derived from `csoap::SoapClient`: - -```cpp -#include -#include "csoap/soap_client.h" - -class CalculatorClient : public csoap::SoapClient { -public: - CalculatorClient() { - 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; - csoap::Error error = Call(operation, std::move(parameters), &result_str); - - // Error handling if any. - if (error != csoap::kNoError) { - std::cerr << "Error: " << error; - std::cerr << ", " << csoap::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. - -## Server Usage - -*NOTE: The server part is currently under development, not stable enough to be used in a real production.* - -Suppose you want to provide a calculator web service just like the one from ParaSoft. - -Firstly, create a class `CalculatorService` which is derived from `csoap::SoapService`, override the `Handle` method: -```cpp -#include "csoap/soap_service.h" - -class CalculatorService : public csoap::SoapService { -public: - CalculatorService(); - - bool Handle(const csoap::SoapRequest& soap_request, - csoap::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 from request. -- Get parameters from request. -- Calculate the result. -- Set namespaces, result name and so on to response. -- Set result to response. - -```cpp -#include "calculator_service.h" - -#include "boost/lexical_cast.hpp" - -#include "csoap/soap_request.h" -#include "csoap/soap_response.h" - -CalculatorService::CalculatorService() { -} - -bool CalculatorService::Handle(const csoap::SoapRequest& soap_request, - csoap::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(csoap::kSoapEnvNamespace); - soap_response->set_service_ns({ "cal", "http://mycalculator/" }); - soap_response->set_operation(soap_request.operation()); - soap_response->set_result_name("Result"); - soap_response->set_result(boost::lexical_cast(result)); - - return true; - - } else { - // NOT_IMPLEMENTED - } - } catch (boost::bad_lexical_cast&) { - // BAD_REQUEST - } - - return false; -} -``` - -The `main` function would be: -```cpp -int main(int argc, char* argv[]) { - if (argc != 2) { - std::cout << "Usage: " << argv[0] << " " << std::endl; - std::cout << " E.g.," << std::endl; - std::cout << " " << argv[0] << " 8080" << std::endl; - return 1; - } - - const char* host = "0.0.0.0"; // TODO - - try { - csoap::HttpServer server(host, argv[1]); - - csoap::SoapServicePtr service(new CalculatorService); - server.RegisterService(service); - - server.Run(); - - } catch (std::exception& e) { - std::cerr << "exception: " << e.what() << "\n"; - return 1; - } - - return 0; -} -``` - -## Limitations +## Dependencies -- Only support `int`, `double`, `bool` and `string` parameters. -- Only support UTF-8 encoded content. -- One connection one call. -- Connection is in synchronous (or blocking) mode; timeout (default to 30s) is configurable. +- C++11 +- Boost 1.66+ +- pugixml (already included in the source tree) (SOAP only) -## Dependencies +## Build -- Boost 1.66+. -- XML processing is based on PugiXml, which is already included in the source tree. -- Build system is CMake, but it should be quite easy to integrate into your own project. +TODO diff --git a/doc/SoapClientTutorial.md b/doc/SoapClientTutorial.md new file mode 100644 index 0000000..71c47e8 --- /dev/null +++ b/doc/SoapClientTutorial.md @@ -0,0 +1,101 @@ +# 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/ClientTutorial_zh-CN.md b/doc/SoapClientTutorial_zh-CN.md similarity index 85% rename from doc/ClientTutorial_zh-CN.md rename to doc/SoapClientTutorial_zh-CN.md index da0961a..979fdf1 100644 --- a/doc/ClientTutorial_zh-CN.md +++ b/doc/SoapClientTutorial_zh-CN.md @@ -1,4 +1,4 @@ -# cSoap 客户端使用指南 +# SOAP Client Tutorial (zh-CN) ## 背景 @@ -8,11 +8,12 @@ WWSAPI 的官方文档经常让人摸不着头脑,没有完整的示例,给出一段代码,常常需要几经调整才能使用。WWSAPI 自动生成的代码,是纯 C 的接口,在难用程度上,较 gSoap 有过之而无不及。在消息参数上,它强制使用双字节 Unicode,我们的输入输出都是 UTF8 的 `std::string`,于是莫名地多出很多编码转换。WWSAPI 需要你手动分配堆(heap),需要你指定消息的缓冲大小,而最严重的问题是,它不够稳定,特别是在子线程里调用时,莫名其妙连接就会断掉。 -于是,我就动手自己写了个 [cSoap](https://github.com/sprinfall/csoap)。 +于是,我就动手自己写了个 [webcc](https://github.com/sprinfall/webcc)。 +一开始 webcc 只支持 SOAP,名字就叫 csoap,后来支持了 REST,于是改名为 webcc,取 Web C++ 的意思。 ## 原理 -cSoap 没有提供从 WSDL 自动生成代码的功能,一来是因为这一过程太复杂了,二来是自动生成的代码一般都不好用。所以 cSoap 最好搭配 [SoapUI](https://www.soapui.org) 一起使用。SoapUI 可以帮助我们为每一个 Web Service 操作(operation)生成请求的样例,基于请求样例,就很容易发起调用了,也避免了直接阅读 WSDL。 +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 @@ -49,19 +50,19 @@ User-Agent: Apache-HttpClient/4.1.1 (java 1.5) ``` -所以 cSoap 所做的,只不过是跟 `ws1.parasoft.com` 建立 TCP Socket 连接,然后发送上面这段内容而已。 +所以 webcc 所做的,只不过是跟 `ws1.parasoft.com` 建立 TCP Socket 连接,然后发送上面这段内容而已。 ## 用法 -首先,创建一个类 `CalculatorClient`,继承自 `csoap::SoapClient`: +首先,创建一个类 `CalcClient`,继承自 `webcc::SoapClient`: ```cpp #include -#include "csoap/soap_client.h" +#include "webcc/soap_client.h" -class CalculatorClient : public csoap::SoapClient { +class CalcClient : public webcc::SoapClient { public: - CalculatorClient() { + CalcClient() { Init(); } ``` @@ -87,19 +88,19 @@ bool Calc(const std::string& operation, double y, double* result) { // Prepare parameters. - std::vector parameters{ + std::vector parameters{ { x_name, x }, { y_name, y } }; // Make the call. std::string result_str; - csoap::Error error = Call(operation, std::move(parameters), &result_str); + webcc::Error error = Call(operation, std::move(parameters), &result_str); // Error handling if any. - if (error != csoap::kNoError) { + if (error != webcc::kNoError) { std::cerr << "Error: " << error; - std::cerr << ", " << csoap::GetErrorMessage(error) << std::endl; + std::cerr << ", " << webcc::GetErrorMessage(error) << std::endl; return false; } @@ -138,7 +139,7 @@ bool Divide(double x, double y, double* result) { ## 局限 -当然,cSoap 有很多局限,比如: +当然,webcc 有很多局限,比如: - 只支持 `int`, `double`, `bool` 和 `string` 这几种参数类型; - 只支持 UTF-8 编码的消息内容; - 一次调用一个连接; @@ -146,7 +147,7 @@ bool Divide(double x, double y, double* result) { ## 依赖 -在实现上,cSoap 有下面这些依赖: +在实现上,webcc 有下面这些依赖: - Boost 1.66+; -- XML 解析和构造基于 PugiXml; +- XML 解析和构造基于 pugixml; - 构建系统是 CMake,应该可以很方便地集成到其他 C++ 项目中。 diff --git a/doc/SoapServerTutorial.md b/doc/SoapServerTutorial.md new file mode 100644 index 0000000..927e4c5 --- /dev/null +++ b/doc/SoapServerTutorial.md @@ -0,0 +1,94 @@ +# 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 +#include "webcc/soap_service.h" + +class CalcService : public webcc::SoapService { +public: + CalcService() = default; + + 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 from request. +- Get parameters from request. +- Calculate the result. +- Set namespaces, result name and so on to response. +- Set result to response. + +```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; + + } else { + // NOT_IMPLEMENTED + } + } catch (boost::bad_lexical_cast&) { + // BAD_REQUEST + } + + return false; +} +``` + +The `main` function would be: +```cpp +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cout << "Usage: " << argv[0] << " " << std::endl; + std::cout << " E.g.," << std::endl; + std::cout << " " << argv[0] << " 8080" << std::endl; + return 1; + } + + 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; +} +```