背景
對於舊有系統的改造和升級,最苦惱的莫過於跨平台,跨語言。我的一個朋友最近從Java專向了專攻.NET——因為.NET的CLR既有類似Java虛擬機器概念這種已經被證明很成功的底層託管能力。又對於Windows的就有案頭應用提供了良好的相容。
最近我的一個個人項目也面臨著這樣的需求。一個C語言開發的中介軟體,通過API暴露給二次開發及外掛程式應用。現在由於對其應用的需求變得日趨複雜,而且正在脫離Unix的管理環境,走向基於JWS這樣的BCS管理。有朋友推薦我用JNI,但這樣一是增加了耦合度,二是讓Java睡在JNI感覺不太安穩。在認知了上下兩層的系統平台後,問題變得明朗起來:如何在HTTP協議下實現Java和C之間的互動?
思路
本人對Java比較熟悉,先從Java的角度入手,Java間的通訊方法:
1、通過URL,Applet/JWS訪問被影射到URL的動態資源(Servlet)
2、通過URL,Applet/JWS訪問共用的靜態資源(Server定期更新靜態資源)
3、通過序列化和還原序列化,實現簡單對象的傳輸(比如Resin的Hessian架構就提供了這種通訊的方式)
4、通過一些工具做代碼產生,利用Web Services實現用戶端和服務端的互動
此外脫離HTTP,還可以做RMI,socket編程。
現在問題是通訊的一端由Java變成了C/C++,於是,解決方案1需要把動態資源由CGI來定義,而方案3變得不再適用。於是方案有:
1、通過URL,Applet/JWS訪問被影射到URL的動態資源(CGI)
2、通過URL,Applet/JWS訪問共用的靜態資源(Server定期更新靜態資源)
3、通過一些工具做代碼產生,利用Web Services實現用戶端和服務端的互動(×××這是我們討論的重點×××)
解決方案
現在針對上文提出的3中通訊方式中的1和3談一談實現的方法,2的實現方案比較靈活,需要發揮大家的想象力了:)
針對CGI:
首先CGI可以配置在各種主流的伺服器中作為後端的指令碼運行。大家可能對Servlet更熟悉一些。
CGI可以用指令碼寫,也可以用C來實現。CGI被觸發後,通過系統的環境變數來獲得輸入,在處理完畢後向標準輸出中輸出結果。
由此可以想見,Web伺服器在接受到來自HTTP協議的請求後,首先把請求的參數擷取到,然後設定到環境變數裡。
根據對訪問的URL的解析和伺服器自身的配置,找到服務於請求的CGI程式的位置,然後執行這個程式。
這個程式被執行後通過環境變數得到了伺服器先前設定在環境變數中的參數。在經過一些複雜的邏輯操作後,向標準輸出輸出結果。
這個輸出又被Web伺服器所捕獲,轉而傳遞迴請求的用戶端。
更多關於CGI的知識和理解,大家可以通過google來尋找答案
上述CGI的方式可以讓我們直接擷取到結果,但是方案比較原始和基礎。其缺點有:
1、需要自己制定類型傳輸協議,做封裝和拆封,否則只支援字串;
2、我們不會為了要用C的API就給它裝一個或者自己實現一個Web伺服器的,這讓我們的底層程式顯得蠢笨而冗餘。我們希望能有一個超薄的Server外殼,在對API封裝後,通過某個連接埠進行開放即可。
針對Web Servcies
Based on上面的兩個不足,我們只能把希望寄託在Web Services身上了,筆者在這裡推薦給大家的是在C/C++很著名的Web Services工具gSOAP。大家可以到http://gsoap2.sourceforge.net/上去下載這個工具。
通過這個工具,我們可以做到:
1、一個Stand-alone的伺服器外殼
2、一個根據API程式自動產生的Web Services服務
3、一個WSDL描述符檔案
有關基於gSOAP的Web Services C服務端和Java用戶端的運行機理,及通過Java用戶端訪問gSOAP的Web Services的過程中需要注意的問題(筆者費了一天周折才搞清楚),將在下邊描述。
接著,我們聊聊gSOAP這個架構,我們把用C寫的舊有系統用gSOAP改造一下,通過SOA的形式發布出去。
上文提到,利用gSOAP可以做到以下3點:
1、一個Stand-alone的伺服器外殼
2、一個根據API程式自動產生的Web Services服務
3、一個WSDL描述符檔案
客戶根據 WSDL 描述文檔,會產生一個 SOAP 請求訊息。Web Services 都是放在Web伺服器後面,客戶產生的SOAP請求會被嵌入在一個HTTP POST請求中,發送到 Web 服務器來。Web 服務器再把這些請求轉寄給 Web Services 要求處理常式。要求處理常式的作用在於,解析收到的 SOAP 請求,調用 Web Services,然後再產生相應的 SOAP 應答。Web 服務器得到 SOAP 應答後,會再通過 HTTP應答的方式把資訊送回到用戶端。
WSDL是Web服務中用戶端和服務端溝通的橋樑,描述了對象提供的方法。SOAP幫我們制定了一份被官方認可的對象的封裝方法。有了WSDL,用戶端只關心如何把參數用Soap封裝起來發出去,並擷取結果。服務端只關心如何對Soap進行拆包->服務->封包。gSOAP可以幫我們實現上述過程中的拆包和封包,而我們可以只關心服務的實現。
言歸正傳,在這裡我們以一個簡單的實現加、減、開放的Web Services的服務為例子,介紹gSOAP的使用:
為了發布這個Web服務,首先我們需要把服務的介面定義好,這個服務可能是一個現有服務的Adapter,為此我們定義標頭檔。
calc.h: typedef double xsd__double; int ns__add(xsd__double a, xsd__double b, xsd__double &result); int ns__sub(xsd__double a, xsd__double b, xsd__double &result); int ns__sqrt(xsd__double a, xsd__double &result);
|
注意到這裡面我們把double定義成了xsd__double(兩個底線),這是為了告訴gSOAP,我們需要的soap格式和WSDL格式是基於Document/literal的而非rpc/encoded.為了不把事情搞複雜,在這裡我只能說,Java1.6內建的Web Services工具只支援Document/literal格式的WSDL,所以我們產生這種格式的WSDL。至於這兩種格式之間選擇和他們的long story,大家可以參考下面的文章:http://www.ibm.com/developerworks/webservices/library/ws-whichwsdl/
編寫好標頭檔後,我們就可以利用gSOAP提供的工具進行產生了:
/usr/lib/gsoap-2.7/bin/soapcpp2 -S -2 calc.h
|
產生的主要檔案詳見附件。
下面我們實現calc.h中定義的函數:
// Contents of file "calc.cpp": #include "soapH.h" #include "ns.nsmap" #include int main() { struct soap soap; int m, s; // master and slave sockets soap_init(&soap); m = soap_bind(&soap, "localhost", 9999, 100); if (m < 0) soap_print_fault(&soap, stderr); else { fprintf(stderr, "Socket connection successful: master socket = %d\n", m); for (int i = 1; ; i++) { s = soap_accept(&soap); if (s < 0) { soap_print_fault(&soap, stderr); break; } fprintf(stderr, "%d: accepted connection from IP=%d.%d.%d.%d socket=%d", i, (soap.ip >> 24)&0xFF, (soap.ip >> 16)&0xFF, (soap.ip >> 8)&0xFF, soap.ip&0xFF, s); if (soap_serve(&soap) != SOAP_OK) // process RPC request soap_print_fault(&soap, stderr); // print error fprintf(stderr, "request served\n"); soap_destroy(&soap); // clean up class instances soap_end(&soap); // clean up everything and close socket } } soap_done(&soap); // close master socket and detach environment } // Implementation of the "add" remote method: int ns__add(struct soap *soap, double a, double b, double &result) { result = a + b; return SOAP_OK; } // Implementation of the "sub" remote method: int ns__sub(struct soap *soap, double a, double b, double &result) { result = a - b; return SOAP_OK; } // Implementation of the "sqrt" remote method: int ns__sqrt(struct soap *soap, double a, double &result) { if (a >= 0) { result = sqrt(a); return SOAP_OK; } else { return soap_sender_fault(soap, "Square root of negative value", "I can only compute the square root of a non-negative value"); } }
|
前文提到過,我們不希望為了發布基於Web Services的C語言的API而開發或應用一個大的Web伺服器。我們代碼中的main函數實現了一個最簡單的Web Server(基於Socket)。這個Server利用gSOAP產生的API來提供針對SOAP的處理。
下面我們把這個嵌入式的web server編譯,編譯的時候注意stdsoap2.cpp這個檔案是從gSOAP包中拷貝而來,不是自動產生的,大家下載gSOAP後直接就能找到這個檔案及其標頭檔。
g++ -o calcServer calc.cpp soapC.cpp soapServer.cpp stdsoap2.cpp
|
一個以Web Servers形式提供的C API誕生了。
在server端執行./calcServer
下面討論如何用Java1.6的內建工具產生一個用戶端stub:
把gSOAP產生的WSDL拷貝到我們的Java開發環境中來,按照Web Services Server中定義的連接埠和伺服器,配置參數產生用戶端Web Services代碼:
/usr/lib/jvm/jdk1.6.0_03/bin/wsimport -extension -httpproxy:localhost:9999 -verbose ns.wsdl
|
產生後,把這個環境添加到eclipse的編譯環境中來,然後在eclipse中建一個新的類:
class Test { public static void main(String args[]) { Service service = new Service(); double h = service.getService().sub(20000, 1); System.out.println(h); } }
|
運行後,得到結果19999.0。
總結:當整合Java和C兩種平台時,我們可以有多種解決方案,但首先我們應該想到gSOAP因為它能夠很出色地完成任務。