在 Delphi XE 推出以前的年代,Delphi的发展方向是笔直朝向资料库连结Windows 应用程式这个目标不断前进的,从Delphi 1开始,到Delphi 7,Delphi奠定了VB Killer的外号,主要依靠的就是与资料库的连接功能超越其他开发工具,而且超越的距离不只一个世代。
在 .NET开始发展,Delphi 8, Delphi .NET 不断延迟的时候,与资料库连接功能的方便性,仍然让许多ERP厂商、软硬体厂商持续爱用 Delphi.
直到 Web 开发与 App 开发超越了 Windows 应用程式的需求,VC, VB, Delphi 也开始随着这波潮流,渐渐不再像 1990年代那么广受爱戴了。
在1990到 2010年之间,Delphi的网路连线功能,主要是借由第三方元件来提供的,其中知名度最高,全球使用人数也最多的,应该就是 Indy 这套元件了。
这套元件在 2000 年前,叫做 WinShose,从第八版之后才改名为Indy,全球投入这套元件开发的开发人员,前后超过40人,从最基础的 TCP/IP 功能到各种协定的Client与Server 端元件,笔者从中得益非常多,也开发了当中的DNS Server元件,对通讯协定的深入了解,Indy团队可说是我不可或缺的师长。
随着Delphi 工具走入了多平台开发的领域,Indy的局限性也在这两三年凸显了出来,主要是在各个作业系统上面对于SSL与加密功能的支援无法紧密结合到作业系统内建的功能所致。
由于这个局限性,Delphi XE6开始,REST Client系列元件渐渐开始成为 Delphi 团队的重点开发项目之一,所以我们从 Delphi XE6, Delphi XE7 之后的版本,可以发现到,使用 TRESTClient, TRESTRequest, TRESTResponse 系列组合的应用程式越来越多了,原厂也不断鼓励大家使用这套元件来提供 REST API 的连线功能。
REST API 的基础是 HTTP 协定,大多以 HTTP 的 POST 方法把 JSON 编码形式的参数传递到 Server,而 Server 再以 JSON 形式的参数回传。
有时作法也会稍有变化,例如以 POST 方法把 Web-Form 编码形式的参数传递给 Server,Server 再以 JSON 形式把资料回传。
形式不一而足,但相同的是 HTTP 协定,最常用的也是以 POST 方法把参数传给 Server 端。
今天要跟大家分享的主题,则是如何『使用 TRESTClient 与 TRESTRequest 作为 HTTP Client』。
前面已经提到过,在没有 TRESTClient 整组元件以前,我们通常用的是 Indy 系列的元件来提供网路传输的功能,而现在有了 TRESTClient 整组元件,我们在行动平台上面就可以不需要另外配置函式库,也能够直接使用 https 与 server 连线了,在勒索病毒泛滥的今天,使用 https 会让使用者比较安心。
POST作业说明
在 HTTP 的 POST 作业当中,参数跟 GET 作业一样,Client端需要以 name=value&name2=value2 这种形式进行字串连接,再传送到 Server 端去。Get 跟 POST的差异,在于 Get 方法是把所有参数当做 URL 的一部分,发送 HTTP GET 指令的时候,参数连同 URL 一起传送。
而 POST 作业则是发送完 POST 指令后,把所有的参数与资料随之传送。依照 HTTP 型定的规范,GET 作业的 URL 是无法加密的,而且长度也有限制。因此,当需要传递的资料比较多,或者有机敏性,透过 HTTPS 传送,就是最直接,也最方便,更是目前最通用的资料保护方法。
透过 POST 传递的参数,除了字串以外,还常常包含了档案传递。我们很常看到在网页上面以按钮提供使用者选择要上传的档案,也常看见提供以拖拉的方式把档案上传到远端系统,尤其网页邮件系统最常见到这种作法。
过去以 TIdHTTP 元件的 POST 方法发送参数时,呼叫方式如下:
1 var 2 httpClient : TIdHTTP; 3 url, params, httpResultStr : string; 4 begin 5 url := 'http://mytestURL.com/test.php'; 6 params := 'name=我的名字&test=测试'; 7 8 httpClient := TIdHTTP.Create(self); 9 try 10 httpResultStr := httpClient.Post(url, params); 11 showMessage(httpResultStr); 12 finally 13 httpClient.Free; 14 end; 15 end;
这样就可以把 params 字串的众参数传到 server 去了。理论上是这样没错,但事情并没有这么简单,在 HTTP 协定当中要传参数给 Server,如果这些字串包含了特殊字元,则必须要先经过编码,而编码,是我们一生都需要与之对抗的繁复程序。
在 HTTP GET 方法当中,所有的参数除了要以 name=value 对每一个参数做描述,以及需要用 & 来连接各组参数,所有的 value 都需要以 url encode 来摆脱 URL 保留字元的纠缠。name 是否需要编码呢?笔者建议,name 就乖乖的用英文吧,可以省下很多问题,以及处理这些可避免的问题所需要的时间!
那么同样的功能,以 TRESTClient 跟 TRESTRequest 要怎么达成呢? 也很容易,作法如下:
1. 在 form 里面放上 TRESTClient 跟 TRESTRequest 元件各一。
2. 把要传递的参数加到 TRESTRequest 实体的 params 属性里面去,这个属性的型别是 TArray,所以可以存放多组参数。
3. 设定 TRESTClient 要传送参数的URL,注意,URL 是设定在TRESTClient 哦!
4. 设定 TRESTRequest 要使用的传输方法,要设定为POST(因为我们正在介绍的是POST方法,请按照您的需求调整)
5. 呼叫 TRESTRequest 实体的 execute 方法,就可以把资料送去 server 了。
写成 Delphi 的程式码,会像以下这样:
self.RESTClient1.BaseURL := 'http://我的网址/acceptNewCard.php'; self.RESTRequest1.Params.Clear; self.RESTRequest1.Method := rmPOST; self.RESTRequest1.AddParameter('test', self.EditCardNo.Text); self.RESTRequest1.AddParameter('name', self.EditName.Text);
是不是很容易呢?的确很容易,里头的问题我们等下再深入探讨,先来看 server 端要怎么接收这些个参数,我们用 PHP 当范例,需要用 C#,JSP的读者朋友们请自行转译喔⋯⋯
PHP Server 端接收 POST 参数的方法
从 1994 年开始,笔者就陆续撰文说明 HTTP POST 方法如何接参数,包含了CGI 用C,perl等语言实作,也包含 ISAPI 以 Delphi 实作,近几年比较流行的是 PHP,JSP,C#,但 PHP 程式码读起来比较简洁易懂,所以我就选择 PHP 来做范例了。
在 PHP 里面,透过GET 跟 POST 方法传递的参数,会被分别存放在 $_GET 跟 $_POST 这两个阵列变数里面,如果要偷懒,不想区分 GET 或 POST 方法,也可以从 $_REQUEST 这个变数试着读取,当中有些安全性考量,最好勤劳一点,把它们区分开来。
以刚刚的例子来看,我们传了一个名为 name,以及一个名为 test 的字串,用的是 POST 方法,所以我们得用以下两个变数来存取这两个字串:
• $_POST['name'] 这个变数可以取得 Client 端发送出来的 name
• $_POST['test'] 这个变数可以取得 Client 端发送出来的 test
所以在 server 端,我们可以这样写,来抓到这两个资料:
$name = $_POST["name"];
$test = $_POST["test"];
这样写会不会出问题呢? 答案是不会!如果使用者不输入中文的话!
中文资料的编码处理
Delphi的开发人员绝大多数都是英美语系的人,我推测因此对于亚洲语系的文字显示与传输比较没有办法完整的测试,但对于我们以中文为母语的人来说,从电脑诞生的那个年代,中文的显示在每个操作系统、每种通讯协定的设计都比英文来的困难。
以上面的例子来看,如果我们直接拿这个例子来测试,笔者写的范例程式,执行传输资料时,Server 所抓到的文字并不是正确的中文字,如下图所示:
可以看得出来,传到 server 的时候,server 是读不到资讯的。这是怎么回事呢?笔者属于不认输的好奇宝宝,使出了浑身解数,终于解决了这个问题。
写过 Web 程式的读者们一定可以立刻推测出来,这绝对是文字编码出问题了,然而,是什么地方出问题?可能出问题的点我列出来跟大家分享:
• HTTP Client 的 charset 设定错了
• HTTP Request 里面的文字编码出问题
检查的方向也是从这两个关键点出发,第一点的检测很容易,从Object Inspector检查一下 RESTRequest1的设定:
AcceptCharset 确定是 UTF-8,没错,所以设定不是问题。
接着,就要从 Client 端发出去的资料下手了。有读者或许会问『你怎么不怀疑Server端程式写错了?』这个问题很好,之所以排除了这个问题,是因为同一个 Server 端的 PHP 程式,我用了 Postman 做过比对测试,回传的结果是正确的,因此判定是 Client 端程式的问题。
接着笔者从 TRESTRequest.AddParameter 的各种多载形式来尝试,AddParameter 这个方法有以下几种多载的形式:
procedure AddParameter(const AName, AValue: string); overload; procedure AddParameter(const AName: string; AJsonObject: TJSONObject; AFreeJson: boolean = true); overload; procedure AddParameter(const AName, AValue: string; const AKind: TRESTRequestParameterKind); overload;
三种形式我都测试过,从 AddParameter 的执行中 trace 进去看各个可能性,由于 TRESTRequest 的参数中,Get 跟 Post 的加入方法是混用的,在程式码里面编码又会有点不同。
在 REST.Client.pas 里面,我曾经怀疑过编码错误,所以也在执行阶段对各个变数都进行观察,最后,找到了原因与解法,至于过程,就不多说了,花了我两天咧。
原因:编码错误
用HTTP传递中文的时候,务必用UTF-8编码,但一定要记得,中文字在作业系统中,都是UCS32编码,这个现象在Windows里面如此,在Android里面如此,在iOS跟Mac我不确定,但处理方法是一样的。
直接以 AddParameter('name', '中文测试'); 把参数加进 TRESTRequest 的时候,REST.Client.pas 的程式码是把 '中文测试' 这个字串直接抓 Ord 的资料来做编码的,然而,这个作法,是错的!!!!!!!!
在 HTTP 传递 UTF-8 资料的时候,我们要传递的是 UTF-8 文字的二进位资料,但直接把 '中文测试' 这个字串直接拿来转成二进位? 当时编码并不是 UTF-8 啊,当然怎么编码送到 server 都是错的!!!!
解法:AddParameter之前先做 UTF-8 转换
这个解法,笔者第一天就已经想到,只是很想像以前改 Indy 程式一样,直接改好 REST.Client.pas 之后,回馈给原厂使用,所以花了不少时间找方法,最后发现这个方法不用动到 REST.Client.pas,又能正确处理,就直接这么跟大家分享了,写成 Delphi 程式码如下:
var nameStr : String; begin ... nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8); self.RESTRequest1.AddParameter('name', nameStr, TRESTRequestParameterKind.pkGETorPOST, [TRESTRequestParameterOption.poDoNotEncode]); ... end;
在把字串透过 AddParameter 加入参数阵列之前,我先把字串做个 UTF-8 转换,在这里用的是 TIdURI 的类别方法 ParamsEncode,这个方法只有两个参数,第一个参数是字串内容,第二个参数则是要文字编码的种类,在这里我选择了 UTF8,写法就是上面范例程式的第一行。
接着,在呼叫 AddParameter 的时候,我使用了多载形式当中的第三种,要求 AddParameter 处理资料的时候不要再动我的编码,因为我已经处理好了。
这么修改过之后,在各个作业系统当中,执行结果都是正确的,上面两个图以 Windows 作业系统为例,现在我们拿 Android 截图来做为例子:
读者可以看到右边的截图里面,server 回传的资料已经是正确的中文字了。
最后,我把 PHP 程式码也附上来给大家参考:
1 <?php 2 date_default_timezone_set("Asia/Taipei"); 3 header('Content-Type: charset=utf-8'); 4 5 include("public/DBClassPDO.php"); 6 $objDBPDO = new DBClassPDO(); 7 8 $cardNum = $_POST["cardNum"]; 9 $floorIdx = $_POST["floorIdx"]; 10 $name = $_POST["name"]; 11 $houseNum = $_POST["houseNum"]; 12 13 $params = array(); 14 $params['name'] = $name; 15 $params['cardno'] = $cardNum; 16 $params['houseNum'] = $houseNum; 17 $params['floorIdx'] = $floorIdx; 18 $params['picFilename'] = $fileSaveName; 19 $params['created'] = 0; 20 21 $result["resultCode"] = "0"; 22 $result["result"] = "成功"; 23 $result["sqlcmd"] = $name; 24 25 $jsonStr = json_encode($result); 26 echo $jsonStr; 27 ?>
这是最基本的传递字串,下次再给大家示范怎么传档案,透过 TRESTRequest来做也是很简单的,TRESTRequest 跟 TRESTClient 的确是取代 TIdHTTP 的好工具。