2022年7月26日 星期二

Encrypt in Delphi, Decrypt in Laravel

Delphi provided cross-platform application ability from Delphi XE2, that happened in 2011 or 2012. With the feature, all the applications created by Delphi needs to communicate with client applications or server applications created by various IDE or technologies. RESTful API is the most frequently channel.

Although Delphi deliver package bundled Indy from Delphi 2005, and Indy did provided wonderful internet protocol capability, Indy did not provide the capability for encryption and decryption. The security protocol of Indy comes with OpenSSL, which is almost best security library in the world from early 1990 era.Even OpenSSL is so great, it's still difficult to deliver OpenSSL to various operating systems. For example, iOS, Linux. 

For solving the issue about deliver SSL/TLS secure channel to iOS/Linux, Embarcadero created NET package to provide secure channel feature in FireMonkey framework since XE5 or XE6. Developers can use NET package for RESTful API communication without any overheads. What we developers need to do is to create application, package it with the request by Apple/Linux platform, no more extra library issues happen.

Howerver, it's not so easy for encryption and decryption. In FireMonkey, all secure channel features depend on the platform native solutions. In macOS, Delphi used mac safari libraries for SSL/TLS. In iOS, Delphi used iOS native framework for SSL/TLS. And Delphi used Windows/Linux native API in Windows/Linux platform. What can we do to provide encrypt/decrypt feature?

I used to work with Delphi, Xcode, Android studio for creating client application, and used to create server side applications with PHP, and Laravel/PHPStorm in recent years. I do know that Laravel framework provide strong capability, and Apache will provide standard SSL/TLS channel, but I still wish to encrypt some sensitive content in the secure channel, and I believe some of you do think so.

And I tried to pick a popular symmetric encryption, AES is my choice. As we know, all encryption support various key length and block process mode. My choice is AES-256-CBC, which means AES with 256 bits length key and CBC mode.

For this choice, I need to solve 2 issues: 

  1. Picking a encryption/decryption library which supports Delphi, and no overhead in delivery package.
  2. The created cipher should be decrypted by Laravel.

Encryption package for Delphi

I studied information security in graduated school, and my master thesis is about CA and personal certificate management in 1998. Encryption is not big deal for me. It's a huge deal for me to create all the encryption algorithms by myself. Thank God, we have so many giants on the internet, and we need to stand on giant's shoulder, rather than create wheel from zero.

For this thought, I searched the key word "AES-256-CBC" from github in recent weeks, and so many relative projects pop-up, including LockBox-2, LockBox-3, and Delphi Encryption Compendium, all 3 projects can be found in Delphi GetIt package manager GUI.

After testing LockBox-3 and Delphi Encryption Compendium. I chose Delphi Encryption Compendium (I will used DEC for it in later article) as my further test candidate because I cannot get enough example or document for LockBox-3, and I got enough sample projects in DEC for FireMonkey with Console/GUI.

As we know, Key, initial vector (iv for short in later article), and padding byte are 3 fundmental factors for symmetric encryption. And 4th factor might be the encoding format for cipher. I checked that basic encryption/decryption feature works, and then I tried to modify the parameter/format/iv generation in the DEC sample project.

First of all, the encoding format. The DEC sample project requires HEX format key and iv, and HEX is the only one format it supports. The sample project allowed us to use any custom padding byte, it's very flexible for developer, and it's good for DEC.

But it's very hard to understand for developers who are not familiar to encryption/decryption.

I had chosen Laravel as server side application, hence I just try to make the communication between DEC and Laravel at this moment.

The profile of encrypt decrypt, encryptstring, decryptstring in Laravel

With XDebug and PHPStorm, I created a test project to trace into encrypt, encryption, decrypt, decryptstring function. Both of the encrypt functions invoke the openssl_encrypt function, and both of the decrypt functions invoke the openssl_decrypt function.

The 2 functions encrypt and encryptstring are the basically identical, encryptstirng will pack the string as a JSON Payload before invoke encrypt, and the reverse sequence happened in decryptstring and decrypt.

When you invoke encrypt/encryptstring, a random iv will be created, and the length of the iv will be different with the encrypt algorithm and key length. For example, AES-256-CBC will generate 16 bytes iv, and the OpenSSL library function will generate the 16 bytes iv randomly, encode it, combine in a JSON payload, and return it.

The result will be the following JSON payload:

{

    "iv":"KSMkCBozRhR3Lxl3MF4OHg==",

    "value":"Cw025jAKF0TLcjxIJ30Vzg==",

    "mac":"e414202d1ae168315bea8d5d68a4e8bc1de22c299f1dd50940e1c8419565d710",

    "tag":""

}

In the above JSON payload, there are 4 elements:

iv: generated by OpenSSL library, and encoded by base64 format.

value: the cipher, encoded by base64 format.

mac: SHA256.hmac(iv + value).base64String // just psudo code, generate hmac by (iv+value), and encoded it with base64.

tag: empty string. 

If the payload does not match above structure, decryption will be failed. Another important factor is padding byte, padding byte MUST following a rule. The "mac" field in above structure is stand for Message Authentic Code, which is generated by SHA256 hmac, the source date is iv contact value, take above data as example, the source string is: 

Cw025jAKF0TLcjxIJ30Vzg==KSMkCBozRhR3Lxl3MF4OHg==

SHA256 hmac requires a key to protect the source, and we will use the encrypt key as the key again. Howerver, DEC does not provide hmac function, hence we need to use the System.Hash.THashSHA2 in FireMonkey. it's a class function of THashSHA2 class, you need to invoke it as following way:

    hMacSrcString := base64IV + base64Cipher;
    hMacResult := THashSHA2.GetHMAC(hMacSrcString,
          TFormat_Base64.Decode(base64Key));

Particular Padding Byte in OpenSSL

In all symmetric encryption algorithms, padding is an important issue. Including DES, 3DES, RC4, RC5, AES, padding is an issue which could not be ignored.

That's caused by the encryption algorithms used block calculation with various block process modes (ECB, CBC, OFB, CFB), all data will be divided into blocks as same size, calculated with key to generate cipher blocks, and process these cipher blocks as completed cipher.

The padding issue will happen in last block, which might be shorter or equal to the block size. If it's shorter, padding byte will be padded in the tail, to make sure the last block size matches the defined block size.

Taking AES-256-CBC as example, the key size is 256 bits (32 bytes), and both iv and block size are 16 bytes. If last block length is less than 16 bytes, we need to pad it to 16 bytes with padding byte.

I mentioned that DEC opens all parameters for developers to config, it's flexible. As we need to make DEC production be decrypted by Laravel decryptstring, we need to figure out how openssl_decrypt handle the padding issue.

I tried every combination from DEC and Laravel, and one conclusion was found: the padding byte value will change with the last block length. If we need to pad 1 byte for the last block, the padding byte should be 0x1, please refer the following table:

Plain Data length
Padding size
Padding Byte value
1 byte or N* 16 + 1 bytes15 byte0xF
2 bytes or N* 16 + 2 bytes14 bytes0xE
3 bytes or N* 16 + 3 bytes13 bytes0xD
4 bytes or N* 16 + 4 bytes12 bytes0xC
5 bytes or N* 16 + 5 bytes11 bytes0xB
6 bytes or N* 16 + 6 bytes10 bytes0xA
7 bytes or N* 16 + 7 bytes9 bytes0x9
8 bytes or N* 16 + 8 bytes8 bytes0x8
9 bytes or N* 16 + 9 bytes7 bytes0x7
10 bytes or N* 16 + 10 bytes6 bytes0x6
11 bytes or N* 16 + 11 bytes5 bytes0x5
12 bytes or N* 16 + 12 bytes4 bytes0x4
13 bytes or N* 16 + 13 bytes3 bytes0x3
14 bytes or N* 16 + 14 bytes2 bytes0x2
15 bytes or N* 16 + 15 bytes1 bytes0x1

If you did not follow the rule in above table, the openssl_decrypt will return failure.

Conclusion

It's very frequently to use Delphi client to communicate with other server technologies. I share this article to make sure every Delphi developer not take time to find a FireMonkey way to do so.

Laravel or any other server side technologies adopt OpenSSL very frequently, so if you check this article out, and use the sample with your Delphi project, it will be very useful.

The sample code is uploaded to github, you can use it as free, I shared the project with Apache License 2.0.


用 Delphi 做 AES 加密, 在 Laravel 裡面解密

從 2011 年開始,Delphi XE2 提供了跨平台的開發能力之後,Delphi 需要跟各種平台溝通,就是日常會發生的事情。雖然 Delphi 從 Delphi 2005 開始,已經把 Indy 作為與 Delphi 或者 RAD Studio 一起預設安裝的網路套件,但對於加密、解密的能力,Indy 仍舊是使用外部函式庫來提供的,而且全世界很多平台對於加解密都是使用 OpenSSL 來製作的。

OpenSSL 是很優秀的函式庫,Indy 也是,但跨平台的時候,要把 OpenSSL 佈建到不同平台上就是一個很辛苦的事情,例如要把 OpenSSL 佈建到 iOS 上面,就不是一件簡單的事情。為了這個不簡單,Delphi 從 XE5 還是 XE6 的版本開始,自己在 FireMonkey 上面開發了一個 NET 套件,當中支援了 HTTPS 的各種加密模式 (SSLv2, SSLv3, TLS 1.0-1.3),通通都從 Delphi 的層級重寫起,這樣在佈建的時候就不用苦惱。但加密演算法就沒有這麼單純了!

我自己的慣用工具,Client 端是 Delphi, Xcode, Android Studio, Server 端這幾年則是 Laravel 7.x-8.x, 主要是以 PHP 為主。概念上是以 Laravel 做 API Server, 跟 Client 端的程式做溝通。雖然 Laravel 透過 HTTPS 也能提供一定的安全性,但我還是想把傳輸過程中一些比較敏感的資料額外加密一下。

而這幾年比較流行、又相對安全一點的加密演算法,就是 AES 了,我自己選擇的是 AES-256-CBC 這個組合,也就是 AES 加密法,金鑰用 256 bits,以 CBC 模式處理多重區塊加密。

因此,就有兩個要面對的課題:1. Client 端要找到能支援這個組合的套件,而且最好是免費的。2. 加密的結果要能夠讓 Laravel 順利解密。

支援 FireMonkey 的加密套件

對於碩士時研究加密演算法與 CA 的我來說,看懂各種加密演算法、參數與使用的方式不會造成困擾,但要我把每一種加密演算法從 OpenSSL 裡面分離出來重製一次,就很煩了。這個網際網路的年代裡,網路上有很多巨人,我們只要能站在巨人的肩膀上,很多事情就可以不用重頭做起。

最近在 GitHub 裡面,我用 AES-256-CBC 跟 Delphi 作為關鍵字,找到了很多的套件,包含 LockBox-2, LockBox-3, Delphi Encryption Compendium 這幾組可以從 Delphi 的 GitIt 套件管理員當中取得的工具。

測試了LockBox-3跟 Delphi Encryption Compendium 這兩組之後,我選擇了 Delphi Encryption Compendium (後文中簡稱 DEC) 來做測試,主要是因為 LockBox-3 的範例不多,而 DEC 支援 FireMonkey 之外,也提供了各種平台的 Console 跟 GUI 範例程式。

在加密工具的使用上,Key, IV, Padding 是三個基本又最重要的元素,第四個元素則是原文跟密文的編碼格式。確認 DEC 本身的加密、解密功能沒問題之後,我就開始著手改寫 DEC 範例程式中的各種功能。

首先是編碼格式,在 DEC 範例程式中,只提供了 HEX 格式的 Key 跟 IV, 也提供開發人員自訂的 Padding Byte (範例程式中稱為 FillerByte),這是一個靈活度超大的工具,但對於只熟悉 Delphi,不熟悉加密解密的開發人員來說,不啻於無字天書。 

但既然我的目標平台是 Laravel,那當然是先追求 DEC 跟 Laravel 之間的互通了。

Laravel 的 encrypt, encryptstring, decrypt, decryptstring 特性

透過 PHPStorm 的 Debug 功能,以 XDebug 進行 break point 的設定之後,我把 Laravel 的 encrypt, encryptstring 深入了解了一下,發現這兩個 function 最後都是呼叫了 OpenSSL 函式庫裡面的 openssl_encrypt 函式,差異只在於是否把加密結果作為物件對待,但我其實只需要對字串加密,所以直接針對 encryptstring 跟 decryptstring 進行了解。

在呼叫了這個 encryptstring 函式之後,會先隨機建立 Initial Vector(後文中簡稱 iv),iv的長度會根據 AES 金鑰的長度而有差異,256 bits 的 AES 加密演算法,其 iv 的長度是 16 bytes,而 OpenSSL 函式庫會隨機產生這 16 bytes 的 iv,並將之回傳,在 Laravel 當中,加密作業完成後,會把 iv, 加密結果這兩個 base64 的編碼字串連結起來,再用 SHA256 d搭配 AES 的 Key 製作 hmac 驗證碼,最終會把這三個值跟一個名為 tag 的空字串做成一個 JSON 格式的物件,再把這個物件做一次 base64 編碼,JSON 物件的格式如下:

{

    "iv":"KSMkCBozRhR3Lxl3MF4OHg==",

    "value":"Cw025jAKF0TLcjxIJ30Vzg==",

    "mac":"e414202d1ae168315bea8d5d68a4e8bc1de22c299f1dd50940e1c8419565d710",

    "tag":""

}

如果沒有做成這樣的結構,且 padding 字元沒有依照特定的規則,openssl_decrypt 在進行解密的時候就會發生解密失敗的問題。

上面提到的這個 mac, 是 Message Authentic Code 的縮寫,在 DEC 套件裡面並沒有提供,但在 FireMonkey 裡面的 System.Hash.THashSHA2 裡面有提供,只是做法有點特別,需要直接呼叫該類別的類別方法 GetHMAC:

    hMacSrcString := base64IV + base64Cipher;
    hMacResult := THashSHA2.GetHMAC(hMacSrcString,
          TFormat_Base64.Decode(base64Key));

特別的 Padding Byte (補位元)

在所有的加密演算法當中,幾乎所有的對稱金鑰加密演算法(Symmetric Encryption Algorithm) 都需要處理補位位元的問題。從早期美國限制出口的 DES, 3DES, 到廣泛使用在各種網路資料交換的 RC4, RC5, 目前最流行的 AES, 全都有這個問題需要處理。

這個問題的由來,是因為這些加密演算法的計算基準都是以單一區塊進行加密,再透過幾種不同的區塊處理模式 (ECB, CBC, OFB, CFB......) 把這些密文區塊做各種不同的處理之後,製作出原始資料的完整密文。而這些區塊的加密,在各種演算法中都有獨特的位元處理,因此需要每個區塊有固定的大小。

Padding的問題,會發生在原始資料的最後一個區塊。以 AES-256-CBC 這個組合來看,它使用 AES 加密演算法,金鑰長度是 256 bits,也就是 32 bytes,以CBC模式來處理每個加密區塊。這個組合的區塊長度是 16 bytes,所以當原始資料的最後一個區塊長度不到 16 bytes 時,就要把原始資料補足到 16 bytes。

前面提過 DEC 開放了所有的參數讓開發人員可以自行設定,這是非常靈活開放的函式庫做法,但要跟 Laravel 所使用的 OpenSSL 當中的 openssl_decrypt 函式能完整互通,就得先知道 openssl_encrypt 對這問題的處理模式才行。

我做了許多測試,發現 openssl_encrypt 所建立的 AES-256-CBC 密文,補位用的資料會隨著最後這個區塊資料的長度改變,所以如果單純的使用同一個位元資料來做補位,怎麼樣都不可能跟 Laravel 的 decryptstring 共通的。我找出了當中的規則,並且已經一一測試過,完全沒有問題了,規則如下表:

原始資料長度需補位長度必須使用的補位位元內容
1 byte 或者 N* 16 + 1 bytes15 byte0xF
2 bytes 或者 N* 16 + 2 bytes14 bytes0xE
3 bytes 或者 N* 16 + 3 bytes13 bytes0xD
4 bytes 或者 N* 16 + 4 bytes12 bytes0xC
5 bytes 或者 N* 16 + 5 bytes11 bytes0xB
6 bytes 或者 N* 16 + 6 bytes10 bytes0xA
7 bytes 或者 N* 16 + 7 bytes9 bytes0x9
8 bytes 或者 N* 16 + 8 bytes8 bytes0x8
9 bytes 或者 N* 16 + 9 bytes7 bytes0x7
10 bytes 或者 N* 16 + 10 bytes6 bytes0x6
11 bytes 或者 N* 16 + 11 bytes5 bytes0x5
12 bytes 或者 N* 16 + 12 bytes4 bytes0x4
13 bytes 或者 N* 16 + 13 bytes3 bytes0x3
14 bytes 或者 N* 16 + 14 bytes2 bytes0x2
15 bytes 或者 N* 16 + 15 bytes1 bytes0x1

如果補位規則沒有依照上面這個表格所列,openssl_decrypt 的解密會回報錯誤。

結語

Delphi 在各種作業系統中需要與其他各式各樣的 Server 端程式溝通已經是日常事務了,本篇的分享主要是希望讓大家在使用 Laravel 作為 Server 端程式,或者是需要使用 openssl 的 openssl_decript 在 Server 端解密時,可以順利的使用 Delphi 在 Client 端程式迅速的完成程式開發,希望大家會喜歡,這個專案我也放在 GitHub 上面了,請放心使用,如果有遇到什麼問題,也歡迎一起討論或者回覆給我。


2022年7月13日 星期三

Delphi 是否還應該保留對Goto/Label, With 的支援?

 Pascal 時代的遺產

熟悉或者使用了 Delphi 一小段時間的開發人員,都知道 Delphi 的語言是 Object Pascal,而 Object Pascal 是從傳統的 Pascal 演進而來的,這個歷史可能得回溯到 1993-1995 之間,Turbo Pascal 6, Turbo Pascal 7, 以及 Delphi 誕生的那個時代,提到那個年代,就不可避免的必須提到 Anders Hejlsberg 這位大神的貢獻,在 Borland,他改進了 Turbo Pascal 6.0 到 Delphi 7 之間的所有記憶體管理功能,之後轉戰微軟創造了 C# 程式語言與 .NET 平台,最近這幾年又創造了 TypeScript,如果過去三十年當中必須選擇一位我最崇拜的技術人員,非 Anders Hejlsberg 莫屬。

嘮叨了一下簡單的歷史,在 Pascal 時代延續到 Delphi 的 Object Pascal 語法,到目前的 Dephi 11 Alexandria,即使 Delphi 已經加入對於 Windows/macOS/iOS/Android/Linux 各種平台的支援,也加入了 Parallel (平行處理)  的功能,讓多核心的系統能真正發揮最大效能,但 Pascal 時代的遺產還是一直存在。

雖然對於傳統 Pascal 的語法支援可以讓大多數熟悉傳統 Pascal 的開發人員更快上手 Delphi,但是這些語法在新支援的功能中還是有些風險的。讓我們一起來看一下當中的問題。

絕對不要使用的語法:GOTO/LABEL

從 1998 年開始,筆者在攻讀博士的時候,也開始在學校兼課,在所有的程式開發課程中,只要開始講到 LOOP, Branch, Function, 都不免會講到這個最古老的語法:GOTO + LABEL,但筆者也一定會跟課堂的所有學生說『這個語法,禁用!在我課堂上的所有作業中,出現GOTO語法,一律 0 分』

GOTO語法在筆者唸高中剛學 BASIC 語言的時候,是不得不使用的功能,畢竟當時的作業系統是單工的DOS,也沒有多核心、多線程、網路或其他週邊可能來干擾,用 GOTO 讓程式碼得到迴圈的功能無可厚非。

但一旦引入了副程式的概念之後,不管是在 BASIC 裡面的 sub, 還是 Pascal 裡面的 procedure/function 或者 C 的 function, 更可怕的是在 OOP 當中的 Method,都有維護、管理變數、屬性生命週期的議題需要注意,即使不用 GOTO 這個惡魔語法,一個不慎都可能讓整個程式 crash,如果有傻傻搞不清楚的開發人員或者開發學員,在 function A 裡面設定了個 LABEL,在另一個 function B 裡面直接 GOTO 到該 LABEL,下場就是程式直接巴比Q了。

現在還有更嚴重的情況,就是在 Class A 的 Method 當中寫了 Label,但從 Class B 的某個 Event Handler 用了 Goto 跳過去,如果這時候 Class A 根本不曾被建立出來,整個程式也是直接 GG..... 或者是在匿名函式當中做了 Label, 從其他 Method 中 Goto, 執行中的程式也是直接 Bye 了.

如果是簡單的工具程式,幫大家做做簡單的計算機或者文字編輯就算了,但如果是排版、圖片處理的應用程式,在輸入了很多資料,還沒存檔之前,突然直接 crash,絕對會讓使用者直接問候開發人員或者提供應用程式的人家裡的祖宗十八代。

所以,修過筆者開課的所有同學都會牢牢的記住『不可以用GOTO,不然一律0分』這個鐵律,也請大家絕對不要用它。只有組合語言是個例外,因為不用 jumper,就沒得玩了!

不建議使用的語法: With

在傳統 Pascal 當中,Record 跟 Packed Record 常被用來作為自訂型別的結構,由於當中可以把多種型別組合成一個新的型別,在還沒有OOP的那個年代,這個做法已經接近現代OOP的屬性概念(當然當時還沒演進到資料封裝的能力、也還不能包含Method的程式碼在 record 裡面,這些功能都在 Delphi 10.4, 11 的這幾個版本當中被加入了)。

為了方便程式碼的撰寫,傳統 Pascal 提供了 With 這個語法,透過 With,開發人員可以不用把完整的變數名稱每次都 key 出來,省下了不少的打字時間。但是有一長就有一短,在With 當中可以一次標明多個變數,如果這幾個變數當中,出現了同名的屬性,在編譯的時候可能會有語焉不詳(ambigution)的情況發生,而在後續的維護中,也可能讓接手的ㄎ發人員弄錯該程式碼的原意,這是很糟糕的情況。

結論

綜合以上的說明,筆者的想法是『GOTO/LABEL』應該要從Delphi 的 Object Pascal 語法中移除,連出現都不該出現。而 With 語法則是建議編譯時出現提示警告,再過一兩個版本就不支援。希望 Embarcadero 能盡早為了 Delphi 的產出程式更穩定而做出這個修改。

您也有想要建議 Embarcadero 移除掉的傳統 Pascal 語法嗎? 歡迎在底下留言,筆者在參加 Embarcadero MVP 會議的時候會幫大家提出的。

2019年9月6日 星期五

建置DataSnap Client Server (3之3)


緣起

在本系列的前面兩篇,第一篇裡頭介紹了使用Delphi製作原生執行檔、二進位資料進行傳輸,完成Client/Server的資料庫程式組,第二篇則是介紹了製作原生執行檔、RESTful格式製作Client/Server程式組,跟第一篇相較,資料內容從二進位改為JSON,傳輸的協定從原生的DataSnap傳輸,改為了HTTP,已經可以通過絕大多數防火牆的阻擋了。
在這第三篇裡面,沿用第二篇的傳輸協定、資料格式,但不再使用原生執行檔,改為製作出ISAPIdll,搭配WindowsIIS來運作,這次我們在Client/Server整組程式完成以後,再用Apache JMeter來實測同一連線、同一資料庫、同一電腦上的原生程式/IIS + ISAPI來驗證兩種Server作法的效能差異。
在本篇的範例中,差異比第二篇和第一篇的差異更小,Client端的使用,差異上只有一個設定要改,Server端和資料庫的連線功能,則是可以完全不改,直接適用。所以最近的幾個專案,我都是先製作了原生執行檔的Server,再作一個IIS ISAPI專案,這樣一來,要逐步偵錯的話,就可以從原生執行檔的專案中設定中斷點,省去了ISAPI專案偵錯的許多複雜步驟。

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


建立空殼DataSnap WebBroker Application (ISAPI)專案

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










當我們選擇專案類型(Project Type)ISAPI dynamic link library之後,原本的六個步驟就變成四個步驟了:
第三步驟直接就選擇Server features了,如果我們製作的是原生執行檔,第三步驟則是要選擇專案執行檔類型(VCL Application/FireMonkey Application/Console Application)了,在ISAPI的專案中,Server features只預設勾選了Server Method class,我依循前兩篇的作法,多勾一個Server Module選項:
 
最後一個步驟跟前兩篇文章設定相同: Server Method是我們要實作各種DataSnap開放給Client使用的API所宣告的地方,因為我們要在這裡連接到SQL Server,所以我選擇TDSServerModule。但選擇TDataModule或者TDSServerModule差別不大,都是要在裡面放置與資料庫Server連線用的TFDConnection等連線元件,所以在三篇文章中,我都是直接就選了TDSServerModule

到這個步驟,點選Finish以後,專案就會建立完成,在Delphi環境裡面,會有三個檔案,分別是專案主畫面、ServerMethodsUnit1.pasServerContainerUnit1.pas,我把專案儲存為TestDSWebBrokerAPI,所以右上角的專案內容,看起來就會變成下圖所示這樣:

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










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











第一篇文章的ServerConatinerUnit1:
















在本篇文章當中,就沒有主畫面了。


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











設定IIS,讓製作出來的dll能掛上去使用



這篇原廠的說明文件裡面,一步一步的說明了如何設定IIS,好讓ISAPI可以掛上去使用,所以我不再多加贅述,直接把我們的範例設定方法介紹給大家看:
1.    建立 ISAPI dll 要放置的目錄,並設定權限給 IIS 使用者
我在C碟底下建立了 DSISAPI這目錄,並在檔案總管裡面設定它的權限:

























資料夾的權限,要加入IIS_IUSRS這個使用者,不然IIS沒辦法存取到資料夾,以及放在資料夾裡面的檔案。
設定虛擬目錄,讓它連接到剛建立的這目錄:
 




















設定好虛擬目錄後,接著要設定專案的輸出目錄(output directory),讓DLL產出在剛剛建立好的這個虛擬目錄裡面:

 
接著就把專案編譯一下,在C:\DSISAPI這目錄裡面就生出了TestDSWebBrokerAPI.dll,我們就可以從瀏覽器直接瀏覽內容了,因為我們在建置專案的時候,有產生Sample Methods,所以還沒寫任何程式碼之前,也有EchoStringReverseString這兩個範例方法可以用,以下,我就用FireFox來瀏覽新產生的這個ISAPI裡面的ReverseString方法:
 
可以看見專案的DLL已經有反應了,我們先來看一下當中的 URL 吧。在原廠的文件中,有提到要存取專案中的API,要結合 Host, Port, DSContext, RESTContext 這幾個屬性的設定值:



http://伺服器Host設定:port號碼/DSContext設定值/RESTContent設定值/ServerModuleUnit的類別名稱/方法名稱/參數1/參數2.......
上面這個寫法,是原生執行檔的作法,但我們現在改用ISAPI
http://localhost/DSAPI/TestDSWebBrokerAPI.dll/datasnap/rest/TServerMethods1/ReverseString/Test
HostPort改為直接用IIS的設定,只是需要加入兩個元素: 虛擬目錄路徑跟DLL名字,所以就成了上面這個 URL
從截圖可以看得出來,執行結果是以JSON編碼的,這樣就能穿透防火牆了,接著,我們就先來製作Client端,再進行功能擴增的作業吧。

 建立DataSnap WebBroker Client程式

首先建立一個空的VCL Application專案,我把它命名為TestDSWebBrokerClient,跟二進位格式的DataSnap客戶端不同的是,我們Client主畫面上放置的元件,改為一個TDSRESTConnection元件,設定好它的Host, Port, Context, RESRTContent屬性即可:
這當中的UrlPath,就得填進我們在IIS裡面建立的虛擬目錄跟 DLL 的檔案名稱,在本範例中,虛擬目錄的名字是 DSISAPI (跟實體目錄不同名喔,請留意),檔案名稱則是TestDSWebBrokerAPI.dll,所以 URLPath 要填入 DSAPI/TestDSWebBrokerAPI.dll
在第二篇文章裡面,因為server端是原生exe檔,所以需要先把製作好的TestDSWebBrokerServer從檔案總管裡面執行起來,才能從Client更新API內容。但本篇是使用ISAPI,所以只需要確定IIS是在執行狀態,就可以切換到Client專案,到畫面上用滑鼠右鍵點擊DSRESTConnection1元件,點選其中的Generate DataSnap client class項目:















接下來的所有步驟,都跟第二篇文章當中的Client作法相同,我不更改文字,如果您還沒有很熟悉第二篇的作法,可以就著這些描述再溫習一下:
點選這個項目後,就會建立出一個新的單元檔,當中會宣告與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名字一致,才不會在執行過程中發生錯誤喔。


本篇當中,我新增了一個檔案上傳的功能,命名為postFileStream,第一個參數是要上傳的檔案,第二個參數則是要上傳的TStreamServer端直接接下這個TStream,把接到的Stream儲存到FileStream裡面就行了。
 

function TServerMethods1.postFileStream(Filename: String;
  FileStrm: TStream): String;
var
   filePath, fullFileName: String;
   LMemStream: TMemoryStream;
   newFileStrm: TFileStream;
begin
   fullFileName := System.IOUtils.Tpath.Combine('C:\DSISAPI', Filename);
   filePath := ExtractFilePath(fullFileName);

   if not DirectoryExists(filePath) then begin
      ForceDirectories(filePath);
   end;

   if FileExists(fullFileName) then begin
      DeleteFile(fullFileName);
   end;

   LMemStream := CopyStream(FileStrm);
   newFileStrm := TFileStream.Create(fullFileName, fmCreate);
   try
      LMemStream.Position := 0;
      newFileStrm.CopyFrom(LMemStream, LMemStream.Size);
      LMemStream.SetSize(0);
   finally
      newFileStrm.Free;
      LMemStream.Free;
   end;

   Result := 'file received';
end;

但是在ClientGenerate DataSnap client classes之後,Client端的功能裡頭,我發現有一個問題,就是在Client端的Destroy方法實作程式碼裡面,會造成Access Violation:
destructor TServerMethods1Client.Destroy;
begin
  FEchoStringCommand.DisposeOf;
  FReverseStringCommand.DisposeOf;
  FgetRecordCountCommand.DisposeOf;
  FStreamGetSQLCommand.DisposeOf;
  FStreamGetSQLCommand_Cache.DisposeOf;
  FpostFileStreamCommand.Parameters.RemoveParameters; //加入這一行,就可以避免AccessViolation .
  FpostFileStreamCommand.DisposeOf;
  inherited;
end;

DataSnap Server的效能比較

在多層式架構中,資料庫伺服器、應用程式伺服器,是當中兩項關鍵的節點。整體服務的效能,還牽涉到兩個服務器之間的網路效能、應用程式伺服器與客戶端的網路效能,以及客戶端的應用程式效能三大部分。
回頭想想,絕大多數的服務供應商,都有自己的資料庫伺服器,而應用程式伺服器與資料庫伺服器之間的網路,也不會因為提供多少客戶而有所變化,應用程式伺服器與客戶端之間的網路效能,可以透過更換ISP而改善。
因此,我們最該努力的,就是把應用程式伺服器的效能提升到極致,這樣一來,整體服務的品質就能提的更高。為了單純測試應用程式伺服器,這次的測試中,我們使用同一個資料庫伺服器、同一個網路、同一個應用程式伺服器,唯一的變因,則是應用程式伺服器當中,使用IIS與原生執行檔提供RESTful資料給Client,這樣就能單純比較出IIS與原生執行檔的效能差異了。
比較工具: Apache JMeter Windows
設定條件: 一共 450Thread,每個Thread10筆查詢。
案例一: 原生執行程式,結果如下面的截圖:

用原生執行檔,每秒處理量是 48.4個要求,但還有33.3%的錯誤率。

案例二: 用相同的設定,改為ISAPI,結果如下面的截圖:

 

每秒處理了83.8個要求,錯誤率為0.
所以,有個被優化過的WebServer作後盾,整個服務的效能提昇、錯誤率下降,比原生執行檔好的多了。

 總結本篇

在這一系列的介紹中,都是以DataSnap作為應用程式伺服器,只是有三個作法,所以分成了三篇介紹:
        DataSnap 原生執行檔,以二進位傳輸資料
        DataSnap 原生執行檔,以RESTful資料傳輸
        DataSnap ISAPI,以RESTful資料傳輸
這三篇文章都是在介紹DataSnap的作法,以三層式的架構與這十多年來WebServer作為應用程式伺服器的比較,我以往也常自己從底層開始製作自己需要的API,用LAMP架構製作的PHP Server端效率也不錯,又免去了Windows各種隱性成本。
但考量到大多數的Delphi開發人員並不熟悉這麼多不同的平台,而且從頭寫起也並不是很方便,所以遇到跟DataSnap相關的專案,也正好讓我從不同的角度回頭檢視了一下DataSnap的技術與作法。
以純Delphi程式人員的角度來看,DataSnapRAD Studio都是不錯的選擇,可以讓我們從頭到尾用Delphi就能夠把服務建置起來。而透過本篇最後的資訊分享,更讓大家可以看的出,借重現有的商用WebServer (IISApache),更可以大大提昇服務的可靠度跟效率。
尤其在Delphi XE 10之後,Linux也已經成為Delphi支援的作業系統之一,因此,製作出Windows DataSnapApache外掛之外,更可以製作出LinuxDataSnapApache外掛,如此一來,LAMP的架構中,也能有Delphi的角色了。
本篇文章的範例程式在此,請大家多多指教。