用gSOAP開發Web Service

內容來源為簡體中文,我把它轉成繁體以便閱讀,有興趣者請到http://hi.baidu.com/winnyang/blog/item/1138fad9bfac1be338012fdf.html


gSOAP是一個綁定SOAP/XML到C/C++語言的工具,使用它可以簡單快速地開發出SOAP/XML的伺服器端和用戶端。由於 gSOAP具 有相當不錯的相容性,通過gSOAP,我們就可以調用由Java, .Net, Delhpi, PHP等語言開發的SOAP服務,或者向它們提供SOAP服務。
gSOAP的主頁是:http://sourceforge.net/projects/gsoap2
下載解壓後,可以在gsoap\bin\win32裏 找到wsdl2h.exe和soapcpp2.exe(另外還有linux和mac版本)。

  • wsdl2h.exe的作用是根據WSDL生成C/C++風格的.h文件。
  • soapcpp2.exe的作用是根據頭檔自動生成調用遠端 SOAP服務的用戶端代碼(稱為存根:Stub)和提供SOAP服務的框架代碼(稱為框架:Skeleton),另外它也能從.h檔生成WSDL檔。

gsoap\stdsoap2.cpp則是gSOAP的核心代碼,要使用gSOAP只要在項目裏包含這個檔以及由soapcpp2.exe生成的代碼即可。另外還有個stdsoap2.c,內容與stdsoap2.cpp一 模一樣,用於純C項目。

gSOAP兩大工具的用法

從WSDL中產生.h檔:
用法:
wsdl2h -o [.h檔案名 WSDL檔案名或URL]
wsdl2h常用選項:
  • -o 檔案名,指定輸出頭檔
  • -n 名空間首碼 代替默認的ns
  • -c 產生純C代碼,否則是C++代碼
  • -s 不要使用STL代碼
  • -t 檔案名,指定type map檔,默認為typemap.dat
  • -e 禁止為enum成員加上名空間首碼
type map檔用於指定SOAP/XML中的類型與C/C++之間的轉換規則,比如在wsmap.dat裏寫
xsd__string = | std::wstring | wchar_t*
那麼SOAP/XML中的string將轉換成std::wstring或wchar_t*,這樣能更好地支持中文。
例如:
wsdl2h -o ayandy.h \
 -n ay -t wsmap.dat \
 http://www.ayandy.com/Service.asmx?WSDL
從http://www.ayandy.com/Service.asmx?WSDL 生成ayandy.h檔,名空間為ay,使用wsmap.dat指定的轉換規則。
wsdl2h生成的頭檔裏的變數、類型等名稱的前面都會加上名空間首碼,以兩個下劃線分隔。如上面的命令生成的頭檔,有這樣的定義:
class ay1__ArrayOfString;
enum ay1__theDayFlagEnum
{
    ay1__theDayFlagEnum__Today,
    ay1__theDayFlagEnum__Tomorrow,
    ay1__theDayFlagEnum__theDayafterTomorrow,
};

前面的ayandy1__的是名空間首碼,用以防止名稱衝突。 wsdl2h的-n選項可以改變這個名空間首碼(默認為ns)。對於枚舉ay1__theDayFlagEnum內 的成員,如果嫌它太長的話,可以用-e命令選項禁止加入名空間首碼。
 
從.h檔生成存根(stub)和框架(Skeleton)原始檔案

編寫SOAP程式除了頭檔是不夠的,還要有連接、通信、XML解析、序列/反序列化等工作。gSOAP提供的socapcpp2.exe就 是用於從頭檔中生成這些代碼的,我們只要關心真正的業務邏輯就行了。
用法
soapcpp2 .h文件
例如:
soapcpp2 ayandy.h
將生成下面這些檔
  • soapStub.h    // soap的存根檔,定義了ayandy.h裏對應的遠端調用模型
  • soapC.c soapH.h // soap的序列和反序列代碼,它已經包含了soapStub.h,伺服器端與用戶端都要包含它
  • soapClient.c soapClientLib.c // 用戶端代碼,soapClientLib.c檔則只是簡單地包含soapClient.c和soapC.c
  • soapServer.c soapServerLib.c // 伺服器端代碼,soapServerLib.c檔則只是簡單地包含soapServer.c和soapC.c
  • ServiceSoap.nsmap ServiceSoap12.nsmap // 名空間定義,伺服器端與用戶端都要包含它
  • soapServiceSoapProxy.h soapServiceSoap12Proxy.h // 用戶端的C++簡單包裝(如果頭檔是純C代碼,這兩個檔就不會生成)
綜上所述
  • 如果編寫伺服器端,項目裏應該加入soapServerLib.c,代碼裏包含頭檔soapH.h
  • 如果編寫用戶端,項目裏應該加入soapClientLib.c,代碼裏包含頭檔SoapH.h(或xxxxProxy.h)
  • 當然,還要加入gsoap庫裏的stdsoap2.cpp檔(如果是寫C代碼,則加入stdsoap2.c)
如果看到soapcpp2提示:”Critical error: #import: Cannot open file "stlvector.h" for reading.“, 那是因為我們的頭檔使用了STL(wsdl2h 沒用-s選項),這時要使用-I選項指定gSOAP的 import檔路徑,這個路徑是"$gsoap\gsoap\import":soapcpp2 ayandy.h -I D:\gsoap-2.7\gsoap\import
soapcpp2常用選項
  • -C 僅生成用戶端代碼
  • -S 僅生成伺服器端代碼
  • -L 不要產生soapClientLib.c和soapServerLib.c文件
  • -c 產生純C代碼,否則是C++代碼(與頭文件有關)
  • -I 指定import路徑(見上文)
  • -x 不要產生XML示例檔
  • -i 生成C++包裝,用戶端為xxxxProxy.h(.cpp),伺服器端為xxxxService.h(.cpp)。
編寫SOAP用戶端

下面將演示使用gSOAP到網上取得天氣預報,互聯網上有不少網站提供SOAP服務,比如Google提供的搜索API(現在已不再提 供新的License Key了),不少博客提供的API等。這裏介紹一個提供天氣預報服務的SOAP服務,位址是http://www.ayandy.com
它提供了三個函數:
  • getSupportCity 查詢本天氣WebService支援的城市資訊。
  • getSupportProvince 查詢本天氣 WebService支援的省份資訊。
  • getWeatherbyCityName 根據城市名稱獲得天 氣情況。
它的WSDL位址是http://www.ayandy.com/Service.asmx?WSDL。
現在,我們編寫一個用戶端去調用getWeatherbyCityName來 取得天氣情況
  1. 從WSDL得到頭文件:
    wsdl2h -o ayandy.h http://www.ayandy.com/Service.asmx?WSDL
  2. 從頭檔得到存根(Stub)原始檔案:
    soapcpp2 -i -C -x ayandy.h -ID:\gsoap-2.7\gsoap\import
    命令選項注釋
    • -i 直接使用C++包裝類
    • -x 不要生成一堆看了就噁心的xml
    • -C 只生成客戶端相關代碼
    • -I 指定import路徑
  3. 建立新專案:
    把gsoap庫裏的stdsoap2.cpp檔,以及上一步生成的soapServiceSoapProxy.cpp和soapC.cpp都加入到項 目。設置加入的這三個檔為不使用預編譯頭。
  4. 編寫代碼:
    由於參數及回傳的資料都是中文,所有讓gSOAP使用UTF8方式傳送以防止亂碼。

#include <iostream>
#include <string>
#include "soapServiceSoapProxy.h"
#include "ServiceSoap.nsmap" //表忘了名空間定義

using namespace std;
   
// 寬 字元轉UTF8
string EncodeUtf8(wstring in)
{
    string s(in.length()*3+1,' ');
    size_t len = ::WideCharToMultiByte(CP_UTF8, 0,
    in.c_str(), in.length(),
    &s[0], s.length(),
    NULL, NULL);
    s.resize(len);
    return s;
}

// UTF8 轉寬字元
wstring DecodeUtf8(string in)
{
    wstring s(in.length(), _T(' '));
    size_t len = ::MultiByteToWideChar(CP_UTF8, 0,
    in.c_str(), in.length(),
    &s[0], s.length());
   s.resize(len);
   return s;
}

int main(int argc, char* argv[])
{
    ServiceSoapProxy gs(SOAP_C_UTFSTRING);
  
    _ns1__getWeatherbyCityName cityname;
    _ns1__getWeatherbyCityNameResponse resp;

    string strCityName = EncodeUtf8(L"蘇州");
    cityname.theCityName = &strCityName;
    cityname.theDayFlag = ns1__theDayFlagEnum__Tomorrow;
   
    if(gs.getWeatherbyCityName(&cityname, &resp) == SOAP_OK)
    {
        ns1__ArrayOfString *aos = resp.getWeatherbyCityNameResult;
        wcout.imbue( std::locale("chs") ); //指定輸出為中文
        for(vector<string>::iterator
            itr=aos->string.begin(), itr_end = aos->string.end();
            itr!=itr_end; ++itr)
            wcout << DecodeUtf8(*itr) << endl;
    }
    return 0;
}

上面的代碼花了一半在UTF8編碼轉換上,如果參數裏沒有中文的話,代碼會簡化很多:
    ServiceSoapProxy gs;
    _ns1__getWeatherbyCityName cityname;
    _ns1__getWeatherbyCityNameResponse resp;
    string strCityName("蘇州");
    cityname.theCityName = &strCityName;
    cityname.theDayFlag = ns1__theDayFlagEnum__Tomorrow;
       
    if(gs.getWeatherbyCityName(&cityname, &resp) == SOAP_OK)
    {
        ns1__ArrayOfString *aos = resp.getWeatherbyCityNameResult;
        for(vector<string>::iterator
            itr=aos->string.begin(), itr_end = aos->string.end();
            itr!=itr_end; ++itr)
        cout << *itr << endl;
    }

但是這個代碼應用到中文字串時,會發現返回的是一堆亂碼,gSOAP有兩種方式支援它:
  1. 使用寬字元集,如用前文演示的type map檔來轉換字串為std::wstring。
  2. 使用UTF8傳送字串,這個例子就是使用的這個方式:首先,定義ServiceSoapProxy gs的傳送模式為SOAP_C_UTFSTRING;然後輸入時把字串轉換成UTF8,得到輸出時把UTF8轉換回來。
使用UTF8時還要注意一點,如果使用純C調用,那麼應該這樣設置UTF8調用:
    soap sp;
    soap_init(&sp);
    soap_set_mode(&sp, SOAP_C_UTFSTRING);
    sp.mode |= SOAP_C_UTFSTRING; //關鍵

也許是gSOAP的bug吧,soap_set_mode只 設置了sp.imode和sp.omode兩個成員,卻沒有設置sp.mode。跟蹤代碼可以發現從伺服器傳回資料後,gSOAP是根據sp.mode來 決定是否使用UTF8轉換的。
編寫SOAP伺服器

現在,我們自己動手寫一個天氣預報服務,當然,是亂報的啦,呵呵。
  1. 這次,我們嘗試使用寬字元集的方式來支援中文:
    寫一個wsmap.dat文件,裏面寫上:xsd__string = | std::wstring | std::wstring*
  2. 從WSDL生成頭檔:
    wsdl2h.exe -o ayandy.h -t wsmap.dat -e http://www.ayandy.com/Service.asmx?WSDL
    命令選項注釋:
    • -o ayandy.h 生成ayandy.h頭文件
    • -t wsmap.dat 根據wsmap.dat規則轉換資料類型
    • -e 枚舉成員不要有長長的名空間首碼
  3. 從頭檔生成伺服器框架代碼:
    soapcpp2 ayandy.h -i -x -S -I D:\Code\libs\gsoap-2.7\gsoap\import
    命令選項注釋:
    • -S 僅生成伺服器框架代碼
  4. 新建項目:
    把gsoap庫裏的stdsoap2.cpp檔,以及上一步生成的soapServiceSoapService.cpp和soapC.cpp都加入到 項目。
    設置加入的這三個檔為不使用預編譯頭。
  5. 編寫代碼:
    打開soapcpp2生成的soapServiceSoapService.h檔,在ServiceSoapService類定義裏 會看到這樣幾行字:

///
/// Service operations (you should define these):
///

它後面的幾個方法是要我們自己實現它的,先看代碼吧:
#include "soapServiceSoapService.h"
#include "ServiceSoap.nsmap"
   
/// Web service operation 'getWeatherbyCityName' (returns error code or SOAP_OK)
int ServiceSoapService::getWeatherbyCityName(
    _ns1__getWeatherbyCityName *ns1__getWeatherbyCityName,
    _ns1__getWeatherbyCityNameResponse *ns1__getWeatherbyCityNameResponse)
{
    if(*(ns1__getWeatherbyCityName->theCityName) != L"蘇州") return SOAP_USER_ERROR;
   
    ns1__ArrayOfString * aos = soap_new_ns1__ArrayOfString(this, -1);

    aos->string.push_back( std::wstring() ); //第0個空著
    if(ns1__getWeatherbyCityName->theDayFlag != Tomorrow)
    {
        aos->string.push_back( L"我只知道明天天氣,其他的不要問我!" );
    }
    else
    {
        aos->string.push_back( L"有日食,不過下大雨,哈哈,氣死你!" );
        aos->string.push_back( L"下雨當然有風啦,多大我也不知道" );
    }
    ns1__getWeatherbyCityNameResponse->getWeatherbyCityNameResult = aos;
    return SOAP_OK;
}
 
/// Web service operation 'getSupportProvince' (returns error code or SOAP_OK)
int ServiceSoapService::getSupportProvince(
    _ns1__getSupportProvince *ns1__getSupportProvince,
    _ns1__getSupportProvinceResponse *ns1__getSupportProvinceResponse)
{
    return SOAP_OK;
}
   
/// Web service operation 'getSupportCity' (returns error code or SOAP_OK)
int ServiceSoapService::getSupportCity(
    _ns1__getSupportCity *ns1__getSupportCity,
    _ns1__getSupportCityResponse *ns1__getSupportCityResponse)
{
    return SOAP_OK;
}

int main(int argc, char* argv[])
{
    ServiceSoapService sev;
    return sev.run(8888);//本機8888埠
}

編譯,運行,現在我們的本機8888埠開始提供天氣預報的SOAP服務了。
修改之前的用戶端,在main()裏第一行:
ServiceSoapProxy gs(SOAP_C_UTFSTRING);

後面加上:
gs.soap_endpoint="http://localhost:8888";

運行這用戶端後可以就看到我們提供的優質服務了!本例中getWeatherbyCityName方 法裏的ns1__getWeatherbyCityNameResponse參 數用於返回資料,它getWeatherbyCityNameResult成 員是由我們來申請記憶體的,這個記憶體應該用“soap_new_ 類名”來取得,這些申請函數可以從soapH.h裏找到,如本例的soap_new_ns1__ArrayOfString。
  • 第一個參數是soap類型,它是ServiceSoapService的父類型,也是gSOAP中最重要的類型。
  • 第二個指定申請的個數,指定為-1表示只生成一個,否則生成一個指定數目的陣列。

0 意見:

張貼留言