2019年9月1日 星期日

建置DataSnap Client Server (3之2)



緣起

在前文裡面,我們介紹了用Delphi XE 10.3製作傳統Delphi當中相當於Midas位階的DataSnap Client Server程式製作。在該篇文章裡面,用的是以DataSnap Server直接套用在程式上,以二進位的資料作為傳輸內容,建立Client/Server的資料庫資料架構。
但在許多網路環境的建構上,越來越嚴謹,防火牆甚至連通過Port的資料交換格式、通訊協定內容都要檢驗,也讓傳統以二進位為交換資料的Midas跟DataSnap在建置、佈署上面常常會遇到網管的質疑與額外要求。

因此,幾年前Embarcadero在Delphi XE的DataSnap元件中,也搭配WebBroker元件協作,讓我們無需額外作什麼處理,就可以用JSON資料回傳DataSnap的所有資料。如果您已經看過本系列文章的第一篇:建置DataSnap Client Server (3之1),就會發現,從第一篇的文章進階到本篇,好簡單啊,只有一兩個Client端的元件要修改,其他的操作幾乎沒有什麼改變。

用FireDAC搭配DataSnap RESTful格式製作Client Server程式

在前一篇文章裡面,我們用二進位的格式來做為傳輸的資料格式,這第二篇,我們要使用RESTful格式來傳輸,也就是以JSON格式來傳輸資料。
看到這裡,千萬不要覺得擔心,其實我們不用多作什麼事情,不用像以前寫CGI的時候得凡事自己處理,看下去,你會覺得跟第一篇好像都一樣……

建立空殼DataSnap WebBroker Application專案

首先我們建立一個DataSnap WebBroker Application專案,以下用圖片來展示:







我同時抓前一篇的第二步驟來做比較,可以發現,前一篇的選項是三種,本篇的WebBroker Application是四種,第一跟第四種都是製作成WebServer的外掛,在這裡,我們先選擇第三種,也就是Stand-alone GUI application,做成單獨的一支Server程式,這樣比較容易進行比較,下一篇我們就會做成ISAPI dynamic link library,就可以掛在IIS上面執行了。
 
第四步驟跟第一篇的第四步驟不同,直接要我們設定用來提供服務的Port號碼,在這個範例中我用預設的8080,也透過Test Port檢測看看該Port是否已經被佔用,從下圖可以看出,8080 Port是可以用的,接著,就可以進行更細節的設定了。



















步驟五跟第一篇的設定差不多,我也是多勾選了Server Module,讓Delphi幫我們建立出ServerModule的單元檔,簡化自己手作的步驟:
第六步驟跟第一篇文章設定相同: Server Method是我們要實作各種DataSnap開放給Client使用的API所宣告的地方,因為我們要在這裡連接到SQL Server,所以我選擇TDSServerModule。但選擇TDataModule或者TDSServerModule差別不大,都是要在裡面放置與資料庫Server連線用的TFDConnection等連線元件,所以我直接就選了TDSServerModule:
到這個步驟,點選Finish以後,專案就會建立完成,在Delphi環境裡面,會有三個檔案,分別是專案主畫面、ServerMethodsUnit1.pas跟ServerContainerUnit1.pas,我把專案儲存為TestDSWebBrokerServer,所以右上角的專案內容,看起來就會變成下圖所示這樣:

這個時候如果點選ServerMethodUni1.pas,切到Design畫面,會看見視窗上什麼元件也沒有:










ServerContainerUnit1.pas當中,Design畫面則是自動被放了一些元件上去,可以看的出來,WebBroker的專案中,ServerContainerUnit裡面比第一篇文章的範例少了DSTCPServerTransport1 跟 DSHTTPService1這兩個元件,本篇的ServerContainerUnit1:











第一篇文章的ServerConatinerUnit1:















主畫面FormUnit1.pas畫面上則是被新增了啟動、停止、以及Open Browser這幾個按鈕:











在這次的範例中,由於使用的是WebBroker應用程式專案,就比第一篇文章的範例多了一個名為WebModuleUnit1的單元檔,其畫面中只有一個DSHTTPWebDispatcher元件,基本上我沒對這些元件作任何調整,都是預設的設定值。











執行TestDSWebBrokerServe.exe,點選當中的Open Browser按鈕,可以看到Server程式是有回應資料的:

















8080 port回應了DataSnap Server這段文字,可以看的出是有在運作的。在原廠的文件中,有提到要存取專案中的API,要結合 Host, Port, DSContext, RESTContext 這幾個屬性的設定值:

http://伺服器Host設定:port號碼/DSContext設定值/RESTContent設定值/ServerModuleUnit的類別名稱/方法名稱/參數1/參數2.......
既然這是RESRful API,在製作Client端程式之前,我們可以先用瀏覽器來處理,在範例中,我們用FireFox作為瀏覽器。一般在DataSnap Server程式中,都會預設建立兩個方法:EchoString跟ReverseString,我們就來用FireFox測試一下當中的ReverseString吧,我用FireFox瀏覽以下這個網址:
http://localhost:8080/datasnap/rest/TServerMethod1/ReverseString/Test1

依照上面介紹過的規則,這是呼叫TServerMethod1.ReverseString(‘Test1’),回傳的結果應該是1tseT這個字串,瀏覽器的截圖如下:


















檢視原始碼,截圖畫面則是:

















可以看得出來,執行結果是以JSON編碼的,這樣就能穿透防火牆了,接著,我們就先來製作Client端,再進行功能擴增的作業吧。

 建立DataSnap WebBroker Client程式

首先建立一個空的VCL Application專案,我把它命名為TestDSWebBrokerClient,跟二進位格式的DataSnap客戶端不同的是,我們Client主畫面上放置的元件,改為一個TDSRESTConnection元件,設定好它的Host, Port, Context, RESRTContent屬性即可:

















 接著,先把剛剛製作好的TestDSWebBrokerServer從檔案總管裡面執行起來,再到畫面上用滑鼠右鍵點擊DSRESTConnection1元件,點選其中的Generate DataSnap client class項目:

















 點選這個項目後,就會建立出一個新的單元檔,當中會宣告與Server連線的新Class,這是開發環境直接跟已經執行起來的Server程式索取資料,即時產生的Class,我們就可以用這個Class直接呼叫Server端的Method了。

因為在第一篇介紹當中,我們已經介紹過基本的方法,所以在這裡我們直接進入SQL指令的實作了,我們在Server端加入資料庫連線,提供兩個方法作為範例,一個是讓Client端傳SQL指令進行查詢,然後Server端把查詢到的資料筆數轉為字串回傳。另一個則是讓Client端傳 select SQL指令,讓Server把查詢到的資料以Table的形式回傳到Client端,讓Client端可以透過MemoryTable讀取資料內容。

擴增Server端功能- getRecordCount

第一篇文章當中,我提到了先使用MySQL做個簡單的DemoDB,作為範例之用,我把MySQL資料庫放在10.10.10.1的IP上頭,設定好一個名為DemoDB的資料庫,並且設定好一個使用者帳號,名為DemoDB,讓它可以接受10.10.10.1連線,並且在上頭新增了四筆資料:

















接著,在Server端的ServerMethodUnit1裡頭,我加了TFDConnection元件,把它的驅動程式設定為MySQL,另在DataModule裡面加入TFDPhysMySQLDriverLink, TFDSchemaAdapter, TFDQuery, TFDStanStorageBinLink這幾個元件,加入以後,畫面如下圖所示:











接著開始設定。首先,把FDConnection設定好,如下畫面中所示:

















接著,把FDQuery1的Connection屬性設定為FDConnection1,這樣一來,FDQuery1的SQL指令就可以透過FDConnection1來執行,送到MySQL伺服器去,而查詢的結果,也會經由FDConnection1回傳到FDQuery1裡面來。

FDQuery1的SchemaAdapter屬性,則用下拉選單設定為FDSchemaAdapter1,這是為了等下要轉換成其他格式回傳給Client之用。

設定好了,我們就可以新增getRecordCount這個新方法了,這步驟就純粹是程式碼編輯囉,先在TServerMethods1類別裡面新增這行宣告:
function getRecordCount(SQLCmd: String): string;

然後按下Ctrl+Shift+C,讓Delphi開發環境自動幫我們建立對應的程式碼,也就是一個空殼的方法實作:
function TServerMethods1.getRecordCount(SQLCmd: String): string;
begin
end;
做好空殼方法之後,我們先重新編譯TestDSWebBrokerServer,並且從檔案總管執行它,再重複前面提到過的『再到畫面上用滑鼠右鍵點擊DSRESTConnection1元件,點選其中的Generate DataSnap client class項目』,執行完成後,剛剛Server新增的這個函式就會被新增到Client端的ClientClassesUnit1.Pas裡面了:

function TServerMethods1Client.getRecordCount(SQLCmd: string; const ARequestFilter: string): string;
begin
  if FgetRecordCountCommand = nil then
  begin
    FgetRecordCountCommand := FConnection.CreateCommand;
    FgetRecordCountCommand.RequestType := 'GET';
    FgetRecordCountCommand.Text := 'TServerMethods1.getRecordCount';
    FgetRecordCountCommand.Prepare(TServerMethods1_getRecordCount);
  end;
  FgetRecordCountCommand.Parameters[0].Value.SetWideString(SQLCmd);
  FgetRecordCountCommand.Execute(ARequestFilter);
  Result := FgetRecordCountCommand.Parameters[1].Value.GetWideString;
end;

這些自動產生的程式碼我們不用處理,接著回頭關掉Server程式、來實作程式碼吧,這些程式碼跟第一篇文章的範例都一樣。
在Server端,我們的getRecordCount有個參數,就是要執行的SQL指令,所以完整的實作如下:
function TServerMethods1.getRecordCount(SQLCmd: String): string;
begin
   self.FDQuery1.Active := False;
   try
      self.FDQuery1.SQL.Text := SQLCmd;
      self.FDQuery1.Active := True;
      Result := IntToStr(self.FDQuery1.RecordCount);
   finally
      self.FDQuery1.Active := False;
   end;
end;

直接執行傳來的SQL指令,執行後,把FDQuery1.RecordCount轉為字串回傳,執行的畫面如下:


Client端的實作,則是放在按鈕 btnGetRecordCount 的Event Handler裡面:
procedure TForm2.btnGetRecordCountClick(Sender: TObject);
var
   Server: TObject;
   retStr: String;
begin
   try
      Server := TServerMethods1Client.Create(DSRestConnection1);
      retStr := TServerMethods1Client(Server)
          .getRecordCount('select * from testRec');
      ShowMessage(retStr);
   finally
      Server.Free;
   end;
end;
所以執行的畫面,就如上面的截圖,出現了4, 因為我只在資料庫裡面建立了四筆資料,所以 select * from testRec 查詢執行後,取得了四筆資料,回傳4這個字串。


擴增Server端功能- StreamGet

接著,我們要把查詢到的資料回傳給Client,由於回傳的資料我們要用Serialization(序列化)處理後,直接回傳到Client,所以就直接回傳TStream,Client端接到了資料,再把Serialization資料轉回為放在記憶體裡面的TTable(為了處理這樣的需求,FireDAC元件當中新增了TFDMemTable這個元件用來處理這樣的資料)。
首先仍然要先做出Server端的空殼方法:
function StreamGetSQL(SqlCmd: String): TStream;

然後重新編譯Server程式、從檔案總管執行它、再重複Client端的步驟『再到畫面上用滑鼠右鍵點擊DSRESTConnection1元件,點選其中的Generate DataSnap client class項目』,執行完成後,剛剛Server新增的這個函式就會被新增到Client端的ClientClassUnit1.Pas裡面了:
function TServerMethods1Client.StreamGetSQL(SQLCmd: string; const ARequestFilter: string): TStream;
begin
  if FStreamGetSQLCommand = nil then
  begin
    FStreamGetSQLCommand := FConnection.CreateCommand;
    FStreamGetSQLCommand.RequestType := 'GET';
    FStreamGetSQLCommand.Text := 'TServerMethods1.StreamGetSQL';
    FStreamGetSQLCommand.Prepare(TServerMethods1_StreamGetSQL);
  end;
  FStreamGetSQLCommand.Parameters[0].Value.SetWideString(SQLCmd);
  FStreamGetSQLCommand.Execute(ARequestFilter);
  Result := FStreamGetSQLCommand.Parameters[1].Value.GetStream(FInstanceOwner);
end;
Client端需要的元件跟第一篇範例一樣,包含了TFDSchemaAdapter, TFDMemTable, TFDGUIxWaitCursor, TFDStanStorageBinLink, TFDTableAdapter 這些元件,這些元件各有其特殊的用途,我們來看一下:
    FDSchemaAdapter1: TFDSchemaAdapter; => 把Stream資料轉為物件
    FDMemTable1: TFDMemTable; => 儲存轉換完成的Table資料
    FDGUIxWaitCursor1: TFDGUIxWaitCursor; => 等待 Server 端連線完成的等待作業
    FDStanStorageBinLink1: TFDStanStorageBinLink; => FDSchemaAdapter轉換二進位資料時,用來處理資料格式的物件。
    FDTableAdapter1: TFDTableAdapter; => 連結 FDSchemaAdapter1 跟 FDMemTable的中介角色,它裡面的DatSTableName必須設定為回傳的TableName,回傳的Table才能正確的被儲存到FDMemTable1裡面去。

這些基本設定完成後,就可以把Client端按鈕的Event Handler寫上去了:
procedure TForm2.btnGetQueryClick(Sender: TObject);
var
   Server: TObject;
   LMemStream: TMemoryStream;
begin
   Server := TServerMethods1Client.Create(DSRestConnection1);
   LMemStream := CopyStream(TServerMethods1Client(Server).StreamGetSQL('select * from testRec'));
   try
      if LMemStream <> nil then begin
         LMemStream.Position := 0;
         self.FDSchemaAdapter1.LoadFromStream(LMemStream,
             TFDStorageFormat.sfBinary);

         self.FDMemTable1.First;
         ShowMessage(self.FDMemTable1.FieldByName('name').AsString);
      end;
   finally
      LMemStream.Free;
      Server.Free;
   end;
end;
這裡面的CopyStream,是Delphi範例裡面的程式碼,我覺得用起來沒問題,所以就Copy來繼續用了。
上面這段程式,是我抓了第一筆資料裡面的name欄位來顯示的,想不起來資料長怎樣嗎?我們回頭看一下MySQL裡面建好的資料:


第一筆資料的name是Apple,所以執行結果也會顯示Apple:

取回的FDMemTable1資料是完整的,所以呼叫Next,就可以依序取得其他各筆的資料。要記得:FDTableAdapter1.DatSTableName得要回傳跟 SQL指令裡面的Table名字一致,才不會在執行過程中發生錯誤喔。

總結本篇

在這一篇的介紹中,我們是直接製作了『DataSnap WebBroker Application』,用Delphi WebBroker應用程式,先用VCL應用程式的形式來製作,在第三篇裡面,我們會把完全相同的方法,改為透過ISAPI,以IIS為伺服器來提供服務。

第二篇跟第三篇會很像,但不同的地方會是透過ISAPI的時候,效能以及網址規則都會與本篇的內容有所不同,在第三篇裡面,我會透過JMeter模擬相同數量的使用者,以相同數量的Thread來索取服務,因為資料庫相同、Client索取資料的作法相同,控制了變因之後,我們就可以比較IIS跟Delphi製作的exe提供Web連線的效能,也可以作為各位讀者在選擇架構時的參考了。

當然,一如過去的幾篇文章,我也把範例程式碼完整的放在這裡,您可以下載回去看看我實作的簡單範例。

2 則留言:

  1. 之前在找資料時,有看到用JSON的效能會比二進位差蠻多的,二進位效能比較好,另外有將WebBroker的Indy改用http.sys來執行的方式,像獨立執行檔,又有掛在IIS的效果,不曉得在下一篇能不能也看到用http.sys連線效能的比較。

    我用的是SynBroker c5soft@189.cn Version 0.9.1.0 2018-6-2

    回覆刪除
    回覆
    1. 二進位的效率一定比較好的, 因為相同的資料, binary data不用再變成 HEX, 一個 byte 變成 2 個 char, 光資料大小就變成兩倍了, 處理上、傳輸上都花了成倍的時間.

      但是考量到防火牆的因素, 二進位的 Datasnap 在只開放 port 80, 443, 更甚者偵測傳輸協定的 Layer 3 Gateway, 更是完全擋死了 DataSnap 的 binary 傳輸途徑。

      所以寄生在 WebServer 上, 至少還能維持 DataSnap 功能正常.

      Embarcadero 從 Delphi XE 10 開始, 已經把原本名為 EMS Server 的新作法改名為 RAD Server, 據說會比 DataSnap 提供給 Apache, IIS 的外掛效率更好, 只是 RAD Server 的成本也不低, 真的還是會讓開發人員考慮很久啊.....

      刪除