Welcome to TiddlyWiki created by Jeremy Ruston, Copyright © 2007 UnaMesa Association
下面是我們對 [[Diffie-Hellman 密鑰交換演算法|http://www.rsa.com/rsalabs/node.asp?id=2248]] 一個簡要描述:
1、伺服器端保存有用於密鑰交換的大素數 p 和它的本原根 g。其中 p 的長度決定了生成密鑰的長度。
2、客戶端請求加密時,伺服器端生成一個亂數 Xa,然後計算出 Ya = g^^Xa^^ mod p,將 p、g、Ya 連同登錄頁面一起發送給客戶端。
3、客戶端也生成一個亂數 Xb,計算 Yb = g^^Xb^^ mod p,k = Ya^^Xb^^ mod p,然後將 Yb 發送給伺服器端。
4、伺服器端計算 k’ = Yb^^Xa^^ mod p,密鑰交換完成。
其中 k = k‘ = g^^~XaXb^^ mod p。因此 k 和 k' 就是交換完成的密鑰。
伺服器端生成的亂數 Xa 和客戶端生成的亂數 Xb 都不傳遞給對方。傳遞的資料只有 p、g、Ya、Yb。
如果你已經把 PHPRPC 安裝好了,那麼接下來就讓我們開始第一個小程式吧。按照慣例,第一個演示程式幾乎總是 HelloWorld,我們也不想打破這個慣例,不過對於 PHPRPC 來說,有伺服器端就要有客戶端,否則我們就沒有什麼好演示的啦,所以我們的第一個演示程式實際上是兩個,一個是伺服器端,另一個是客戶端。我們都先用 PHP 語言來寫好了。
''伺服器端''
<code php:firstline[0]>
<?php
include ("php/phprpc_server.php");
function HelloWorld() {
return 'Hello World!';
}
$server = new PHPRPC_Server();
$server->add('HelloWorld');
$server->start();
?>
</code>
''客戶端''
<code php:firstline[0]>
<?php
include ("php/phprpc_client.php");
$client = new PHPRPC_Client('http://127.0.0.1/server.php');
echo $client->HelloWorld();
?>
</code>
對於伺服器端程式,我們應該將它命名為 server.php(這是因為客戶端調用時用的是這個名字,而不是 PHPRPC 的什麼規定),然後把它放在本地 Web 伺服器的根目錄下,並保證伺服器可以正常運行 PHP 程式,之後在流覽器或命令行下運行客戶端程式,你就可以看到結果了。
這兩個程式幾乎簡單到無需解釋的地步,所以如果你已經明白它們的意思,那麼就可以直接跳過下面的解釋,繼續看後面的例子。
伺服器端第 1 句是將 PHPRPC 的伺服器端套裝程式含到你的程式裏,之後的 2 - 4 句是定義一個遠端調用的函數,你會發現它與本地函數沒有任何區別。第 5 句是創建伺服器端物件,第 6 句是添加要發佈的方法,這裏添加的就是剛剛定義的 ~HelloWorld 函數,在 PHP 中,添加的發佈方法是函數名的字串表示,在其他語言中可能略有不同。第 7 句是啟動服務。
客戶端就更簡單了,第 1 句是將 PHPRPC 的客戶端套裝程式含到你的程式裏。第 2 句是創建客戶端物件,其中的參數就是伺服器端的位址。第 3 句是對遠端方法(函數)的調用,之後通過 echo 將它顯示出來。如果順利的話,執行後你就會看到輸出的 {{{Hello World!}}}。
上面的例子是發佈的是函數,下面我們來看一下類中的靜態方法如何發佈:
<code php:firstline[0]>
<?php
include ("php/phprpc_server.php");
class Hello {
static function HelloWorld() {
return 'Hello World!';
}
}
$server = new PHPRPC_Server();
$server->add('HelloWorld', 'Hello');
$server->start();
?>
</code>
這個伺服器端只要它的名字與發佈的位址與上面那個發佈函數的例子一樣的話,上面的那個客戶端就可以得到同樣的結果,也就是說,在客戶端看來是沒有任何區別的。
PHPRPC 並不是只可以在 PHP 中使用,它同樣支援其他語言的伺服器和客戶端,而且還可以無差別的相互調用。
現在我們來看一下如何在 Java 中調用這個 PHP 的伺服器方法:
<code java>
import org.phprpc.*;
interface IHello {
public String helloWorld();
}
public class HelloWorld {
public static void main ( String [] args ) {
PHPRPC_Client client = new PHPRPC_Client("http://127.0.0.1/server.php");
IHello clientProxy = (IHello)client.useService(IHello.class);
System.out.println(clientProxy.helloWorld());
}
}
</code>
當我們把這個例子編譯之後,在命令行中輸入以下命令就可以看到執行結果了:
{{{
java -classpath .;phprpc_client.jar HelloWorld
}}}
這個 Java 的客戶端看上去比 PHP 的要稍微複雜一些,不過仍然很好理解。在 Java 客戶端中,我們使用了介面來描述遠端方法,之後我們通過 useService 方法返回一個遠端代理物件,該物件實現了我們定義的介面,之後我們就可以直接調用遠端方法 helloWorld 了。如果你比較細心的話,你還會發現我們在 PHP 中定義的方法和在 Java 中定義的介面的名字的大小寫有點不同,但是仍然可以正常調用。是的,PHPRPC 發佈的方法是不區分大小寫的。所以不論你所使用的語言是否區分大小寫,都可以按照自己(或語言)的習慣來定義方法名。
在本章的最後,我們再來看一下在 ~JavaScript 如何調用 PHPRPC 服務。順便再強調一下,伺服器端不止是可以用 PHP 來編寫,你同樣可以使用其他語言(比如 Java,.NET,Ruby,Python 等),這裏我們只是以 PHP 為例而已。在這個例子中你還會看到如何使用加密傳輸。
<code javascript>
<html>
<head>
<title>HelloWorld</title>
<script type="text/javascript" src="phprpc_client.js"></script>
<script type="text/javascript">
var client = new PHPRPC_Client('http://127.0.0.1/server.php', ['HelloWorld']);
client.setEncryptMode(2);
client.HelloWorld(function (result) {
alert(result);
});
</script>
</head>
<body>
</body>
</html>
</code>
這個 ~JavaScript 是在網頁中運行的,這裏建議大家要把 head 和 body 標籤都寫全,即使它們對你來說看上去沒有什麼用處,但是在有些流覽器中,如果這些標籤沒有寫全,或者寫的不正確,程式就不能正確運行。
在這個例子中,我們會發現在創建 ~PHPRPC_Client 物件時,除了要寫伺服器位址以外,還要將遠端方法的名字作為參數,因為遠端方法可能不止一個,所以這個參數是陣列類型的。
{{{client.setEncryptMode(2);}}} 這句是設置加密傳輸,參數 2 表示雙向加密,就是參數和結果都加密(只不過這個 ~HelloWorld 比較特殊,它沒有參數)。
接下來就是調用遠端方法 ~HelloWorld 了,我們會發現它跟 PHP 和 Java 客戶端的調用不太一樣。是的,在 ~JavaScript 中遠端調用都是採用非同步方式的,也就是說要獲得結果,需要用回調函數,回調函數作為遠端方法的最後一個參數,回調函數也有四個參數,這裏我們只使用第一個參數,也就是返回結果 result,通過 {{{alert(result);}}} 我們就可以在流覽器中看到最後彈出的 {{{Hello World!}}} 提示框了。
通過上面的例子,我們已經看到在 PHP、Java 和 ~JavaScript 這三種語言中使用 PHPRPC 都很簡單,但因為語言的不同,在寫法和用法上又有一些差別。不過你不用擔心其他的語言跟它們也會有很大的差別,因為在接下來的章節中你很快就會發現其他語言跟這上面三種語言實現的寫法和用法上的相似或相同之處了。
<script>
var t=story.findContainingTiddler(place);
if (!t || t.id =='tiddlerHideTiddlerTitle') return;
var nodes=t.getElementsByTagName("*");
for (var i=0; i<nodes.length; i++)
if (hasClass(nodes[i],"title"))
nodes[i].style.display="none";
</script>
<script>
var t=story.findContainingTiddler(place);
if (!t || !readOnly) return;
var nodes=t.getElementsByTagName("*");
for (var i=0; i<nodes.length; i++)
if (hasClass(nodes[i],"toolbar"))
nodes[i].style.display="none";
</script>
ISerializable 定義如下:
<code delphi>
{$M+}
ISerializable = interface
['{2523DF0D-A532-4CF9-AA96-C60DA18E21A4}']
function Serialize: RawByteString;
procedure UnSerialize(ss: RawByteString);
end;
{$M-}
</code>
*@@color(red):''PHPRPC 3.0 用戶指南''@@
##[[歡迎來到 PHPRPC 世界]]
##[[快速入門]]
##[[PHPRPC for PHP]]
##[[PHPRPC for Java]]
##[[PHPRPC for JavaScript]]
##[[PHPRPC for .NET]]
##[[PHPRPC for Delphi]]
##[[PHPRPC for ActionScript]]
##[[PHPRPC for ASP]]
##[[PHPRPC for Ruby]]
##[[PHPRPC for Python]]
##[[PHPRPC for Groovy]]
*@@color(red):''PHPRPC 3.0 開發指南''@@
##[[PHPRPC 協議概述]]
##[[PHPRPC 資料表示]]
##[[PHPRPC 加密傳輸]]
##[[PHPRPC 會話管理]]
##[[PHPRPC 其他話題]]
NULL 序列化為:
{{{
N;
}}}
它與各語言之間的對應表如下:
| 語言 | 序列化 | 反序列化 |
| PHP | NULL | NULL |
| Java | null | null |
| C# | null | null |
| VB(.NET) | Nothing | Nothing |
| ~JavaScript | null 或 undefined | null |
| ~ActionScript 2 | null 或 undefined | null |
| ~ActionScript 3 | null 或 undefined | null |
| Delphi | nil | nil |
| Ruby | nil | nil |
| Python | None | None |
* [[PHP 序列化概述]]
* [[NULL 序列化]]
* [[數值序列化]]
* [[字串序列化]]
* [[容器序列化]]
* [[物件序列化]]
* [[物件自定義序列化]]
* [[引用序列化]]
PHP 序列化格式是一種超羽量級的資料交換格式,易於機器解析和生成。同時也易於人閱讀。它最初在 PHP 3.05 中被引入,之後在 PHP 4、PHP 5、PHP 6 中被繼承並發展。其中 PHP 3 序列化格式已經過時。PHP 4 序列化格式是 PHP 5 序列化格式的子集,PHP 5 序列化格式是 PHP 6 序列化格式的一個子集,本文所述的 PHP 序列化格式是指 PHP 6 序列化格式,它包含了 PHP 4 和 PHP 5 序列化格式的所有內容。PHP 序列化格式實際上是完全獨立於語言的半文本格式,它不但支援 [[JSON|http://www.json.org]] 所支援的所有資料類型和結構,而且支援物件和引用結構。這些特性使 PHP 序列化格式成為更為理想的資料交換格式。
PHP 序列化格式是半文本格式,對於數位,它是採用十進位數字字字串來表示的```因為二進位格式中,數位是按照機器存儲格式保存的,存在位元組序問題```,因此不存在位元組序問題,非常有利於跨平臺跨語言實現。但是對字母大小寫和空白(空格、回車、換行等)敏感,因此,又非常利於機器解析。
PHP 序列化格式用[[擴充巴科斯-瑙爾範式(ABNF)|http://tools.ietf.org/html/rfc4234]]描述如下:
<code ABNF>
value = null / boolean / double / integer /
binary-string / escaped-binary-string / unicode-string /
array / object / custom-object / reference / pointer-reference
null = %x4E ";"
boolean = %x62 ":" BIT ";"
integer = %x69 ":" SINT ";"
double = %x64 ":" (NAN | INF | NINF | SINT ["." 1*DIGIT ["E" SINT]]) ";"
binary-string = %x73 ":" UINT ":" DQUOTE *OCTET DQUOTE ";"
escaped-binary-string = %x53 ":" UINT ":" DQUOTE *(VCHAR / %x5C 2HEXDIG) DQUOTE ";"
unicode-string = %x55 ":" UINT ":" DQUOTE *(VCHAR / %x5C 4HEXDIG) DQUOTE ";"
array = %x61 ":" UINT ":" "{" *(key value) "}"
object = %x4F ":" UINT ":" DQUOTE *OCTET DQUOTE ":" UINT ":" "{" *(name value) "}"
custom-object = %x43 ":" UINT ":" "{" *OCTET "}"
reference = %x72 ":" UINT ";"
pointer-reference = %x52 ":" UINT ";"
UINT = "0" / %x31-39 *DIGIT
SINT = *("+" / "-") UINT
NAN = %x4E.41.4E ; "NAN"
INF = %x49.4E.46 ; "INF"
NINF = "-" INF ; "-INF"
key = integer / name
name = binary-string / escaped-binary-string / unicode-string
</code>
.NET 是一個功能強大的平臺,在其早期的版本中就已經內置了 Web Service、.NET Remoting 等遠端調用技術,在其最新的 .NET 3.5 中還引入了 WCF 這個統一的通訊平臺。如果你只是在用 .NET 做程式開發的話,這些技術應該已經足夠滿足你的需求啦!
但是,有時候你不得不採用 .NET 和其他語言結合來做系統,上述的技術就不再那麼可愛了,因為你要面對互操作性的很多麻煩。而 PHPRPC for .NET 就是破除這些麻煩的利器!
PHPRPC for .NET 不僅僅為互操作性而生,就單純的 .NET 平臺應用而言,它也是非常優秀的!它可以讓你用比 Web Service、.NET Remoting 甚至 WCF 更容易的方式做分散式編程。其執行效率也相當的高,遠超 Web Service,接近 .NET Remoting。而統一平臺的能力則猶為突出,連 WCF 都無法比擬。它不但可以在所有版本的 .NET Framework 之間互通,還支持所有版本的 .NET Compact Framework 和最新的 ~SliverLight 2.0,對 Mono 平臺也是完整支持!
我想現在你一定迫不及待地想要瞭解 PHPRPC for .NET 的細節了吧,那下面就讓我們進入主題吧!
*[[PHPRPC for .NET 的安裝]]
*[[PHPRPC for .NET 伺服器]]
*[[PHPRPC for .NET 客戶端]]
*[[PHPRPC for .NET 工具類]]
*[[PHPRPC for .NET 常見問題解答]]
!!PHPRPC for .NET 支援哪些伺服器?
只要是支援 aspx 的伺服器都可以運行 PHPRPC for .NET 伺服器。所以 IIS、apache + mod_mono、xsp 等都支持。
!!如何發佈服務?
.NET 中的物件方法或類的靜態方法都可以直接作為 PHPRPC 服務來發佈。但發佈的方法必須是用 public 聲明的方法,參數和返回值都必須是基本類型或可序列化的類型(帶有 {{{SerializableAttribute}}} 屬性的物件),對於泛型容器,只有 List 和 Dictionary 支持。除此之外,沒有別的要求。
服務通過 aspx 發佈。下面我們舉一個簡單發佈的例子,這個例子我們採用 ASP.NET 2.0 代碼分離方式編寫:
''server.aspx''
<code xml>
<%@ Page Language="C#" CodeFile="server.aspx.cs" Inherits="server" %>
</code>
''server.aspx.cs''
<code c#>
using System;
using System.Collections.Generic;
using org.phprpc;
class ServerTest {
public String Hi(String name) {
return "Hello " + name;
}
public static List<int> Sort(List<int> list) {
list.Sort();
return list;
}
}
public partial class server : System.Web.UI.Page {
protected void Page_Load(object sender, EventArgs e) {
PHPRPC_Server server = new PHPRPC_Server();
server.Add("Hi", new ServerTest());
server.Add("Sort", typeof(ServerTest));
server.Start();
}
}
</code>
通過這個例子,你會發現用 PHPRPC 發佈服務很簡單,只要先創建 {{{PHPRPC_Server}}} 類的一個物件實例,然後執行它的 {{{Add}}} 方法來添加要發佈的方法,最後執行它的 {{{Start}}} 方法來啟動服務就可以。
另外,提醒大家一點,不要忘記發佈服務時,把 org.phprpc.dll 放到服務頁面所在的 bin 目錄下。服務頁面的名字沒有要求,server.aspx、index.aspx 或其他什麼名字都可以。只要在調用時指定的 URL 與發佈時的名字一樣就可以了。
!!如何發佈整個物件中的方法或整個類中的靜態方法?
如果在使用 {{{Add}}} 方法時,只代入一個 {{{Object}}} 類型的參數,則該物件上的所有 {{{public}}} 方法都會作為服務發佈。如果只代入一個 {{{Class}}} 類型的參數,則該類上的所有 {{{public static}}} 方法都會作為服務發佈。
從 3.0.2 開始,對上述行為作了稍許改變,在 3.0.2 之後,只發佈物件所在類或指定類上聲明的方法,而祖先類的方法不會被發佈,這一修改可以避免發佈 {{{Object}}} 上或其他基類上你不希望發佈的方法。
!!如何發佈一個物件中的一組方法或一個類中一組靜態方法?
如果第一個參數不是字串,而是字串陣列,則所有與該陣列中字串值匹配的 {{{public}}} 方法都會作為服務發佈。
!!如何用別名來發佈方法?
有時你發佈的幾個物件或類中的方法可能具有相同的方法名,你也許會希望用不同的名字來發佈它們,以便能夠將它們區分開來。例如,你可能要同時發佈一個 User 類和一個 Catalog 類,但是它們都有 Add、Update、Selete 方法,那麼你可以通過這種方式來發佈它們:
<code c#>
PHPRPC_Server server = new PHPRPC_Server();
server.Add("Add", typeof(User), "addUser");
server.Add("Update", typeof(User), "updateUser");
server.Add("Delete", typeof(User), "deleteUser");
server.Add(new string[] {"Add", "Update", "Delete"},
typeof(Catalog),
new string[] { "addCatalog", "updateCatalog", "deleteCatalog"});
server.Start();
</code>
從上面的代碼中我們可以看到,不管是添加一個方法還是添加一組方法,都是支持別名的。
!!如何在發佈的方法中使用會話(Session)?
有時,你可能希望伺服器端發佈的方法支持會話功能,在 .NET 中這很容易做到,只要在要發佈的方法中,使用 {{{HttpContext.Current.Session}}} 就能獲取到 {{{Session}}} 對象了。另外,你還可以通過 {{{HttpContext.Current.Request}}} 獲取 {{{Request}}} 對象,通過 {{{HttpContext.Current.Server}}} 獲取 {{{Server}}} 對象。但是不要用 {{{HttpContext.Current.Response}}} 獲取 {{{Response}}} 物件並用它做任何輸出操作。
!!如何讓發佈的方法支援輸出重定向?
有時,我們定義的方法除了返回結果以外,還會輸出一些資訊,在遠端調用這種方法時,我們可能會希望客戶端不但可以得到遠端方法的結果,還可以單獨得到這些輸出資訊。PHPRPC 提供了這種能力。
你只需要將最後一個參數設置為 {{{System.IO.TextWriter}}} 或 {{{System.IO.StreamWriter}}} 類型的變數(任選其一,但不能同時定義它們),就可以通過該變數輸出資訊,並返回給客戶端了。客戶端在進行遠端調用時,不需要代入該參數,而是通過 {{{Output}}} 屬性來獲取遠端的輸出資訊。
當你需要返回給客戶端許多字串形式的內容時,我們推薦你用這種輸出重定向方式來返回,因為它不需要伺服器端序列化和客戶端反序列化的過程,可以大大提高伺服器和客戶端的處理速度。
!!如何在伺服器端方法執行出錯時,返回更多的調試資訊?
在發佈服務時,只需要設置 {{{DebugMode}}} 熟悉值為 {{{true}}},然後再執行 {{{Start}}} 方法就可以返回更加詳細的出錯調試資訊了。
!!如何發佈全局方法?
有很多時候,你希望發佈方法能夠更加高效一些,而不希望每個請求都去執行方法的發佈動作,這很容易,只需要使用{{{AddGlobal}}} 方法就可以了,不過這個方法你需要在 Global.asax 中執行,下面我們來看一個例子:
''Global.asax''
<code xml>
<%@ Application Language="C#" CodeBehind="Global.asax.cs" Inherits="Global" %>
</code>
''Global.asax.cs''
<code c#>
using System;
using org.phprpc;
using System.Collections.Generic;
class Test {
public double add(double a, double b) {
return a + b;
}
public string add(string a, string b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
public int inc(ref int n) {
return n++;
}
public static string hello(string name, System.IO.TextWriter output) {
string result = String.Concat("hello ", name);
output.Write("output: " + result);
return result;
}
};
public class Global: System.Web.HttpApplication {
protected void Application_Start(Object sender, EventArgs e) {
PHPRPC_Server.AddGlobal(new string[] { "add", "sub", "inc" }, new Test());
PHPRPC_Server.AddGlobal("hello", typeof(Test));
}
}
</code>
要注意,全局方法不要跟每個請求中用 {{{Add}}} 發佈的方法重名,否則會產生衝突。
這樣就給全局的 {{{PHPRPC_Server}}} 類添加上了 add, sub, inc 和 hello 方法了。接下來發佈的網頁就簡單多了,比如發佈的網頁是 index.aspx 的話,那麼它的內容只需要這麼點就夠了:
''index.aspx''
<code xml>
<%@ Page Language="C#" %>
<% new org.phprpc.PHPRPC_Server().Start(); %>
</code>
是不是很簡單啊?
!!同步調用和非同步調用
PHPRPC for .NET 用戶端與其他語言不同,PHPRPC for .NET 用戶端同時提供了同步調用和非同步調用兩種方式。不過針對不同版本的 .NET Framework,對同步調用和非同步調用的支援也有一些區別,~SilverLight 2.0 不支持同步調用,以下版本不支援非同步調用:
* .NET Framework 1.0、1.1
* 所有版本的 .NET Compact Framework
* Mono 1.0(也就是用 mcs 編譯的程式)
另外,非同步調用僅能用於 ~WinForm 和 ~WebForm 程式中(一般只用於 ~WinForm 程式),不支援在控制臺程式中使用非同步調用,另外,目前的 GTK# 程式也不支援非同步調用。
!!直接調用和代理調用
PHPRPC for .NET 同 PHPRPC for Java 一樣,也提供了直接調用和代理調用兩種方式,不過 .NET Compact Framework 版本只支援直接調用方式,不支援代理調用方式。
下面我們以控制臺程式為例,先來看一下直接方式的同步調用:
<code c#>
using System;
using org.phprpc;
using org.phprpc.util;
namespace console_test
{
class Program
{
static void Main(string[] args)
{
PHPRPC_Client client = new PHPRPC_Client("http://127.0.0.1:8080/server.aspx");
Console.WriteLine(PHPConvert.ToString(client.Invoke("Hi", new Object[] {"Ma Bingyao"})));
int[] a = new int[10];
Random r = new Random();
for (int i = 0; i < 10; i++)
{
a[i] = r.Next();
}
a = (int[])PHPConvert.ToArray(client.Invoke("Sort", new Object[] {a}), typeof(int[]));
for (int i = 0; i < 10; i++)
{
Console.Write(a[i] + "\r\n");
}
Console.ReadLine();
}
}
}
</code>
通過這段程式,你會發現直接調用並不難,直接用 {{{PHPRPC_Client}}} 的 {{{Invoke}}} 方法就可以了,第一個參數是遠端方法名(大小寫無關的),第二個參數就是遠端方法的參數列表,因此即使只有一個參數,也要用陣列傳遞。不過處理返回值,需要用 {{{PHPConvert}}} 類中的相應轉換方法來進行轉型操作,才能夠得到想要的結果。總體來說,有兩個缺點,第一是不夠直觀,第二是稍微有些麻煩。因此,就有了下面這種代理調用方式。
下面我們繼續以控制臺程式為例,來看一下代理方式的同步調用:
<code c#>
using System;
using org.phprpc;
namespace console_test
{
public interface ITest
{
string Hi(string name);
int[] sort(int[] a);
}
class Program
{
static void Main(string[] args)
{
PHPRPC_Client client = new PHPRPC_Client("http://127.0.0.1:8080/server.aspx");
ITest test = (ITest)client.UseService(typeof(ITest));
Console.WriteLine(test.Hi("Ma Bingyao"));
int[] a = new int[10];
Random r = new Random();
for (int i = 0; i < 10; i++)
{
a[i] = r.Next();
}
a = test.sort(a);
for (int i = 0; i < 10; i++)
{
Console.Write(a[i] + "\r\n");
}
Console.ReadLine();
}
}
}
</code>
上面程式中,我們首先定義了一個 {{{ITest}}} 介面,這個介面是用來描述遠端方法的,但是伺服器無需去實現該介面,也無需在伺服器端定義該介面。用戶端介面中的方法也無需跟伺服器端的發佈的方法完全一致,因為方法名是不區分大小寫的,參數類型只要能夠保證相互可以轉換就可以。例如上面程式中的 {{{sort}}} 方法,在伺服器發佈時,是首字母大寫的,而這裏是首字母小寫;伺服器端定義的參數和返回值類型都是 {{{List<int>}}} 類型,而這裏都是 {{{int[]}}} 類型。因為有了這些特點,跟其他的動態類型語言通訊時就會更加靈活了,也為不同版本的 .NET 框架之間互通提供了保證。
後面 {{{UseService}}} 方法根據參數指定的介面,返回一個實現了該介面的用戶端代理物件,再之後,我們就可以通過這個代理物件來進行遠端調用了。後面在調用時,我們會發現跟本地調用看上去沒什麼區別。
當然,這種沒區別只是從程式形式上看上去沒區別,而且因為是控制臺程式,同步調用的結果看上去也很合理。但如果你是在做 ~WinForm 這樣的表單程式,那麼同步調用就顯得不那麼可愛了。因為它會阻塞當前線程,當你按下某個執行同步調用的按鈕時,如果網速不是很快,你就會發現視窗假死了,它不再有任何回應,直到遠端調用執行完畢。
要解決這個問題,其實也不難,只要單獨開一個線程,讓同步調用運行在區別於表單操作線程之外的另一獨立線程中,就可以避免這種表單假死現象了。不過,多線程程式設計並不是一件很容易的事情,畢竟線程之間的資料交換本來就很麻煩,尤其是當要處理的業務非常複雜時,你很可能會被多線程給搞暈。
所以,PHPRPC for .NET 提供了非同步調用,有了它之後,你做表單程式就會變得非常容易了。另外,在 ~SilverLight 2.0 中,PHPRPC 限制了只能使用非同步調用,這樣你就再也不用擔心你的 ~SilverLight 程式會搞“死”用戶的流覽器了。
下面我們用一個 WPF 程式來舉例說明如何使用代理方式來進行非同步調用:
''Window1.xaml''
<code xml>
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Button HorizontalAlignment="Right" Name="button1" Width="75" Height="23" VerticalAlignment="Top"
Click="button1_Click">Button</Button>
<TextBlock Margin="0,2,81,0" Name="textBlock1" />
</Grid>
</Window>
</code>
''Window1.xaml.cs''
<code c#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using org.phprpc;
using org.phprpc.util;
namespace WpfApplication1
{
public interface ITest {
void Hi(string name, PHPRPC_Callback callback);
void Hi(string name, PHPRPC_Callback<String> callback);
void Sort(List<int> list, PHPRPC_Callback<int[]> callback);
}
public partial class Window1 : Window {
PHPRPC_Client client;
ITest it;
public Window1() {
InitializeComponent();
client = new PHPRPC_Client();
it = (ITest)client.UseService("http://127.0.0.1:8080/server.aspx", typeof(ITest));
}
private void callback1(Object result, Object[] args, String output, PHPRPC_Error warning) {
textBlock1.Text += "\r\n" + PHPConvert.ToString(result);
}
private void callback2(String result, Object[] args, String output, PHPRPC_Error error, Boolean failure) {
if (failure) {
textBlock1.Text += error.ToString();
}
else {
textBlock1.Text += "\r\n" + result + " Generic";
}
}
private void button1_Click(object sender, RoutedEventArgs e) {
client.KeyLength = 256;
client.EncryptMode = 1;
it.Hi("Ma Bingyao", new PHPRPC_Callback(callback1));
it.Hi("PHPRPC", callback2);
List<int> list = new List<int>(10);
Random r = new Random();
for (int i = 0; i < 10; i++) {
list.Add(r.Next());
}
it.Sort(list, delegate(int[] result, Object[] args, String output, PHPRPC_Error error, Boolean failure) {
foreach (int i in result) {
textBlock1.Text += "\r\n" + i.ToString();
}
});
}
}
}
</code>
上面這段程式的主代碼你幾乎可以不加修改的移植到 WPF 流覽器應用程式或者 ~SilverLight 2.0 程式中去。當然在 ~WinForm 程式中用法也差不多,所以就不再單獨舉 ~WinForm 的例子了。下面來對它做一個簡單的說明。
這段程式中也定義了一個 {{{ITest}}} 介面,這個介面跟前面控制臺程式中的介面差別很大,每個方法的返回值都變成了 {{{void}}},而在每個方法的最後都定義了一個 {{{PHPRPC_Callback}}} 類型的參數,而且可以是普通的 {{{PHPRPC_Callback}}} 類型,也可以是 {{{PHPRPC_Callback}}} 的泛型。
凡是定義了帶有這個 {{{PHPRPC_Callback}}} 類型參數的方法(注意:一定是最後一個參數),都是非同步調用的方法。我們再往下看會發現,{{{PHPRPC_Callback}}} 類型其實是一個委託類型,這個委託有四個參數(泛型版本有五個參數):
第一個參數 {{{result}}} 對於普通的 {{{PHPRPC_Callback}}} 類型來說,是 {{{Object}}} 型的,對於 {{{PHPRPC_Callback}}} 泛型來說,它就是泛型指定的類型。這個參數就是伺服器端發佈的方法的返回值,對於普通的 {{{PHPRPC_Callback}}} 來說,因為這個參數是 {{{Object}}} 類型,所以使用這個參數時,需要自己用 {{{PHPConvert}}} 類中的相應方法進行轉型操作,而泛型版本則無需自己轉換了,因此為了方便,推薦使用泛型版本。
第二個參數 {{{args}}} 是用戶端傳到伺服器的參數,它以 {{{Object[]}}} 的形式返回,它其中的值有可能與用戶端傳遞時不同,因為在伺服器端可能被修改(當調用是引用參數傳遞時),如果需要取出每一個參數值,你也需要用 {{{PHPConvert}}} 類中的相應方法進行轉型操作,而不能直接強制類型轉換,否則可能會因為類型不匹配而出錯。
第三個參數 {{{output}}} 就是伺服器端輸出的內容,如果伺服器端沒有輸出,該參數值就為空字串(不是 {{{null}}})。
第四個參數 {{{warning}}} 是伺服器端發生的警告錯誤。泛型版本該參數({{{error}}})表示調用過程中發生的錯誤(包括警告錯誤)。
第五個參數 {{{failure}}} 表示是否是失敗性錯誤,如果是 {{{false}}},則表示警告錯誤。
{{{PHPRPC_Callback}}} 的普通版本和泛型版本還有一個區別,就是如果遠端程序呼叫中發生錯誤,普通版本以第一個參數 {{{result}}} 返回,這時 {{{result}}} 是一個 {{{PHPRPC_Error}}} 類型的物件。泛型版本則以第四個參數 {{{error}}} 返回 {{{PHPRPC_Error}}} 類型的對象,並且將第五個參數值設置為 {{{true}}}。
對於這個委託參數,可以傳遞方法名,也可以使用匿名委託,因為匿名委託支援閉包特性,這會使程式設計更加方便。
非同步調用雖然也支援直接調用的方式,但是我們完全沒有必要使用它,因為支援非同步調用的用戶端都完全支援代理調用方式,而代理方式可以實現直接調用方式的所有功能,並且更加簡單有效。
!!引用參數傳遞
對於 .NET 來說,引用參數傳遞非常簡單,只要在代理介面方法中,將引用參數聲明為 {{{ref}}} 或 {{{out}}} 參數,就可以實現跟本地調用一樣的引用參數傳遞效果。對於直接方式調用,則需要設置 {{{Invoke}}} 方法的 {{{byRef}}} 參數為 {{{true}}},但通常我們沒有必要用這種方式,除非是在使用 .NET Compact Framework 用戶端。
!!獲取伺服器端輸出
對於同步調用來說,通過 {{{PHPRPC_Client}}} 的 {{{Outout}}} 屬性就可以獲得伺服器端輸出。
!!加密傳輸
{{{KeyLength}}} 屬性用於設置加密傳輸前[[密鑰長度]],默認長度是 128。
{{{EncryptMode}}} 屬性用於設置[[加密模式]],預設值為 0。
!! Web 代理伺服器
{{{Proxy}}} 屬性用於設置 Web 代理伺服器。默認為系統默認的代理或者無代理設置(由具體運行環境決定),對於 ~SilverLight 2.0 用戶端來說,沒有這個屬性。
PHPRPC for .NET 中除了伺服器和客戶端實現之外,還有一些工具類,這些工具類除了在伺服器和客戶端的實現中被調用之外,你也可以單獨使用它們。下面是關於這些工具類的常見問題解答。
!! ~AssocArray 類如何使用?
3.0.1 版中新增加了一個 {{{AssocArray}}} 類,該類主要用於優化反序列化聯合陣列類型時的效率。3.0.1 版本之前反序列化聯合陣列時,是根據下標類型來自動判斷是 {{{ArrayList}}} 類型還是 {{{Hashtable}}} 類型的,但這樣做的缺點是,如果聯合陣列中包含對其自身的引用,則反序列化將會重複多次遞迴操作來完成,效率較低。而且其自動判斷的類型也不一定準確。3.0.1 版本中的 {{{AssocArray}}} 類就是為解決這個問題而加入的。當反序列化聯合陣列時,直接反序列化為 {{{AssocArray}}} 類型的物件,而不再進行類型判斷和遞迴,大大提高了反序列化的效率。並且,{{{AssocArray}}} 物件提供了 {{{ToArrayList}}} 和 {{{ToHashtable}}} 兩個方法,可以在使用時轉換為你需要的物件類型。引入 {{{AssocArray}}} 物件的另一個好處是在某些語言中對於數位下標的陣列序列化時並不一定是按照數位順序排序的,這種情況下,原來的實現將會反序列化為 {{{Hashtable}}} 類型,而原來的 {{{PHPConvert}}} 類中的 {{{ChangeType}}} 方法不能將 {{{Hashtable}}} 轉換為 {{{ArrayList}}} 或陣列物件,而 {{{AssocArray}}} 物件則可以接受不按數位順序序列化的陣列類型,並且當你將它轉換為 {{{ArrayList}}} 時,將自動返回排好序的 {{{ArrayList}}}。
!!~PHPConvert 類如何使用?
PHPRPC for .NET 包中的 {{{org.phprpc.util.PHPConvert}}} 靜態類提供了 PHPRPC 中相容類型(關於相容類型的解釋請參見[[PHPRPC for .NET 常見問題解答]]部分)的轉化功能。其中最重要的方法是 {{{ChangeType}}} 方法,它有幾個重載,你可以根據需要來使用它進行類型轉換,例如將位元組陣列轉換為字串,或者將字串轉換位元組陣列。你也可以通過該方法將 {{{AssocArray}}} 轉換為 {{{ArrayList}}}、{{{Hashtable}}}、泛型容器({{{List}}} 或 {{{Dictionary}}})或者陣列。
該類是在 PHPRPC 進行遠端調用時,進行自動類型轉換用的,在使用代理方式調用時,一般不需要使用。但對於直接方式調用或者返回結果是容器類型的情況,則需要用該類轉換結果類型或容器元素的類型。
!!~PHPFormatter 類如何使用?
{{{org.phprpc.util.PHPFormatter}}} 是用來序列化和反序列化資料的類,它最主要的兩個方法是 {{{Serialize}}} 和 {{{Deserialize}}}。{{{Serialize}}} 方法用於序列化資料,它會把序列化之後的資料寫入流中,{{{Deserialize}}} 方法用於反序列化資料,它會從流中讀取資料並反序列化為物件。注意操作前後要設置好流的位置({{{Position}}} 屬性),這樣就可以避免發生無法讀取流而產生的錯誤了。
另外,它還有一個 {{{Encoding}}} 屬性,用於設置序列化和反序列化時應用的字元集。
!!哪些類型是相容類型?
PHPRPC 是支持弱類型(不是無類型,這一點一定要區分開)參數傳遞的,這是它跟 Web Service、.NET Remoting 等遠端程序呼叫機制的一個重要區別。弱類型參數傳遞的支援對於目前動態腳本語言來說是一個很大的特徵,這個特徵會讓你的程式設計變得更加靈活。
對於弱類型語言來說,相容類型之間是可以根據實際調用情況來自動轉換的,而且這個轉換是雙向的而不是單向的。例如,數位可以轉換為字串,字串也可以轉換為數位。
PHPRPC 中支持的所有簡單類型都是相容類型,它們包括整數、實數、布林類型和字串。在 PHPRPC for .NET 中,{{{Boolean}}}、{{{Byte}}}、{{{Char}}}、{{{Decimal}}}、{{{Double}}}、{{{Int16}}}、{{{Int32}}}、{{{Int64}}}、{{{SByte}}}、{{{Single}}}、{{{String}}}、{{{UInt16}}}、{{{UInt32}}}、{{{UInt64}}}都是相容類型。
另外,{{{String}}}、{{{StringBuilder}}},{{{Char[]}}} 和 {{{Byte[]}}} 之間是相容類型。{{{Byte[]}}} 類型更適合傳輸 binary 類型的資料,例如檔、圖片等。
陣列類型、{{{ArrayList}}}、{{{Hashtable}}}、泛型容器({{{List}}} 和 {{{Dictionary}}}) 和 {{{AssocArray}}} 是相容類型。如果 {{{ArrayList}}} 中存放的類型是單一的資料類型,那麼它跟該資料類型的陣列是相容的。如果 {{{Hashtable}}} 中的索引鍵都是陣列,並且是從零開始遞增的,那麼它與 {{{ArrayList}}} 是相容的。{{{ArrayList}}} 和 {{{Hashtable}}} 類型都可以通過 {{{AssocArray}}} 的構造方法轉換為 {{{AssocArray}}} 類型,另外,其他的實現了 {{{ICollection}}}、{{{IList}}} 或 {{{IDictionary}}} 介面的類型也都可以通過 {{{AssocArray}}} 的構造方法轉換為 {{{AssocArray}}} 類型。
上面所說的這些類型都是可以通過 PHPRPC 直接跟其他語言交互的。
另外,{{{DateTime}}} 類型比較特殊,PHPRPC 會作為一個特殊的物件對它序列化,序列化後的類名為 {{{PHPRPC_Date}}},該類與其他語言中相應的日期類型相容。
!!PHPRPC for .NET 是否支援自定義類型?
PHPRPC for .NET 當然也支援自定義類型,並且自定義類型也可以跟其他語言交互。關於自定義類型需要注意以下幾個問題。
自定義類型必須是可序列化的類型。也就是標記了{{{SerializableAttribute}}} 屬性的類,並且該類型當中所有欄位都是可序列化的類型,例如:
<code c#>
[Serializable]
public class Book {
public string title;
public string[] authors;
public DateTime publishDate;
public Decimal price;
}
</code>
可序列化的類型可以包含方法,但如果要包含構造方法的話,必須要有一個無參構造方法,否則不能被反序列化。
欄位不一定必須是 {{{public}}} 的,也可以是 {{{prvate}}}、{{{protected}}} 的,但如果要跟其他語言交互,必須要保證欄位名相同(區分大小寫),而欄位類型不必一致,只要是相容類型即可。
PHPRPC 不支持 .NET 中實現了 {{{System.Runtime.Serialization.ISerializable}}} 介面的類型。
在 .NET Compact Framework 中,自定義序列化的類型的定義方式相同,雖然 .NET Compact Framework 本身沒有 {{{SerializableAttribute}}} 這個類(.NET Compact Framework 3.5 中已經增加了),但是 PHPRPC 實現中提供了這個類的實現,所以可以實現無差別編程。
判斷是否是同一個類的物件,PHPRPC 是判斷類的全名,即包含了名空間的類名。但是要注意一點,如果將名稱空間與類名之間的分隔符號(例如 {{{.}}},{{{::}}} 等)全部替換為 {{{_}}} 之後,可以得到相同的名稱,則這兩個類型在 PHPRPC 中也被認為是可以跨語言傳輸的類型。例如:.NET 中如果定義了一個 {{{demo.data.MyData}}} 類,那麼在 PHP 中應該定義名為 {{{demo_data_MyData}}} 的類來跟它進行交互。並且,應該保證 {{{demo_data_MyData}}} 中的所有欄位和 .NET 中的 {{{demo.data.MyData}}} 中的所有欄位名稱一致。
所以在 .NET 中定義類時要避免通過該規則變換之後出現重名的情況。
!!PHPRPC for .NET 是否支持 .NET 中的其他內建類型?
只要是符合以上規則的 .NET 類,都可以通過 PHPRPC 傳遞,不管是內建類型還是自定義類型。不過如果要跟其他語言進行交互。你需要在其他語言中實現跟 .NET 中的內建類型定義相同的類,包括內部的各個欄位名稱和類型都要一致(或相容)才可以。要實現這一點,對用戶來說比較困難。因此不推薦使用其他的 .NET 內建類型作為參數或結果類型傳遞,除非你只打算把這個服務提供給 .NET 客戶端來調用。
!!org.phprpc.util.Serializable 這個介面是做什麼用的?
PHPRPC 除了支援以上規則定義的序列化類型以外,還支援用戶自定義序列化方式的類型。而且用戶自定義序列化方式也分兩種。這兩種都跟 PHP 語言所提供的用戶自定義序列化方式類似。{{{org.phprpc.util.Serializable}}} 介面就是其中的一種方式,該方式跟 ~PHP5 中所提供的用戶自定義序列化方式相容。
{{{org.phprpc.util.Serializable}}} 中定義了兩個方法,如果一個類實現了標記了 {{{SerializableAttribute}}} 屬性(這是一個很關鍵的前提)並且同時實現了 {{{org.phprpc.util.Serializable}}} 介面中定義的兩個方法,那麼它就具有了自定義序列化方式的能力。
{{{org.phprpc.util.Serializable}}} 中的 {{{Serialize}}} 方法是在物件被序列化時自動觸發的,你可以在 Serialize 方法中對該物件做任何方式的序列化,例如 SOAP、JSON 或者 WDDX 等,只要返回的結果是位元組陣列就可以了。
{{{org.phprpc.util.Serializable}}} 中的 {{{Deserialize}}} 方法是在物件被反序列化時自動觸發的,傳入的參數就是前面 {{{Serialize}}} 方法的返回內容。你可以用同樣的方式將它反序列化。{{{Deserialize}}} 方法沒有返回值。
一般情況下,我們沒有必要使用這種方式來自定義序列化,如果你只是想要將類中的部分欄位序列化,或者只是想在物件被序列化之前做一些清除操作的話,那麼你可以實現 {{{__sleep}}} 和 {{{__wakeup}}} 這兩個魔術方法來實現。
!!{{{__sleep}}} 和 {{{__wakeup}}} 這兩個魔術方法如何使用?
{{{__sleep}}} 和 {{{__wakeup}}} 方法也是與 PHP 自定義序列化相相容的一種方式。這兩個方法不需要從某個介面繼承,直接在類中實現即可。
{{{__sleep}}} 方法也是在物件被序列化之前自動觸發,但是它的返回值不是序列化後的內容,而是指定哪些欄位需要序列化。它的返回值類型是一個字串陣列,陣列的每一個元素是需要序列化的欄位名。你除了在 {{{__sleep}}} 方法中返回這些欄位之外,你也可以作其他一些操作,比如關閉 socket 連接或者關閉資料庫連接等操作。
{{{__wakeup}}} 方法是在物件被反序列化之後自動觸發的,它被觸發時,所有序列化的欄位都已經被反序列化完畢,所以,你在 {{{__wakeup}}} 中要做的是反序列化的事後工作,比如恢復資料庫連接或者 socket 連接等。
{{{__sleep}}} 和 {{{__wakeup}}} 方法無需同時實現,你只需把你用到的方法實現即可。
另外,{{{__sleep}}} 和 {{{__wakeup}}} 方法和前面提到的 {{{org.phprpc.util.Serializable}}} 介面中的 {{{Serialize}}} 和 {{{Deserialize}}} 方法都無需定義成 {{{public}}} 方法,定義成 {{{private}}} 即可。
!! 為什麼有時候用介面類型來聲明發佈方法的返回值和參數類型會出錯?
不管是在伺服器端定義發佈方法時,還是在客戶端定義代理介面時,都不建議用介面類型來聲明返回值和參數類型。因為介面是抽象的不能被實例化,因此對於容器類型的參數和返回結果來說,介面並不能表明究竟該轉換為何種具體類型。不過對於自定義類型可以用介面,只要傳遞的類型實現了該介面即可。
!!為什麼有時候 ~UseSevice 不能返回代理介面的物件?
如果在這一步發生錯誤,請檢查代理介面是否明確聲明為了 {{{public}}} 介面,因為目前的動態代理類只支援 {{{public}}} 介面。另外,用 {{{UseService}}} 返回介面物件時,不要忘記轉型操作。
!!~ArrayList 和 ~HashMap 中的元素資料類型如何轉換?
{{{ArrayList}}} 和 {{{HashMap}}} 作為結果返回或者作為參數傳遞給伺服器處理的時候,對它們當中元素類型的處理不要用直接的轉型操作,因為資料的真實類型可能不是你所認為的,所以直接轉型的操作很可能會失敗。最好的方式是使用 {{{PHPConvert}}} 類中的方法來進行轉型。
PHPRPC for .NET 提供了完整的源代碼,並且同時提供了編譯好的所有版本的二進位庫檔。一般情況下,你無需自己編譯源代碼,但是如果你有這個需求(比如對源代碼的某些部分做一些特別的修改),只需要安裝有相應版本的 .NET 運行時就可以進行編譯,無需安裝 SDK 和 Visual Studio。
另外,如果要編譯 .NET Compact Framework 1.0 版本,至少需要安裝 Microsoft .NET Compact Framework 1.0 ~SP3 Developer 和 Microsoft .NET Framework 1.1 ~SP1。
如果要編譯 .NET Compact Framework 2.0 版本,至少需要安裝 Microsoft .NET Compact Framework 2.0 ~SP2 和 Microsoft .NET Framework 2.0 或 3.5。
如果要編譯 .NET Compact Framework 3.5 版本,至少需要安裝 Microsoft .NET Compact Framework 3.5 和 Microsoft .NET Framework 3.5。
如果要編譯 Sliverlight 2.0 版本,至少需要安裝 Microsoft .NET Framework 3.5 和 Microsoft Sliverlight SDK。
如果要編譯 Mono 版本,可以安裝 Mono 1.x 或者 Mono 2.x。
如果要編譯 Mono 2.0 版本,至少需要安裝 Mono 2.x。
編譯方法非常簡單,在保證上述軟體安裝好的情況下,在 Windows 下只需要執行 make.bat 即可完成編譯,在 Linux 下只需要執行 make 即可完成編譯。
bin 目錄下是保存的編譯好的二進位庫檔:
{{{
1.0---------+-- org.phprpc.dll // PHPRPC for .NET Framework 1.0 的完整包
| +-- org.phprpc.client.dll // PHPRPC for .NET Framework 1.0 的客戶端包
|
1.1---------+-- org.phprpc.dll // PHPRPC for .NET Framework 1.1 的完整包
| +-- org.phprpc.client.dll // PHPRPC for .NET Framework 1.1 的客戶端包
|
2.0---------+-- org.phprpc.dll // PHPRPC for .NET Framework 2.0 的完整包
| +-- org.phprpc.client.dll // PHPRPC for .NET Framework 2.0 的客戶端包
|
3.5---------+-- org.phprpc.dll // PHPRPC for .NET Framework 3.5 的完整包
| +-- org.phprpc.client.dll // PHPRPC for .NET Framework 3.5 的客戶端包
|
CF1.0---------- org.phprpc.client.dll // PHPRPC for .NET Compact Framework 1.0 的客戶端包
|
CF2.0---------- org.phprpc.client.dll // PHPRPC for .NET Compact Framework 2.0 的客戶端包
|
CF3.5---------- org.phprpc.client.dll // PHPRPC for .NET Compact Framework 3.5 的客戶端包
|
Mono--------+-- org.phprpc.dll // PHPRPC for Mono 1.x 的完整包
| +-- org.phprpc.client.dll // PHPRPC for Mono 1.x 的客戶端包
|
Mono2-------+-- org.phprpc.dll // PHPRPC for Mono 2.x 的完整包
| +-- org.phprpc.client.dll // PHPRPC for Mono 2.x 的客戶端包
|
SilverLight2--- org.phprpc.client.dll // PHPRPC for SilverLight 2.0 的客戶端包
}}}
其中完整包中包含了伺服器和客戶端的實現,而客戶端包中只有客戶端的實現。你可以根據自己的需要來選擇使用哪個包。使用時非常簡單,只要把對應版本的 dll 引用到你的項目中就可以了。
PHPRPC for ASP 是使用 ~JScript 語言實現的,但是它支援 ~VBScript 和 ~JScript 兩種語言,當然 ~VBScript 的一些類型跟 ~JScript 中的一些類型有所區別,所以在 compat.js 中我們提供了一些函數,通過這些函數可以實現這些類型之間的轉換,我們在後面將會對這些函數做詳細介紹。
*[[PHPRPC for ASP 的安裝]]
*[[PHPRPC for ASP 伺服器]]
*[[PHPRPC for ASP 客戶端]]
*[[PHPRPC for ASP 工具包]]
*[[PHPRPC for ASP 常見問題解答]]
!!PHPRPC for ASP 支援哪些版本的 IIS 伺服器?
目前 PHPRPC for ASP 只在 Windows XP/2003 的 IIS 5.1/IIS 6 上進行過完整測試,其他版本未做測試。我們非常歡迎您能夠報告您在其他版本上的部署情況,以幫助我們改善該開發包。
!!如何發佈服務?
ASP 中的函數或 ~JScript 物件的方法都可以直接作為 PHPRPC 服務來發佈。~VBScript 中使用 Class 定義的類的物件方法也可以作為服務發佈,但發佈時必須定義別名(關於別名我們後面討論)。
直接使用 ASP 頁面就可以發佈 PHPRPC 服務,下面是一個簡單的例子:
<code javascript>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="JScript">
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
var server = new PHPRPC_Server();
server.add("add");
server.add(subtract);
server.start();
</script>
</code>
上面的代碼是用 ~JScript 的編寫的,發佈的也是 ~JScript 的函數。
通過這個例子,你會發現用 PHPRPC 發佈服務很簡單,只要先創建 {{{PHPRPC_Server}}} 的一個物件實例,然後執行它的 {{{add}}} 方法來添加要發佈的方法,最後執行它的 {{{start}}} 方法來啟動服務就可以。{{{start}}} 方法沒有參數。
添加發佈方法,可以使用函數名的字串表示,也可以直接使用函數引用,但直接使用函數引用僅限於 ~JScript 中定義的函數,~VBScript 中定義的過程和函數都只能用字串來發佈。
服務頁面的名字沒有要求,rpc.asp、index.asp 或其他什麼名字都可以。只要在調用時指定的 URL 與發佈時的名字一樣就可以了。
另外還要注意一點,上面的代碼是 ~JScript 寫的,~JScript 是區分大小寫的,所以 {{{server}}} 物件這個名字是沒有問題的。但是 ~VBScript 不區分大小寫,如果在 ~VBScript 中用這個名字,就會跟 ASP 內置的 {{{Server}}} 物件衝突,所以一定要注意這一點。
下面我們來看一下用 ~VBScript 如何來發佈服務。
<code vb>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="VBScript">
Function add(a, b)
add = a + b
End Function
Function subtract(a, b)
subtract = a - b
End Function
Set rpc_server = PHPRPC_Server.create()
rpc_server.add "add"
rpc_server.add "subtract"
rpc_server.start
</script>
</code>
這個例子,跟上面用 ~JScript 完成的例子差不多,區別主要在於創建 {{{PHPRPC_Server}}} 對象時,使用的是 {{{PHPRPC_Server}}} 的靜態方法 {{{create}}},因為在 ~VBScript 中無法使用 {{{New}}} 來直接創建 ~JScript 物件,因此我們提供了這個 {{{create}}} 方法。
另一點區別就是,調用 {{{add}}} 和 {{{start}}} 方法時要遵守 ~VBScript 慣例,也就是程序呼叫不加括弧,否則會報錯。還有就是添加的 ~VBScript 函數和過程只能用字串表示。
我們還會注意到不管是用 ~JScript 還是 ~VBScript,最開始都有
<code xml>
<%@ CodePage = 65001 %>
</code>
這一句。這是因為 PHPRPC for ASP 只支援 ~UTF-8 編碼輸出。65001 表示的就是 UTF-8。
!!如何發佈 ~JScript 物件方法?
~JScript 物件方法跟 ~VBScript 物件方法發佈方法不同,這裏我們分開討論。添加 ~JScript 物件的方法較為靈活,可以一次添加一個物件中的所有方法,也可以一次只添加方法。下面這個例子將演示一次添加一個物件中所有方法:
<code js>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="JScript">
function Test() {
this.add = function (a, b) {
return a + b;
}
this.subtract = function (a, b) {
return a - b;
}
}
var rpc_server = new PHPRPC_Server();
var t = new Test();
rpc_server.add(t);
rpc_server.start();
</script>
</code>
也就是說只要把物件直接作為 {{{add}}} 的第一個參數(也是唯一的)傳入,就可以添加這個物件上的所有方法了。
那添加 ~JScript 物件方法的第二種方式如下所示:
<code js>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="JScript">
function Test() {
this.add = function (a, b) {
return a + b;
}
this.subtract = function (a, b) {
return a - b;
}
}
var rpc_server = new PHPRPC_Server();
var t = new Test();
rpc_server.add("add", t);
rpc_server.add(t.subtract, t, "sub");
rpc_server.start();
</script>
</code>
這裏要注意,如果第一個參數(方法名)是字串表示的話,只寫方法名本身,而不寫方法名首碼(這點跟後面介紹的添加 ~VBScript 物件方法的方式正好相反),第二個參數是方法所在物件,這種情況下,第三個參數(別名)可有可無。
而如果第一個參數(方法名)是方法引用表示的話,則第三個參數(別名)一定要寫。
!!如何發佈 ~VBScript 物件方法?
~VBScript 物件方法發佈方式如下:
<code vb>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="VBScript">
Class Test
Function add(a, b)
add = a + b
End Function
Function subtract(a, b)
subtract = a - b
End Function
End Class
Set rpc_server = PHPRPC_Server.create()
Set t = new Test
rpc_server.add "t.add", null, "add"
rpc_server.add "t.subtract", null, "sub"
rpc_server.start
</script>
</code>
只有聲明為 {{{Public}}}(默認就是 {{{Public}}})的 ~VBScript 物件方法才可以發佈,聲明為 {{{Private}}} 的 ~VBScript 物件方法如果發佈的話,客戶端將無法調用到該方法。
發佈 ~VBScript 物件方法時,除了第一個參數是完整的物件方法的字串表示外,第二個參數必須為 {{{null}}},否則客戶端調用時將會產生“'undefined' 為空或不是物件”的錯誤。另外,別名也是必須的,否則客戶端將無法調用該方法。
!!如何發佈一個物件中的一組方法?
{{{PHPRPC_Server}}} 物件的 {{{add}}} 方法的第一個參數(方法名)和第三個參數(別名)可以接受陣列,這時,陣列中的所有的方法都會發佈。當第一個參數是 ~JScript 物件方法名的字串表示的陣列時,第三個參數可以省略。其他情況必須指定第三個參數(別名)。當這兩個參數都指定時,它們的長度必須相等,各個元素也要一一對應。另外,除了可以接受 ~JScript 陣列外,也可以接受 ~VBScript 陣列,但第一個參數和第三個參數的類型必須一致。
!!在發佈的方法中是否可以使用 ASP 中的內置物件?
ASP 中提供了 {{{Request}}}、{{{Response}}}、{{{Session}}}、{{{Server}}}、{{{Application}}} 這幾個主要內置物件,這些內置物件當中,除了 {{{Response}}} 不能直接使用以外,其他的物件都可以直接使用。效果與在普通的 ASP 程式中使用相同。
!!如何在發佈的方法中支援輸出重定向?
因為 ASP 中的 {{{Response}}} 物件不具有獲取輸出到緩存中資料的能力,因此,你無法直接通過 {{{Response}}} 來輸出內容並重定向到客戶端。但是 PHPRPC 3.0 for ASP 提供了一種變通的方法。在 PHPRPC 3.0 for ASP 中,提供了一個 {{{FakeResponse}}} 物件,如果你要發佈的函數中有使用 {{{Response}}} 的話,只需要先定義一個局部變數 {{{Response}}} 給它賦值為 {{{FakeResponse}}},然後在使用 {{{Response}}} 就可以了,例如:
<code vb>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_server.js"></script>
<script runat="server" language="VBScript">
Dim Response
Set Response = FakeResponse
Class Test
Function add(a, b)
add = a + b
End Function
Function subtract(a, b)
subtract = a - b
Response.Write "hah"
End Function
End Class
Set rpc_server = PHPRPC_Server.create()
Set t = new Test
rpc_server.add "t.add", null, "add"
rpc_server.add "t.subtract", null, "sub"
rpc_server.start
</script>
</code>
這裏要注意,{{{Response}}} 的首字母在 {{{Dim}}} 時不能夠小寫,如果是使用 ~JScript 而不是 ~VBScript,則 {{{Response}}} 這個局部變數必須在函數當中定義而不能在全局環境下定義,否則將於 ASP 的內置 {{{Response}}} 物件衝突,客戶端調用將不會得到正確結果。
!!如何調用 PHPRPC 服務
我們先通過一個簡單的例子,來介紹如何調用 PHPRPC 服務:
<code vb>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_client.js"></script>
<script runat="server" language="VBScript">
Dim client
Set client = PHPRPC_Client.create("http://192.168.1.111/test.asp")
client.setProxy("http://10.1.0.1:8000/")
client.setKeyLength(96)
client.setEncryptMode(2)
Response.write(client.add(1, 2))
Response.write(client.sub(1, 2))
Response.write(client.getOutput())
</script>
</code>
上面這段代碼是使用 ~VBScript 編寫的,大家會發現創建 {{{client}}} 使用的是 {{{PHPRPC_Client}}} 的 {{{create}}} 方法,參數為 PHPRPC 伺服器的 URL,如果我們在創建 {{{client}}} 物件時不指定伺服器的 URL,後面我們也可以在調用 {{{useService}}} 方法時再指定伺服器的 URL。
接下來,通過 {{{client}}} 的 {{{setProxy}}} 方法,我們可以設置連接到伺服器所經過的 HTTP 代理伺服器,代理伺服器的設置除了可以單獨指定 IP 位址和埠號以外,還可以直接通過 {{{http://10.1.0.1:8000/}}} 這種 URL 方式來指定代理伺服器。另外,{{{setProxy}}} 方法也支援 Basic 方式認證,只要將用戶名和密碼作為參數帶入即可。
{{{PHPRPC_Client}}} 物件的 {{{setKeyLength}}} 和 {{{setEncryptMode}}} 這兩個方法是跟加密傳輸有關的。
{{{setKeyLength}}} 方法用於設置[[密鑰長度]]。
{{{setEncryptMode}}} 方法用於設置[[加密模式]]。
上面設置代理、設置密鑰長度、加密模式都是可選項,如果你不需要這些功能,可以直接忽略它們。
PHPRPC 3.0 for ASP 客戶端與 Java、.NET 客戶端不同,它不需要使用 {{{useService}}} 來返回指定介面的遠端代理物件,ASP 客戶端本身就是一個代理物件。所以,上面例子中 {{{client.add}}} 和 {{{client.sub}}} 這兩個調用實際上調用的就是遠端方法,對於 ASP 客戶端來說,遠端方法名不需要事先聲明,客戶端會自動從伺服器端獲取到所有的遠端方法。但是你可能希望直接在客戶端代碼中指定所需要調用的遠端方法名,而不是讓它自動獲取,這樣至少可以減少一次跟伺服器的通訊,這樣做也是可以的,只需將方法名字串陣列作為第二個參數傳遞給 {{{create}}} 方法即可。但需要注意的是,這個方法名字串陣列是 ~JScript 陣列,而不是 ~VBScript 中的陣列。另外,你也無法在 ~VBScript 中通過 {{{["add", "sub"]}}} 這種方式構造 ~JScript 陣列。因此,要調用帶有這個參數的 {{{create}}} 方法,用 ~JScript 語言編寫會比較方便。但是,如果你一定要用 ~VBScript 編寫,也是能夠完成的,你可以先創建一個 ~VBScript 的陣列,然後給它的各個元素賦值,最後調用 {{{VBArrayToJSArray}}} 方法即可得到 ~JScript 陣列。{{{VBArrayToJSArray}}} 是 PHPRPC 3.0 for ASP 中提供的一個 ~VBScript 和 ~JScript 之間容器類型轉換的函數,關於更多這方面的內容,我們將在後面詳細介紹。
最後,如果伺服器端方法有輸出重定向內容,你可以用 {{{getOutput}}} 方法來獲取它。如果伺服器端沒有輸出內容,該方法會返回空字串。
通過這個例子,我想你已經可以掌握 PHPRPC for ASP 客戶端的基本使用方法了。
!!如何在調用 PHPRPC 服務時,進行引用參數傳遞?
我們可以直接調用 {{{PHPRPC_Client}}} 物件的 {{{invoke}}} 方法進行引用參數傳遞的遠端程序呼叫。上面的例子中,我們是通過遠端代理方法來進行遠端程序呼叫的,而代理方法本身實際上也是通過調用 {{{PHPRPC_Client}}} 的 {{{invoke}}} 方法來實現遠端調用的。下面我們來通過一個簡單的例子,看一下如何直接用 {{{invoke}}} 方法來調用遠端過程,並實現引用參數傳遞。這個例子中我們用 ASP 版本的客戶端來調用 PHP 版本的伺服器端。下面是伺服器端的 PHP 代碼:
<code php>
<?php
include('phprpc_server.php');
function inc(&$n) {
$n++;
}
$phprpc_server = new PHPRPC_Server();
$phprpc_server->add('inc');
$phprpc_server->start();
?>
</code>
這個伺服器發佈了一個 {{{inc}}} 方法,該方法是將參數值加一。這個方法是需要引用參數傳遞的。下面我們來看看如何在 ASP 中調用這個遠程過程:
<code vb>
<%@ CodePage = 65001 %>
<script runat="server" language="JScript" type="text/javascript" src="phprpc_client.js"></script>
<script runat="server" language="VBScript">
Dim client, args
Set client = PHPRPC_Client.create("http://127.0.0.1/inc.php")
args = Array(1)
Set args = VBArrayToJSArray(args)
client.invoke "inc", args, true
args = JSArrayToVBArray(args)
Response.write(args(0))
</script>
</code>
這個例子我們是使用 ~VBScript 編寫的,如果用 ~JScript 編寫會更簡單一些。因為 {{{invoke}}} 方法的第二個參數是 ~JScript 陣列物件類型,所以 ~VBScript 的陣列是不能直接帶入的,需要先用 {{{VBArrayToJSArray}}} 方法轉換成 ~JScript 的陣列類型,轉換後的結果是一個物件,所以需要在賦值時使用關鍵字 {{{Set}}},否則運行結果將會變得異常。當 {{{invoke}}} 方法的第三個參數設置為 {{{true}}} 時,表示的就是引用參數傳遞。當 {{{invoke}}} 方法調用完成後,{{{args}}} 中的參數值就已經改變了。但是如果需要顯示出來的話,我們還需要把 {{{args}}} 從 ~JScript 的陣列類型轉換回 ~VBScript 陣列類型,否則我們無法在 ~VBScript 中訪問 ~JScript 陣列中的元素,轉換用到的是 {{{JSArrayToVBArray}}} 這個函數,這個函數也是 PHPRPC 3.0 for ASP 中提供的。
除了在引用參數傳遞時會用到 {{{invoke}}} 方法外,還有一種情況可能會用到它(極其罕見)。那就是當你的遠端方法名稱與 {{{PHPRPC_Client}}} 物件中的方法名稱重名的時候,假如你有一個遠端方法叫做 {{{setProxy}}},這時就會跟 {{{PHPRPC_Client}}} 中的 {{{setProxy}}} 方法有衝突了,當遇到這種情況,使用 {{{invoke}}} 方法就可以正確調用到遠端方法了。
!!如何來得到遠端過程執行的錯誤資訊?
PHPRPC 把錯誤分為兩種,一種是致命錯誤,一種是警告性錯誤。
當遠端過程執行發生致命錯誤時,遠端程序呼叫的返回值是一個 {{{PHPRPC_Error}}} 類型的物件,它其中包含了遠端過程執行時發生的致命錯誤資訊。
當遠端過程執行發生警告性錯誤時,你可以通過 {{{PHPRPC_Client}}} 物件的 {{{getWarning}}} 方法來得到警告錯誤,{{{getWarning}}} 方法的返回的值也是 {{{PHPRPC_Error}}} 類型的物件。如果沒有發生警告錯誤,{{{getWarning}}} 方法返回 {{{null}}}。
!!遠端程序呼叫中的超時問題如何解決?
PHPRPC for ASP 客戶端在進行遠端程序呼叫時,默認的超時時間為 30 秒,如果你的網路線路不好,或者需要傳遞的資料量較大,或者伺服器端處理資料的時間較長,則在執行過程中會發生超時錯誤。這時,只需要調用 {{{PHPRPC_Client}}} 物件的 {{{setTimeout}}} 方法來設置超時時間就可以了,單位是毫秒。
{{{setTimeout}}} 設置的超時時間是資料發送和資料接收的總時間,不包含功能變數名稱解析和與伺服器建立連接的時間。
!!遠程程序呼叫中參數長度是否有限制?
PHPRPC 客戶端本身沒有對參數長度做限制,但是因為 PHPRPC 協定是通過 HTTP 協定進行傳輸的,如果 PHPRPC 伺服器端的設置限制了 HTTP POST 資料的長度,則調用過程中,如果參數序列化之後的長度大於了這個限制,就會調用失敗。
另外,如果伺服器沒有限制 POST 資料的大小,則客戶端可以提交任意大小的資料,直到伺服器記憶體耗盡才會出錯。
PHPRPC 傳輸的資料量通常小於同樣調用的 Web Service 傳輸的資料量的。因此,PHPRPC 調用所允許的參數長度一般比 Web Service 調用所允許的參數長度更大。
PHPRPC for ASP 中除了伺服器和客戶端實現之外,還提供了一些有用的函數和物件,該工具包除了在伺服器和客戶端的實現中被調用之外,你也可以單獨使用它們。下面是關於該工具包的一些常見問題解答。
!!~VBScript 與 ~JScript 的一些轉換輔助函數
前面在介紹客戶端時,我們已經提到了兩個函數 {{{VBArrayToJSArray}}} 和 {{{JSArrayToVBArray}}},這兩個函數可以將 ~VBScript 中的陣列和 ~JScript 中的陣列進行相互轉換。這兩個函數主要在 ~VBScript 中使用。
當使用 {{{VBArrayToJSArray}}} 時,如果要將結果賦值給 ~VBScript 的變數,需要使用 {{{Set}}} 關鍵字,而 {{{JSArrayToVBArray}}} 的返回結果不需要。
{{{JSArrayToVBArray}}} 的返回值不能賦值給用 {{{Dim ()}}} 和 {{{Redim ()}}} 定義的 ~VBScript 陣列變數。
{{{VBArrayToJSArray}}} 支援將 ~VBScript 中的多維陣列轉換為 ~JScript 的聯合陣列。但 {{{JSArrayToVBArray}}} 不能將 ~JScript 的聯合陣列轉換為 ~VBScript 的多維陣列,而只能轉換為 ~VBScript 的一維陣列,這時的 ~VBScript 陣列中的元素為 ~JScript 的陣列物件。
另外,在 ~VBScript 中因為沒有 ~JScript 中的 {{{Object}}} 物件,所以通常使用 {{{Scripting.Dictionary}}} 物件來作為字典容器。但是 {{{Scripting.Dictionary}}} 在 PHPRPC 中不支援直接傳遞,所以,要傳遞 {{{Scripting.Dictionary}}} 類型就需要將其跟 ~JScript 的 {{{Object}}} 物件進行相互轉換。這時你可能就需要工具包中的這兩個函數了:{{{DictionaryToObject}}} 和 {{{ObjectToDictionary}}}。
{{{DictionaryToObject}}} 除了把 {{{Scripting.Dictionary}}} 對象轉換為 ~JScript 的 {{{Object}}} 對象以外,還會把 {{{Scripting.Dictionary}}} 物件當中的元素也進行轉換,比如元素類型如果是 ~VBScript 的陣列,則該陣列也會自動轉換為 ~JScript 的陣列。
但是 {{{ObjectToDictionary}}} 不會對元素類型進行轉換。
!!用於 Base64 編碼解碼的函數
在 Firefox 中,內置了兩個用於 Base64 編碼解碼的函數,它們分別是[[btoa|http://developer.mozilla.org/en/docs/DOM:window.btoa]] 和[[atob|http://developer.mozilla.org/en/docs/DOM:window.atob]]。為了跟 Firefox 中的這兩個函數保持一致,在 PHPRPC 3.0 for ASP 中也提供了這兩個函數。{{{btoa}}} 的參數是 binray 字串類型(即字串中的每個字元值都在 0-255 之間),{{{atob}}} 的結果是 binary 字串類型。
!!XXTEA 物件如何使用?
{{{XXTEA}}} 物件提供了加密解密資料的方法。{{{encrypt}}} 是加密資料的方法,第一個參數是原文資料,第二個參數是密鑰;{{{decrypt}}} 是解密資料的方法,第一個參數是密文資料,第二個參數是密鑰。這兩個方法的參數和返回值都是 binray 字串類型。
!!~PHPSerializer 物件如何使用?
{{{PHPSerializer}}} 包含 {{{serialize}}} 和 {{{unserialize}}} 兩個方法。{{{serialize}}} 可以將物件序列化,而 {{{unserialize}}} 可以把序列化的內容還原為物件。它除了支持 {{{Number}}}、{{{String}}}、{{{Date}}}、{{{Array}}}、{{{Object}}} 等 ~JScript 的內置類型以外,也支援自定義類型的序列化。您通常不需要直接使用這兩個方法,在進行遠端程序呼叫時,PHPRPC 會自動調用它們來完成序列化和反序列化的工作。
!! PHPRPC for ASP 是否支援自定義類型傳輸?
當然支持!所有在 ~JScript 中通過非匿名 {{{function}}} 定義的類型(對構造函數做 {{{new}}} 操作之後會得到該類型的物件)都可以序列化傳輸。但在 ~VBScript 中通過 {{{Class}}} 定義的類型不支持序列化。因此當您在 ASP 中使用 PHPRPC 要傳遞自定義類型的物件時,推薦選擇 ~JScript 作為首選語言。
在 ~JScript 中通過匿名 {{{function}}} 定義的類型也支援序列化,但是反序列化後的類型為 {{{Object}}} 類型(對應於其他語言的字典或聯合陣列類型)。
自定義類型的名稱與 {{{function}}} 定義的構造函數名相同。
~JScript 中沒有名字空間的概念,不過仍然可以通過類比的方式實現。但如果要讓 PHPRPC 支持這種類比的名稱空間,需要在定義構造函數名時遵守以下約定:將包含名稱空間的完整類型名稱中的“.”號用“_”替換,將替換之後的字串作為構造函數名。例如:
<code js>
// top namespace
var MySpace = {};
// sub namespace
MySpace.util = {};
// full type name
MySpace.util.MyType = (function () {
// Hide MySpace_util_MyType
return function MySpace_util_MyType() {
...
}
})();
</code>
當這個類型的物件序列化後,就會被序列化為類型名稱為 {{{MySpace_util_MyType}}} 的一個物件了,而反序列化時,他會自動尋找 {{{MySpace.util.MyType}}} 這個名字來進行反序列化。這樣反序列化後,還可以得到原來該類型中定義的方法和屬性,因此通過這種方式,~JScript 中定義的類型就可以與 java、.NET 這種帶有名稱空間的語言中定義的類型進行交換了。這種命名約定在 PHPRPC 3.0 for ~JavaScript 中同樣適用。
PHPRPC 還支援自定義序列化方式,自定義序列化方式有兩種,一種是通過在自定義類型中實現 {{{__sleep}}} 和 {{{__wakeup}}} 這兩個方法來自定義序列化方式,這種較為簡單,但這種方式只能限制對哪些欄位進行序列化,而不能選擇序列化格式。另一種是在其他支援介面繼承的語言中通過為自定義類型實現 {{{Serializable}}} 介面來實現自定義序列化方式,這種較為複雜,允許自定義序列化格式。但是因為 ~JScript 是不支援介面繼承的,不過它仍然兩種方式都支援。只是第二種方式下,只需要在自定義類型中實現 {{{serialize}}} 和 {{{unserialize}}} 這兩個方法就可以了。
下面我們分別對這兩種方式進行一下簡要的介紹:
{{{__sleep}}} 方法是在物件被序列化之前自動觸發的。它的返回值類型是一個字串陣列,陣列的每一個元素是需要序列化的欄位名。你除了在 {{{__sleep}}} 方法中返回這些欄位之外,你也可以作其他一些操作,比如關閉檔或者關閉資料庫連接等操作。
{{{__wakeup}}} 方法是在物件被反序列化之後自動觸發的,它被觸發時,所有序列化的欄位都已經被反序列化完畢,所以,你在 {{{__wakeup}}} 中要做的是反序列化的事後工作,比如打開檔或著恢復資料庫連接等操作。
{{{__sleep}}} 和 {{{__wakeup}}} 方法無需同時實現,你只需把你用到的方法實現即可。
{{{serialize}}} 方法也是在物件被序列化時自動觸發的,你可以在 {{{serialize}}} 方法中對該物件做任何方式的序列化,例如 SOAP、JSON 或者 WDDX 等,只要返回的結果是 binray 字串就可以了。
{{{unserialize}}} 方法是在物件被反序列化時自動觸發的,傳入的參數就是前面 {{{serialize}}} 方法的返回內容。你可以用同樣的方式將它反序列化。{{{unserialize}}} 方法沒有返回值。
這兩種方式最多只能任選其一,如果兩種方式都實現的話,只有 {{{serialize}}} 和 {{{unserialize}}} 方式生效。
PHPRPC for ASP 下載之後,在 asp 目錄下的是所有的 PHPRPC for ASP 的源代碼。為了方便開發者調用,asp/compressed 目錄下我們放置了已經打包壓縮好的腳本。所以部署時,只需要把 asp/compressed 下的 2 個檔複製到你的專案目錄中即可,另外,不要忘記把 dhparams 目錄(一定要連同目錄)也一起複製到你的專案目錄中,該目錄中存放的是加密傳輸時,生成密鑰的參數。如果忘記複製該目錄,則伺服器端在加密傳輸情況下將無法工作。
Flash 中儘管有提供基於 amf 的 Flash Remoting 技術可以實現遠端調用,但是 Flash Remoting 的 Java 和 .NET 伺服器並不是開源免費的,而開源免費的 PHP 伺服器又相當難用。最大的問題是,Flash Remoting 被限制在了 Flash 這個應用平臺上,利用 Flash Remoting 技術很難做到一個伺服器服務於多種類型的客戶端。而有了 PHPRPC 之後,這一切麻煩都消失了。它不但可以有效的取代 Flash Remoting,而且比 Flash Remoting 使用更方便(即使是在 Flash 中),功能更強大,並可以服務於多種類型的客戶端。
下面我們就來看一下,在 Flash/Flex 中如何來使用 PHPRPC 開發應用吧。
*[[PHPRPC for ActionScript 的安裝]]
*[[PHPRPC for ActionScript 客戶端]]
*[[PHPRPC for ActionScript 常見問題解答]]
PHPRPC for ~ActionScript 2.0 和 PHPRPC for ~ActionScript 3.0 用法基本一致,但也有不同之處,先讓我們從最基本的開始說起吧。
!!如何調用 PHPRPC 服務
我們先通過一個簡單的例子,來介紹如何調用 PHPRPC 服務。
下面是 ~ActionScript 2.0 中的調用:
<code as3>
import org.phprpc.PHPRPC_Error;
import org.phprpc.PHPRPC_Client;
var client:PHPRPC_Client = new PHPRPC_Client('http://localhost:8080/index.aspx', ['add', 'sub']);
client.keyLength = 256;
client.encryptMode = 2;
client.add(1, 2, function (result, args, output, warning) {
trace(result.toString());
});
client.sub(1, 2, function (result, args, output, warning) {
trace(result.toString());
});
</code>
下面是 ~ActionScript 3.0 中的調用:
<code as3>
import org.phprpc.PHPRPC_Error;
import org.phprpc.PHPRPC_Client;
var client:PHPRPC_Client = new PHPRPC_Client('http://localhost:8080/index.aspx', ['add', 'sub']);
client.keyLength = 256;
client.encryptMode = 2;
client.add(1, 2, function (result:*, args:Array, output:String, warning:PHPRPC_Error):void {
trace(result.toString());
});
client.sub(1, 2, function (result:*, args:Array, output:String, warning:PHPRPC_Error):void {
trace(result.toString());
});
</code>
大家會發現上面這兩段代碼唯一不同的地方就是 ~ActionScript 3.0 版本的回調函數參數類型更明確了,當然這也是語言本身要求的,因此也可以說它們在使用上是沒有區別的。
{{{PHPRPC_Client}}} 物件的 {{{keyLength}}} 和 {{{encryptMode}}} 這兩個屬性是跟加密傳輸有關的。
{{{keyLength}}} 方法用於設置[[密鑰長度]]。
{{{encryptMode}}} 方法用於設置[[加密模式]]。
上面設置密鑰長度、加密模式都是可選項,如果你不需要這些功能,可以直接忽略它們。
PHPRPC 3.0 for ~ActionScript 客戶端與 Java、.NET 客戶端不同,它不需要使用 {{{useService}}} 來返回指定介面的遠端代理物件,~ActionScript 客戶端本身就是一個代理物件。所以,上面例子中 {{{client.add}}} 和 {{{client.sub}}} 這兩個調用實際上調用的就是遠端方法,對於 ~ActionScript 客戶端來說,遠端方法名不需要事先聲明,但是還是建議像上面那樣直接在客戶端代碼中指定所需要調用的遠端方法名,這樣可以避免當你使用一個不存在的方法時要到伺服器端做檢查,而不是在客戶端做檢查。
回調函數有四個參數,你可以認為它們是伺服器端方法執行之後返回的內容。
第一個參數 {{{result}}} 是伺服器端方法(函數)的返回值,它可以是任意類型。但如果伺服器端返回字串類型的資料的話,一定要用 {{{toString()}}} 方法進行明確轉換才能得到正確結果,否則得到的是 {{{ByteArray}}} 類型的資料```包括陣列、物件中的字串類型資料也是一樣要用 {{{toString()}}} 明確轉換的。```。
第二個參數 {{{args}}} 是方法調用的參數,如果這個調用是一個引用參數傳遞的調用,參數也有可能被修改,這時候,你可以通過 {{{args}}} 來獲得修改後的參數,關於引用參數傳遞的調用我們後面會做進一步說明。
第三個參數 {{{output}}} 是伺服器端輸出的內容,它是字串類型的,無需用 {{{toString()}}} 方法明確轉換為字串。
第四個參數 {{{warning}}} 是伺服器端發生的警告錯誤(目前只有 PHP 伺服器會產生警告錯誤),一般只調試過程中可能會用到。
通過這個例子,我想你已經可以掌握 PHPRPC for ~ActionScript 客戶端的基本使用方法了。
!!如何在調用 PHPRPC 服務時,進行引用參數傳遞?
引用參數傳遞實際上非常簡單,看下面這個例子,首先來看 PHP 的伺服器端:
<code php>
<?php
include('phprpc_server.php');
function inc(&$n) {
$n++;
}
$phprpc_server = new PHPRPC_Server();
$phprpc_server->add('inc');
$phprpc_server->start();
?>
</code>
這個伺服器發佈了一個 {{{inc}}} 方法,該方法是將參數值加一。這個方法是需要引用參數傳遞的。下面我們來看看如何在 ActionScript 中調用這個遠程過程:
''~ActionScript 2.0''
<code as3>
import org.phprpc.PHPRPC_Error;
import org.phprpc.PHPRPC_Client;
var client:PHPRPC_Client = new PHPRPC_Client('http://localhost/index.php', ['inc']);
client.inc(1, function (result, args, output, warning) {
trace(args[0]);
}, true);
</code>
''~ActionScript 3.0''
<code as3>
import org.phprpc.PHPRPC_Error;
import org.phprpc.PHPRPC_Client;
var client:PHPRPC_Client = new PHPRPC_Client('http://localhost/index.php', ['inc']);
client.inc(1, function (result:*, args:Array, output:String, warning:PHPRPC_Error):void {
trace(args[0]);
}, true);
</code>
其實很簡單,只要在回調函數之後跟一個 {{{true}}} 參數就可以了。這個 {{{true}}} 就是表示啟用引用參數傳遞。
!!如何來得到遠端過程執行的錯誤資訊?
PHPRPC 把錯誤分為兩種,一種是致命錯誤,一種是警告性錯誤。
當遠端過程執行發生致命錯誤時,遠端程序呼叫的返回值是一個 {{{PHPRPC_Error}}} 類型的物件,它其中包含了遠端過程執行時發生的致命錯誤資訊。
當遠端過程執行發生警告性錯誤時,你可以通過回調函數的第四個參數 {{{warning}}} 得到警告錯誤,{{{warning}}} 的值也是 {{{PHPRPC_Error}}} 類型的物件。如果沒有發生警告錯誤,warning 為 {{{null}}}。
!!PHPRPC for ~ActionScript 支持 Flash Lite 嗎?
支持 Flash Lite 2.0 以上的版本。不過因為 Flash Lite 本身現在還不支援 ~ActionScript 3.0,所以你只要使用 PHPRPC for ~ActionScript 2.0 就可以開發 Flash Lite 程式了。
!!PHPRPC for ~ActionScript 支持 AIR 嗎?
支援,不論在 Flash 還是 Flex 中都可以使用 PHPRPC for ~ActionScript 3.0 開發 AIR 程式。因為 PHPRPC for ~ActionScript 3.0 功能比 2.0 要完備,可以算是完美支援 AIR 程式開發!
!!PHPRPC for ~ActionScript 支援傳輸自定義類型的物件嗎?
PHPRPC for ~ActionScript 3.0 可以完美支援傳輸自定義類型的物件。PHPRPC for ~ActionScript 2.0 對於自定義物件作為字典傳輸,接收自定義物件也按照字典接收,但包含一個 name 屬性,其值為該物件的類名。所以,一般情況下,推薦使用 PHPRPC for ~ActionScript 3.0。除非是做 Flash Lite 開發,否則完全沒有必要使用 PHPRPC for ~ActionScript 2.0。
!!PHPRPC for ~ActionScript 3.0 如何支援自定義類型的物件?
PHPRPC for ~ActionScript 3.0 支援動態類和非動態類物件都支援,但是只能傳輸 public 成員和動態成員,不能傳輸 private 和 protected 成員```因為 ~ActionScript 3.0 不支持反射私有和保護成員```。例如下面這個類:
<code as3>
package {
public dynamic class Test {
public var a:String = "Hello";
protected var b:Boolean = true;
private var _x:int = 1;
public function get x() {
return _x;
}
public function set x(value:int) {
_x = value;
}
}
}
</code>
只有欄位 {{{a}}} 和屬性 {{{x}}} 會被傳輸,{{{b}}} 和 {{{_x}}} 不會被傳輸。對於屬性來說,必須滿足可讀、可寫並且都是 public 的才可以被傳輸。
關於類名的規定與 PHPRPC for Java 類似:將完整名(包名+類名)中的分隔符號({{{.}}} 和 {{{::}}})全部替換為 {{{_}}} 後,如果得到的名稱相同則認為是相同的類。
PHPRPC for ~ActionScript 3.0 支持 {{{__sleep}}} 和 {{{__wakeup}}} 這兩個與序列化相關的魔術方法。同樣還支持通過實現 {{{org.phprpc.util.Serializable}}} 介面來實現自定義序列化。因為這兩個特性跟 PHPRPC for Java 相似,而且很少用到,這裏就不再詳細講解,感興趣的讀者可以參見 PHPRPC for Java 相關章節的內容。
!!PHPRPC for ~ActionScript 支援字元集設置嗎?
PHPRPC for ~ActionScript 2.0 不支援字元集設置,只能用 ~UTF-8 字元集與伺服器通訊。PHPRPC for ~ActionScript 3.0 支援字元集設置,可以以任何字元集跟伺服器通訊。但仍然建議用 ~UTF-8,因為更通用效率也更高。
PHPRPC for ~ActionScript 有兩個版本,它們分別是針對 ~ActionScript 2.0 和 ~ActionScript 3.0 的。在安裝方面,它們基本上沒有什麼區別,所以這裏按開發環境來分別介紹。
!!Flash 8 到 Flash ~CS3 的安裝方法
首先確認你已經安裝了相應版本的 [[Extension Manager|http://www.adobe.com/cn/exchange/em_download/]],然後安裝 PHPRPC for ~ActionScript 2.0 只需要雙擊 ~PHPRPC_AS2.mxp 即可完成安裝,安裝 PHPRPC for ~ActionScript 3.0 只需要雙擊 ~PHPRPC_AS3.mxp 即可完成安裝。安裝之後可以在元件面板的 Data 組中找到,直接把它拖到你的庫中就可以在你的程式中使用了。
!!Flash ~CS4 的安裝方法
同樣首先確認你已經安裝了Adobe Extension Manager ~CS4,一般來說,安裝 Flash ~CS4 時就會一同安裝好的。然後安裝 PHPRPC for ~ActionScript 2.0 只需要雙擊 ~PHPRPC_AS2_CS4.mxp 即可完成安裝,安裝 PHPRPC for ~ActionScript 3.0 只需要雙擊 ~PHPRPC_AS3_CS4.mxp 即可完成安裝。安裝之後可以在元件面板的 Data 組中找到,直接把它拖到你的庫中就可以在你的程式中使用了。
!!Flex Builder 下的安裝方法
實際上,在 Flex Builder 下無需安裝,只要在你創建程式工程時,將 ~PHPRPC_AS2.swc 或 ~PHPRPC_AS3.swc```根據你的程式使用哪個版本的 ~ActionScript 來決定包含那個 swc 檔,不要同時包含。``` 添加到庫中就可以使用了。
本章所述的 PHPRPC for Delphi 是針對 Delphi 6 - 2009 原生程式開發版本的。如果你想將 PHPRPC 用於 Delphi.NET,請參見 [[PHPRPC for .NET]] 章節的相關內容。
*[[PHPRPC for Delphi 的安裝]]
*[[PHPRPC for Delphi 客戶端]]
*[[PHPRPC for Delphi 對容器類型的支援]]
*[[PHPRPC for Delphi 對自定義類型的支援]]
另外,PHPRPC 還提供了 Lazarus(Free Pascal)版本,Lazarus 與 Delphi 很像,你可以認為它是一個開源版本的 Delphi。只不過現在的 Lazarus 還沒有 Delphi 那樣穩定。PHPRPC for Lazarus(Free Pascal)的用法與 for Delphi 版本的用法基本一致。這裏就不再單獨介紹了。
為了方便講解,這裏給出的演示是控制臺程式:
<code delphi>
program Example;
{$APPTYPE CONSOLE}
uses
PHPRPC;
var
client: TPHPRPC_Client;
clientProxy: Variant;
intArray: array of Integer;
arraylist: TArrayList;
vhashmap: Variant;
ohashmap: THashMap;
i: Integer;
begin
// 創建客戶端物件,如果是圖形介面程式,直接拖放元件到 Form 即可。
client := TPHPRPC_Client.Create;
// 返回遠端代理物件,通過它就可以直接調用遠端方法了。
// 因為實際上該代理物件是 client 物件的 Variant 包裝,所以該代理物件不需要單獨釋放。
clientProxy := client.useService('http://localhost/index.aspx');
// 設置交換密鑰的長度(可選,默認 128)
client.KeyLength := 256;
// 設置加密模式(0 - 不加密、1 - 單向加密、2 - 雙向加密、3 - 雙向且輸出加密),默認為 0
client.EncryptMode := 1;
// 數字參數
writeln(clientProxy.add(1, 2));
// 英文字串參數
writeln(clientProxy.add('foo', 'bar'));
// Delphi 7 中文字串參數要轉換為UTF8編碼
writeln(UTF8ToAnsi(clientProxy.Hello(AnsiToUTF8('馬秉堯'))));
// Delphi 2009 中文字串參數默認為UnicodeString
// writeln(UTF8ToAnsi(clientProxy.Hello('馬秉堯')));
// 中文的伺服器輸出的也是UTF8編碼,本地列印要轉換為本地編碼
writeln(UTF8ToAnsi(client.Output));
// 傳遞動態陣列參數
setLength(intArray, 10);
// 主要不要越界
for i := 0 to 9 do intArray[i] := Random(100000);
// 返回值是 Variant 包裝的 THashMap
vhashmap := clientProxy.sort(Variant(intArray));
// 這裏不能用索引方式訪問
for i := 0 to 9 do writeln(vhashmap.get(i));
writeln;
// 使用完之後釋放掉, 以免造成記憶體洩漏
// 但動態陣列不需要顯式釋放,它會在程式運行結束後自動釋放
vhashmap.Free;
// 傳遞陣列列表參數
// 注意這裏初始化了10個元素的空間,但是實際用了11個元素
// 因為陣列列表有自增長的特性,因此不會發生越界訪問的情況
// 但上面的動態陣列則不可以越界訪問,否則會發生意想不到的錯誤
arraylist := TArrayList.Create(10);
for i := 0 to 10 do arraylist[i] := Random(100000);
vhashmap := clientProxy.sort(arraylist.ToVariant);
// 這裏不能用索引方式訪問
for i := 0 to 10 do writeln(vhashmap.get(i));
writeln;
// 可以顯式通過 FromVariant 方法轉換成 THashMap 物件
ohashmap := THashMap(THashMap.FromVariant(vhashmap));
// 這裏不能用get方法訪問
for i := 0 to 10 do writeln(ohashmap[i]);
writeln;
// 釋放掉原來的未排序陣列
arraylist.Free;
// 將返回結果轉換為 TArrayList 類型
arraylist := ohashmap.ToArrayList;
for i := 0 to 10 do writeln(arraylist[i]);
writeln;
vhashmap.Free;
// vhashmap 釋放後 ohashmap 也一起釋放,反之亦然
// 因此下面注釋掉的語句如果執行的話會出錯(或讓程式崩潰)
// for i := 0 to 10 do writeln(ohashmap[i]);
// 但通過 ToArrayList 轉換得到的是新物件,所以仍然可以使用
for i := 0 to 10 do writeln(arraylist[i]);
// 因此不要忘記釋放資源,以免造成記憶體洩漏
arraylist.Free;
client.Free;
readln;
end.
</code>
因為 Delphi 2009 中引入了 {{{UnicodeString}}},所以不需要 {{{AnsiToUTF8}}} 就可以正確按照 ~UTF-8 編碼來傳輸 {{{string}}} 了。上面的程式中關於中文字串部分已經給出了在不同版本下的解決方案,但是 Delphi 7 的寫法在 Delphi 2009 上也可以繼續使用。不過目前的 PHP 4/5 還不支持 Delphi 中傳輸的 {{{WideString}}} 字串,但將來的 PHP 6 會支持。目前其他所有語言的 PHPRPC 伺服器都已經支持 Delphi 中傳輸的 {{{WideString}}} 字串。
其他需要注意的問題都已經在注釋中寫清楚了,這裏就不再重複了。
儘管 Delphi 的 VCL/CLX 中提供了一些容器類型(比如 {{{TList}}},{{{TObjectList}}},{{{TStringList}}} 等),但是這些容器要麼是針對指標的(無所不包),要麼是針對物件的(基本類型就無法存了),甚至是針對某種特殊類型的(這樣就限制就更大了)。所以這些容器類型與其他語言中的列表或字典類容器進行直接交換就存在一些問題了,不是包括的範圍太大就是範圍太小。
所以,在 PHPRPC for Delphi 中,專門提供了一組用於跟其他語言交換資料的又方便操作的容器類型。下面就對這些類型做一下詳細的介紹。
首先第一個要介紹的是 Delphi 的動態陣列類型,這個不是 PHPRPC for Delphi 中提供的,而是 Delphi 自己提供的。比如 {{{TIntegerDynArray}}},{{{TWordDynArray}}},{{{TDoubleDynArray}}} 等,這些是在 Delphi 的 {{{Types}}} 單元中定義的,如果需要直接引用這個單元就可以使用這些類型。
不過有兩點要注意,第一是 {{{TStringDynArray}}}({{{array of string}}}),它在通過 PHPRPC 傳輸的時候可能並不像你希望的那樣工作,因為 Delphi 本身會將其中的字串轉換成 {{{OLEStr}}} 來進行傳遞,如果你的 String 是二進位字元串,而不是本地編碼的文本,最後的結果可能就不是你期望的了。所以在跟其他語言進行資料交互時,不推薦使用動態字串陣列類型。
第二是 {{{TByteDynArray}}}({{{array of Byte}}}),為了優化該類型傳輸,該類型會以 {{{AnsiString}}}(RawByteString)方式序列化傳輸。在被反序列化時,也會被作為 {{{AnsiString}}} 類型被反序列化。如果希望結果被直接反序列化為 {{{TByteDynArray}}} 類型,則可以將 TPHPRPC_Client 物件的 {{{StringAsByteArray}}} 屬性設置為 {{{True}}} 即可。但對於自定義類型的屬性,不論是 {{{AnsiString}}} 還是 {{{TByteDynArray}}} 類型,都無需設置 {{{StringAsByteArray}}},PHPRPC 會自動按照正確的類型反序列化。
PHPRPC for Delphi 除了支援動態陣列外,還提供了更加高級的容器類型,它們是 TArrayList,THashedArrayList 和 THashMap。另外,還有一個字串操作的幫助類 TStringBuffer。
TArrayList 類似於動態陣列 TVariantDynArray,可以保存的元素也是 Variant 類型,但是 TArrayList 提供了一些方法允許你方便的添加、刪除、查找、修改、移動元素,並且容器大小是可自動增長的。
THashedArrayList 是 TArrayList 的一個子類,它跟 TArrayList 實現的操作是一樣的,不過它在存取下標不連續的陣列時,效率更高。{{{IndexOf}}} 操作效率也更高,THashedArrayList 效率為 O(1),而 TArrayList 為 O(n)。
THashMap 可以讓你通過一個 {{{Variant}}} 變數來索引另一個 {{{Variant}}} 變數,不過通常用來做索引的是整數或者字串。如果要通過 PHPRPC 傳遞的話,那麼索引必須是整數或字串類型,其他類型的索引會被忽略。
通過 PHPRPC 調用其他語言發佈的服務時,如果其他語言返回的類型是陣列、列表或者字典等類型的資料,在 Delphi 中接收到之後都會作為 THashMap 返回,如果接收到的是一個物件類型,當這個物件類型在 Delphi 中沒有對應的定義時,也將作為 THashMap 返回。如果你希望以 TArrayList 來操作返回值,可以用 THashMap 的 {{{ToArrayList}}} 方法將結果轉換為 TArrayList 類型,這個 {{{ToArrayList}}} 返回的是一個獨立的 TArrayList 物件,使用後注意要用 {{{Free}}} 方法將它釋放,否則會產生記憶體洩漏。
TStringBuffer 就不用詳細介紹了,它沒有什麼特別的,就是一個用來可以方便修改二進位字元串的幫助類,通過它的 {{{ToString}}} 方法可以得到最後的字串。
這些類型都是 TPHPObject 的子類,都可以轉化成 {{{Variant}}} 類型。瞭解了這些容器類型以後,用 PHPRPC for Delphi 寫程式就會方便多了。
PHPRPC for Delphi 中除了支持基本資料類型、容器類型傳遞以外,同樣也支持自定義類型的傳遞,而能夠進行傳遞的自定義類型的基類就是 TPHPObject。這個類是在 PHPRPC 單元中定義的。下面我們就對這個類進行一下深入的剖析。
首先第一個問題,為什麼要以 TPHPObject 作為 PHPRPC 傳輸的自定義類型的基類,而不是 {{{TPersistent}}} 呢?
因為 PHPRPC 支援傳輸的資料類型除了物件類型外,還有基本資料類型,為了可以使它們用同一種方式傳輸,最簡單的方式就是把他們都變成 {{{Variant}}} 類型。而 {{{TPersistent}}} 類是不能轉換成 {{{Variant}}} 類型的,而且 {{{TPersistent}}} 提供的持久化方式也與 PHPRPC 所支援的序列化方式不同,因此才沒有使用 {{{TPersistent}}},而是通過定義一個新的類 TPHPObject 作為 PHPRPC 支持的可傳輸自定義類型的基類。
TPHPObject 有一個無參 {{{Create}}} 方法,因為在反序列化該物件時,是需要調用這個無參構造方法來創建物件的。另外,它還有一個繼承自 {{{TComponent}}} 的帶有 {{{AOwner}}} 參數的 {{{Create}}} 方法。一般情況下,無需覆蓋這兩個方法,除非你需要初始化一些資料。
該類中提供的其他方法如果沒有特殊需求,一般也都不需要覆蓋,只需要定義需要序列化的屬性就可以了,例如:
<code delphi>
TUser = class(TPHPObject)
private
FId: Integer;
FName: AnsiString;
FPassword: AnsiString;
FBirthday: TDateTime;
published
property ID: Integer read FId write FId;
property Name: AnsiString read FName write FName;
property Password: AnsiString read FPassword write FPassword;
property Birthday: TDateTime read FBirthday write FBirthday;
end;
</code>
這個類中,{{{ID}}},{{{Name}}},{{{Password}}} 和 {{{Birthday}}} 這四個屬性在傳輸時就會自動被序列化了,如果某個屬性你不希望它被序列化(比如它是一個唯讀屬性),使用 {{{stored}}} 指令將其設置為 {{{False}}} 就可以了。注意,要序列化的屬性必須是 {{{published}}} 的,而不能是 {{{public}}} 的。
另外,TPHPObject 有一個 {{{Properties}}} 屬性(它不是 {{{published}}} 的,所以無需擔心它在傳輸時會被一起序列化),通過該屬性,你可以通過屬性名的字串表示來訪問屬性值,返回值都是 {{{Variant}}} 類型的。
是否這樣定義之後就可以了在 PHPRPC 中傳輸它了呢?不,還需要做一步小工作,那就是註冊它。TPHPObject 提供了一個類方法 {{{RegisterClass}}} 用於註冊類自身。例如:
<code delphi:nocontrols>
TUser.RegisterClass('User');
</code>
就將 {{{TUser}}} 類註冊成了別名為 User 的類。
那這個別名是幹啥的呢?因為在其他語言中定義類的命名規則可能跟 Delphi 中不同,例如 PHP 或者 .NET 中,是沒有在類名前加 ''T'' 這個約定的,但是在 Delphi 的類基本上都是以 ''T'' 開頭的,為了能夠跟其他語言互通,所以就提供了這樣一個別名機制,註冊之後就可以跟其他語言中的具有同樣屬性(或欄位)的 User 類進行交互了。但是在 Delphi 中,你仍然使用的是 {{{TUser}}} 這個類。
另外,向 .NET 有名空間的概念,Java 也有包的概念,比如你定義的 {{{TUser}}} 如果要跟 .NET 或 Java 中的 {{{myNameSpace.mySubNameSpace.User}}} 那麼在註冊時,註冊為:
<code delphi:nocontrols>
TUser.RegisterClass('myNameSpace_mySubNameSpace_User');
</code>
就可以了,注意這裏把 {{{.}}} 變成了 {{{_}}}。
如果類名與其他語言相同是否可以省略這個註冊過程呢?不可以的,因為在反序列化時只有註冊過的類才能被查找到,否則就不能夠被反序列化了(簡單的說就是沒有經過註冊的類可以作為參數傳出,但不能作為結果返回)。不過這種同名的註冊可以簡化一下,直接執行不帶參數的 {{{RegisterClass}}} 方法就可以了。```如果在定義這些類的單元的 {{{initialization}}} 段中加入這些註冊語句的調用的話,就可以避免在使用時忘記註冊它們了。```
如何創建 {{{TUser}}} 的物件呢?當然你可以使用 {{{Create}}} 來創建了,但是這樣創建的物件是一個普通的物件,而不是一個 {{{Variant}}} 物件。那麼怎樣才能將它變成一個 {{{Variant}}} 物件呢?TPHPObject 提供了兩種方式。一種是使用 {{{New}}} 方法(注意:這不是一個運算符),另一種是使用 {{{ToVariant}}} 方法。實際上 {{{New}}} 方法所做的就是在調用了 {{{Create}}} 方法之後又調用了 {{{ToVariant}}} 方法。因此如果你定義了帶參的 {{{Create}}} 構造方法,可以自己同時定義一個帶參的 {{{New}}} 方法,這樣創建可傳遞的 {{{Variant}}} 物件就方便多了。
不過有一點大家要注意,Delphi 是沒有自動垃圾回收機制的,因此當你創建了物件之後,在你不再需要它時,記得調用 {{{Free}}} 方法將它釋放。TPHPObject 的 {{{Variant}}} 類型,在賦值時或傳參時是引用傳遞的,而不是克隆對象,因此在你 {{{Free}}} 之後,它的所有副本也都不可以再用。
那如何將一個 {{{Variant}}} 表示的 TPHPObject 子類物件變成一個普通物件呢?也很簡單,TPHPObject 提供了一個 {{{FromVariant}}} 類方法,可以用它來完成這個工作,例如:
<code delphi>
var
user: TUser;
vuser: Variant;
begin
vuser := TUser.New;
user := TUser(TUser.FromVariant(vuser));
user.Free;
end;
</code>
上面這個例子中,你發現我是在 {{{user}}} 上調用的 {{{Free}}} 方法,實際上在 {{{vuser}}} 上調用 {{{Free}}} 方法也可以得到同樣的效果。
那是否可以給這種自定義的類型添加我們自己的方法呢?當然可以。但是你可能會發現,通過 {{{Create}}} 創建出來的物件,可以調用你加的這些方法,而通過 {{{New}}} 創建出來的 {{{Variant}}} 物件卻不能像上面的 {{{Free}}} 方法那樣調用。如何才能使我們自己定義的方法也可以在 {{{Variant}}} 物件上直接被調用呢?也很簡單,TPHPObject 有兩個保護方法:{{{DoFunction}}} 和 {{{DoProcedure}}},只要覆寫(override)它們就可以了。下面是 TStringBuffer 中對這兩個方法的覆寫(override):
<code delphi>
function TStringBuffer.DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean;
var
Ident: string;
begin
Ident := LowerCase(Name);
Result := True;
if Ident = 'readstring' then
Variant(Dest) := ReadString(Variant(Arguments[0]))
else if Ident = 'seek' then
Variant(Dest) := Seek(Variant(Arguments[0]), Variant(Arguments[1]))
else
Result := inherited DoFunction(Dest, Name, Arguments);
end;
function TStringBuffer.DoProcedure(const Name: string;
const Arguments: TVarDataArray): Boolean;
var
Ident: string;
begin
Ident := LowerCase(Name);
Result := True;
if Ident = 'insertstring' then
InsertString(RawByteString(Variant(Arguments[0])))
else if Ident = 'writestring' then
WriteString(RawByteString(Variant(Arguments[0])))
else
Result := inherited DoProcedure(Name, Arguments);
end;
</code>
大家可以以這個為範本在在自己的類中覆寫(override)它們,就可以直接以 {{{Variant}}} 物件的形式來直接調用自定義的方法了。
如果有特殊需求的話,你可能會覆寫的方法大約有這幾個:{{{__sleep}}},{{{__wakeup}}},{{{ToBoolean}}},{{{ToDate}}},{{{ToDouble}}}},{{{ToInt64}}},{{{ToInteger}}} 和 {{{ToString}}}。其中,{{{__sleep}}} 和 {{{__wakeup}}} 與序列化和反序列化有關。{{{ToXXX}}} 的方法是用來進行類型轉換的。一般情況下,你自定義的類是不會轉換為這些類型的({{{ToString}}} 也許是個例外)。
{{{__sleep}}} 方法的返回值是一個 {{{TStringDynArray}}} 類型,它用來做一些序列化之前的工作。如果它的返回值為空({{{nil}}}),則按照默認的方式(就是前面介紹的方式)序列化物件,如果它的返回值不為空,則只序列化那些與返回值中包含的名稱相同的屬性(當然這些屬性必須要出現在 {{{published}}} 聲明部分,否則就會出錯了)。
{{{__wakeup}}} 方法是在反序列化之後被調用,它沒有參數也沒有返回值,它一般用於反序列化之後的掃尾工作。
{{{ToBoolean}}},{{{ToDate}}},{{{ToDouble}}},{{{ToInt64}}} 默認是拋出異常,當然它們都是 {{{protected}}} 的方法,如果你不覆寫它們,你也不會調用到它們。
{{{ToInteger}}} 也是 {{{protected}}} 方法,它返回的是物件的指標位址的整數表示,不要改寫它,它是有特殊用途的。
{{{ToString}}} 默認是返回該物件序列化後的 {{{AnsiString}}} 表示,你當然可以把它覆寫成你需要的形式,覆寫它不會影響序列化的正常工作。
另外,還有一些雖然也是 {{{virtual}}} 的方法,但是你一定不要去試圖覆寫它們,除非你知道你在做什麼。這些方法包括 {{{DoSerialize}}},{{{DoUnSerialize}}},{{{GetProperty}}},{{{SetProperty}}},{{{Equal}}},{{{HashCode}}} 和 {{{ToVariant}}}。
最後,再補充說明一下 ISerializable 介面。如果你的自定義類型繼承自 TPHPObject,同時實現了 ISerializable 介面的話,那麼這個類型就可以使用你自定義的序列化方式傳輸了。關於這種自定義序列化方式,可以參見 PHP 手冊中關於 {{{Serializable}}} 介面的說明,它們的意義和實現的功能是完全相同的,這裏就不再詳細敍述了,因為你可能一輩子都不會用到它。
PHPRPC for Delphi 是以元件形式提供的,目前只提供了客戶端元件,它是非視覺化的元件,所以不論是在視窗程式還是控制臺程式中你都可以使用它。
直接在 Delphi 中安裝該元件包後就可以使用了,不過目前只提供了 Delphi 7 和 Delphi 2009 的元件安裝包。如果你是用其他版本的 Delphi 可以通過自己創建元件包來安裝,或者將現有版本的元件包轉換為你所使用版本的元件包。
安裝元件包非常簡單。如果你是用的是 Delphi 7,打開 ~PHPRPC_D7.dpk 編譯安裝即可。如果你使用的是 Delphi 2009,打開 ~PHPRPC_D2009.dproj 編譯安裝即可。按照之後就可以直接從 Internet 面板上找到 ~PHPRPC_Client 元件了。
Groovy = Java + Scripting
Groovy 自 2004 年發展至今,嘗試擷取其他直譯式語言的部分優點,加上 JDK 十餘年優良傳統的加持,它試圖在開發的領域上多一些樂趣及簡捷。並且把類似 Python Ruby 等的強大功能帶到 Java 的世界裏。
他是完全貼近 Java Platform 的腳本語言。Groovy 可以採用直譯方式以便於開發,或是編譯成位元組碼提升執行效能。終極理想是支援所有的 Java 功能,讓使用者可以同時使用兩者的撰寫方式及程式庫,並將兩者的優點發揮到極致。
*[[PHPRPC for Groovy 的安裝]]
*[[PHPRPC for Groovy 客戶端]]
PHPRPC for Groovy 和 PHPRPC for Java 用法基本一致,先讓我們從最基本的開始說起吧。
!!如何調用 PHPRPC 服務
我們先通過一個簡單的例子,來介紹如何調用 PHPRPC 服務。
<code java>
import org.phprpc.*
import org.phprpc.util.*
class A implements java.io.Serializable {
public String a
}
class B implements java.io.Serializable {
public Integer a
public List b
public String c
}
interface IHello {
public String get_str(str)
public Integer add(Integer a, Integer b)
public Integer get_int()
public List get_array()
public HashMap get_hash()
public String send_obj(A obj)
public B get_obj()
}
client = new PHPRPC_Client("http://phprpc-example.appspot.com/")
client.setKeyLength(512) // 加密傳長度
client.setEncryptMode(1) // 加密模式
clientProxy = client.useService(IHello)
println clientProxy.get_str('Hello World') // 傳回 PHPRPC Example.Hello World
println clientProxy.add(1,2) // 調用遠端函數 add,將列印 3
println clientProxy.get_int() // 調用遠端函數 get_int,將列印12345
println clientProxy.get_array() // 調用遠端函數 get_array,將列印 [1, 2, 3, 4, 5]
println clientProxy.get_hash() // 調用遠端函數 get_hash,將列印 [a:1, c:3, b:2]
obj_a = new A()
obj_a.a = 'test'
println clientProxy.send_obj(obj_a) //調用遠端函數 send_obj,將列印 A.a = test
obj_b = clientProxy.get_obj() //調用遠端函數 get_obj,將列印 B object
println obj_b.a //值為 1
println obj_b.b //值為 [1, 2, 3, 4, 5]
println obj_b.c //值為 PHPRPC Example.
</code>
setKeyLength 用於設置[[密鑰長度]]。
setEncryptMode 用於設置[[加密模式]]。
看完以上範例,相信很快就能使用安全又有效率的 PHPRPC 了!
須要更詳細的說明,請參考 [[PHPRPC for Java 客戶端]]
直接使用 [[PHPRPC for Java]] 的源碼編譯出來的 phprpc_client.jar 放置 Groovy 的 lib 目錄下即安裝完畢。
Java 中似乎並不缺少遠端程序呼叫一類的工具,從最初簡單的只能在 Java 之間通訊的 RMI,到現在各種各樣的 ~WebService 實現,看上去你確實有足夠多的選擇。不過經過一番痛苦的挑選後,你會發現不是功能太簡單以至於不能滿足你的要求,就是複雜到讓你根本不知道如何下手。還有一些你儘管花了大力氣學會了使用它,但當你確實需要用到它那些所鼓吹的特性時(比如可以與各種語言無障礙交互),你會發現你要付出的代價比你最初想像的要多得多。
這方面例子我就親身經歷過一些。當我還在學校工作的時候,一次學校要上一個統一身份認證系統,有幾家公司前來競標,其中有一家是使用所謂的 ~J2EE 實現的。當學校提出我們需要在各種不同語言實現的現有異構系統到這個平臺的統一認證時,他們說已經提供了 ~WebService 介面來實現這一點。這聽上去簡直太棒了。可是當他們真的帶他們的系統來測試時,我們就發現完全不是這麼回事了。他們沒有提供任何可以讓我們在其他語言客戶端中調用他們服務的工具,他們只是不停的強調 ~WebService 是一個標準,只要按照標準去做就可以了。可是事實上,就連他們也根本不知道該如何按照所謂的標準來實現一個可以用的客戶端,他們甚至不知道他們所用的工具(Axis)使用的是哪個版本的 ~WebService 標準。
這還不算最糟糕的,更糟糕的是,他們甚至不理解 ~WebService 本身會將資料序列化為 XML 傳輸。所以,他們在使用 Axis 發佈他們的服務時,居然傳遞的參數和結果都是他們自己定義的 XML 字串,他們要在自己的程式中將物件構造成 XML,之後,Axis 把他們處理後的 XML 又一次包裝成了更加臃腫的 XML,而當他們獲取資料時,Axis 把收到的資料作為 XML 解析,但解析後仍然是一個 XML,接著他們自己的程式再將經過一次解析後的 XML 解析為對象。也就是說,他們用 XML 包裝了 XML!這是一個多麼愚蠢、多麼可笑的對 ~WebService 的應用啊!可是當我問他們為什麼要這樣愚蠢的```當然在我問他們時,並沒有在嘴上說出這個形容詞,但在心裏不知道已經說了多少遍了```來使用 ~WebService 時,他們的回答更是讓我吃驚,他們居然說如果不這樣做,根本不能傳遞物件!我想事實上並不是不能夠傳遞物件,而是因為它太難用了,以至於讓人根本無法掌握傳遞物件的辦法。
當然,如果你是 Java ~WebService 這方面的專家的話,你可能會說,這不是 ~WebService 的問題,而是因為這些使用者太愚蠢了。或者你還會責怪他們所使用的工具太過時了,如果他們用 ~CXF2 這類新的 ~WebService 工具事情可能就會簡單很多了。可是事實真的是這樣嗎?你能告訴我你到底用了多少時間才成為了這方面的專家嗎?是幾個星期?還是幾個月?甚至幾年?是的,要成為這方面的專家,你確實需要一段很長的時間。
從後來的例子,也證實了我的這個觀點,並不是開發者們愚蠢,而是現在的那些 ~WebService 實在太難以理解和應用了。那是我在學校參與的最後一個項目——一個省級的考試報名系統,這個專案也是基於 ~J2EE 來開發的。這個系統需要在每個高校部署一個獨立的報名系統,在省教育廳部署一個集中的伺服器處理上報來的資料,這需要在各個高校與省教育廳之間進行資料交互。另外,這兩個部分並不是都由我們來完成的,而是分別由兩個高校做。我們學校負責高校報名系統,而省教育廳的部分由另一個比我們學校實力更強的高校來負責。
當然到了這個系統最終要整合時,我們和另一個高校就需要坐下來討論該用何種方式在兩個相對獨立又緊密聯繫的系統之間交換資料了。他們首先想到的當然是 ~WebService,並且跟我們大談了一番 ~WebService 的好處,我們確實差一點被他們說動了,讓我們認為他們確實有這方面充足的開發經驗和良好體驗。不過我們還是提出了我們的方案,那就是使用 PHPRPC,他們當然沒聽說過,也不敢相信 PHPRPC 能夠代替 ~WebService 來做同樣的事情,他們更不敢相信 PHPRPC 會做的更好。所以,最終 PHPRPC 只是作為一個備用方案,也就是說如果他們使用 ~WebService 失敗了的話,再上 PHPRPC。
要知道,這個方案從確定下來到最終上線並開始使用一共只有 2 個星期,而第一個星期的時間他們全都浪費在了 ~WebService 上。在第一個星期快要結束時,他們還沒有搞定如何來用 ~WebService 傳輸那些並不算複雜的報名資料結構,更不要說安全的傳輸了。他們終於打電話過來告訴我們他們的 ~WebService 計畫並沒有成功,他們想換 PHPRPC 來試一下。於是當天下午,他們就派人帶著他們的系統過來,準備用 PHPRPC 試一下可不可以完成他們認為不可能完成的任務。
不好意思的說,在當時,PHPRPC for Java 還不算成熟,不要說詳細的用戶指南,連一個簡單的使用說明都還沒有。於是當天下午,我先用了不到一個小時的時間寫了一個簡單的使用說明給他們。他們對照我寫的簡單說明,只用了不到一個小時的時間,就解決了他們用 ~WebService 花了一個星期都沒有解決的問題。他們簡直不敢相信這麼簡單好用的 PHPRPC 是我一個人完成的。就這樣,最終使用 PHPRPC 的方案確定了下來。
之後的一切都變得很順利了,系統最後基本按時上線```比預定的上線時間還是晚了一天,但導致時間延期一天的問題是出在對系統的打包安裝上面,其中 ~MySQL 打包到安裝程式中的時間比他們預期的時間要多得多。但這跟 PHPRPC 沒有絲毫關係。```。系統上線後,他們又在線上照片採集上遇到了問題,開始他們用的是 Java Applet + RMI 來做的照片採集,可是這個 Java Applet 在對攝像頭的控制上需要安裝一個媒體包到每一個使用這個 Web 系統報名的桌面,這顯然是不現實的事情,更糟糕的是即使這樣照出來的照片還是變形的,這個問題雖然不能怪到 RMI 頭上,不過如果他們能在 Flash 中用 RMI,就不會用 Applet 來採集照片了,不是嗎?最終他們還是把這個棘手的問題交給了我,我把照相客戶端從 Java Applet 換成了 Flash,通訊從 RMI 換成了 PHPRPC,通過 PHPRPC for ~ActionScript 客戶端到 PHPRPC for Java 的伺服器端很容易就完成了這個不同語言之間的資料交換問題。
好了,我想我應該不需要再舉更多的例子了,雖然類似的例子我還可以舉出好多。但如果我再說太多,你可能就要把我當成賣大力丸```江湖上一種據說可以讓人力大無窮的神奇藥丸,但實際上可能只是糯米麵團。```的啦,呵呵。所以,下面我們還是來看看如何具體的安裝和使用 PHPRPC for Java 吧。
本章的內容比較多,放在一整頁裏不便於閱讀,所以這裏列了一個目錄,你只要點擊相應的條目,就可以進入你想看的章節了。
*[[PHPRPC for Java 的安裝]]
*[[PHPRPC for Java 伺服器]]
*[[PHPRPC for Java 客戶端]]
*[[PHPRPC for Java 工具類]]
*[[PHPRPC for Java 常見問題解答]]
!!PHPRPC for Java 支援哪些 ~J2EE 伺服器?
目前已經完全通過測試的伺服器有 Tomcat、Resin、~JRun、~GlassFish 和 Jetty,在這些伺服器的各個版本上都可以正常運行。其他的 ~J2EE 伺服器我們因為無法得到相應的伺服器軟體而未做測試,因此這並非說它不支持其他的 ~J2EE 伺服器。我們非常歡迎您能夠報告您在其他伺服器上的部署情況,以幫助我們改善該開發包。
!!如何發佈服務?
Java 中的物件方法或類的靜態方法都可以直接作為 PHPRPC 服務來發佈。但發佈的方法必須是用 {{{public}}} 聲明的方法,而且參數和返回值都必須是可序列化的類型(繼承自 {{{java.io.Serializable}}} 的類型)。除此之外,沒有別的要求。
服務可以通過 JSP 發佈,也可以直接通過 Servlet 發佈。下面我們舉一個簡單的用 JSP 發佈的例子:
''rpc.jsp''
<code java>
<%@ page import="java.lang.*" %>
<%@ page import="org.phprpc.*" %>
<%
PHPRPC_Server phprpc_server = new PHPRPC_Server();
phprpc_server.add("abs", Math.class);
phprpc_server.add("min", Math.class);
phprpc_server.start(request, response);
%>
</code>
上面的代碼發佈的是 {{{java.lang.Math}}} 類中的 {{{abs}}} 和 {{{min}}} 這兩個靜態方法,它們都有重載方法,PHPRPC 支援重載方法的發佈。
通過這個例子,你會發現用 PHPRPC 發佈服務很簡單,只要先創建 {{{PHPRPC_Server}}} 類的一個物件實例,然後執行它的 {{{add}}} 方法來添加要發佈的方法,最後執行它的 {{{start}}} 方法來啟動服務就可以。{{{start}}} 方法的兩個參數 {{{request}}} 和 {{{response}}} 是固定的。
另外,提醒大家一點,不要忘記發佈服務時,把 phprpc.jar 放到服務頁面所在的 {{{WEB-INF\lib}}} 目錄下。服務頁面的名字沒有要求,rpc.jsp、index.jsp 或其他什麼名字都可以。只要在調用時指定的 URL 與發佈時的名字一樣就可以了。
!!如何發佈一個物件方法?
上面的例子中發佈的是靜態方法,如果要發佈物件方法,只需要把 {{{add}}} 第二個參數換成方法所在的物件即可。
!!如何發佈整個物件中的方法或整個類中的靜態方法?
如果在使用 {{{add}}} 方法時,只代入一個 {{{Object}}} 類型的參數,則該物件上的所有 {{{public}}} 方法都會作為服務發佈。如果只代入一個 {{{Class}}} 類型的參數,則該類上的所有 {{{static public}}} 方法都會作為服務發佈。
從 3.0.2 開始,對上述行為作了稍許改變,在 3.0.2 之後,只發佈物件所在類或指定類上聲明的方法,而祖先類的方法不會被發佈,這一修改可以避免發佈 {{{Object}}} 上或其他基類上你不希望發佈的方法。
例如:
<code java>
class A {
public void a() {}
}
class B extends A {
public void b() {}
}
class C extends B {
public void c() {}
}
C o = new C();
server.add(o); // 只發佈 c();
server.add(o, B); // 只發佈 b();
server.add(o, A); // 只發佈 a();
</code>
以上代碼中,{{{server}}} 為 {{{PHPRPC_Server}}} 的實例物件。而在 3.0.2 之前:
<code java>
server.add(o); // 會把 C、B、A 以及 Object 上的所有方法都發佈。
</code>
這種行為顯然不是我們期望的,因此,如果您想發佈整個物件或類上的方法,推薦使用 3.0.2 及其之後的版本。
!!如何發佈一個物件中的一組方法或一個類中一組靜態方法?
如果第一個參數不是字串,而是字串陣列,則所有與該陣列中字串值匹配的 {{{public}}} 方法都會作為服務發佈。
!!如何用別名來發佈方法?
有時你發佈的幾個物件或類中的方法可能具有相同的方法名,你也許會希望用不同的名字來發佈它們,以便能夠將它們區分開來。例如,你可能要同時發佈一個 User 類和一個 Catalog 類,但是它們都有 add、update、delete 方法,那麼你可以通過這種方式來發佈它們:
<code java>
<%@ page import="demo.User" %>
<%@ page import="demo.Catalog" %>
<%@ page import="org.phprpc.*" %>
<%
PHPRPC_Server phprpc_server = new PHPRPC_Server();
phprpc_server.add("add", User.Class, "addUser");
phprpc_server.add("update", User.Class, "updateUser");
phprpc_server.add("delete", User.Class, "deleteUser");
phprpc_server.add(new String[] { "add",
"update",
"delete"
},
Catalog.class,
new String[] { "addCatalog",
"updateCatalog",
"deleteCatalog"
}
);
phprpc_server.start(request, response);
%>
</code>
從上面的代碼中我們可以看到,不管是添加一個方法還是添加一組方法,都是支持別名的。當然,如果 {{{User.add}}} 和 {{{Catalog.add}}} 方法的參數類型不同,並且不能相互轉換的話,那麼你也可以用同一個名字來發佈它們,當客戶端用不同的參數調用時,伺服器端會根據參數類型來判斷該調用哪個類的 {{{add}}} 方法,也就是說,PHPRPC 所發佈的服務,不但支援同一個類或物件中方法的重載,而且支援不同的類或物件中方法的重載。
當然,在這種情況下,我們絕不推薦您使用這種不同的類或物件中方法的重載特性,用不同的別名來發佈方法,可以減少伺服器查找方法的時間。
!!如何在發佈的方法中使用會話(session)?
有時,你可能希望伺服器端發佈的物件支援會話功能,例如下面這個類:
<code java>
package demo;
import java.io.*;
import java.util.*;
final public class Upload {
private ArrayList files;
public Upload() {
files = new ArrayList();
}
final public int open(String filename) throws IOException {
try {
FileOutputStream file = new FileOutputStream(filename);
int length = files.size();
for (int i = 0; i < length; i++) {
if (files.get(i) == null) {
files.set(i, file);
return i;
}
}
files.add(file);
return (files.size() - 1);
}
catch (Exception e) {
return -1;
}
}
final public void close(int handler) throws IOException {
FileOutputStream file = (FileOutputStream)files.get(handler);
file.close();
files.set(handler, null);
}
final public void write(int handler, byte[] b, int off, int len) throws IOException {
FileOutputStream file = (FileOutputStream)files.get(handler);
file.write(b, off, len);
}
final public void write(int handler, byte[] b) throws IOException {
FileOutputStream file = (FileOutputStream)files.get(handler);
file.write(b);
}
}
</code>
這個類中有 4 個方法,一個 {{{open}}},一個 {{{close}}},還有兩個 {{{write}}} 方法,如果我們在發佈方法時,僅僅簡單的創建 {{{Upload}}} 物件,並且直接將它的這些方法發佈的話,那麼客戶端調用 {{{open}}} 方法後,再調用 {{{write}}} 方法時,將無法通過 {{{open}}} 返回的 {{{handler}}} 來找到已經打開的 {{{FileOutputStream}}} 物件,因為,客戶端調用的 {{{write}}} 方法是一個新的 {{{Upload}}} 物件中的,而不是原來那個執行過 {{{open}}} 方法的 {{{Upload}}} 物件中的 {{{write}}} 方法了。
這時我們就需要會話支援了。這種情況下,只需要將創建的 {{{Upload}}} 物件放入 Session 中,下次調用時,從 Session 中把 {{{Upload}}} 物件取出再發佈就可以了:
<code java>
<%@ page import="demo.Upload" %>
<%@ page import="org.phprpc.*" %>
<%
Upload upload;
if (session.getAttribute("upload") == null) {
upload = new Upload();
session.setAttribute("upload", upload);
}
else {
upload = (Upload)session.getAttribute("upload");
}
PHPRPC_Server phprpc_server = new PHPRPC_Server();
phprpc_server.add(upload);
phprpc_server.start(request, response);
%>
</code>
通過上面這段 jsp 代碼,就可以正確的發佈我們的服務了。
但有時我們可能希望更細粒度的控制會話,比如我們可能需要在方法中存取會話變數的值。PHPRPC 提供了這樣的能力。只要在定義要發佈的方法時,將最後的參數設置為 {{{javax.servlet.http.HttpSession}}} 類型的變數,就可以直接在方法中存取會話了。客戶端在調用時這種方法時,只需要代入其他參數即可,該會話參數應直接忽略,PHPRPC 服務會自動將 {{{session}}} 變數傳遞給該參數。
!!如何在發佈的方法中使用 request 物件?
有時我們還需要在遠端方法中直接獲取 {{{request}}} 物件中的參數,PHPRPC 也提供了這種直接在遠端方法中直接存取 {{{request}}} 物件的能力。只要在定義要發佈的方法時,將最後一個參數設置為 {{{javax.servlet.http.HttpServletRequest}}} 類型的變數,PHPRPC 服務就會自動把 {{{request}}} 變數傳入了。另外,在同一個方法中,{{{javax.servlet.http.HttpServletRequest}}} 和 {{{javax.servlet.http.HttpSession}}} 這兩個類型的參數不能同時定義。如果你需要同時存取 {{{request}}} 物件和 {{{session}}} 物件,你只需要定義 {{{javax.servlet.http.HttpServletRequest}}} 類型的參數,然後通過 {{{request}}} 物件的 {{{getSession}}} 方法來獲取 {{{session}}} 物件就可以了。
同樣,你還可以將最後一個參數設置為 {{{javax.servlet.ServletContext}}} 類型的變數,讓 PHPRPC 自動傳入 {{{context}}} 物件,或者通過 {{{session}}} 物件的 {{{getServletContext}}} 方法來獲取它。
總之,你可以在定義遠端方法時,將最後一個參數設置為以上三種類型之一,就可以自由存取 {{{request}}}、{{{session}}} 和 {{{context}}} 對象了。
!!如何讓發佈的方法支援輸出重定向?
有時,我們定義的方法除了返回結果以外,還會輸出一些資訊,在遠端調用這種方法時,我們可能會希望客戶端不但可以得到遠端方法的結果,還可以單獨得到這些輸出資訊。PHPRPC 提供了這種能力。
你只需要將最後一個參數設置為 {{{java.io.PrintStream}}} 或 {{{java.io.PrintWriter}}} 類型的變數(任選其一,但不能同時定義它們),就可以通過該變數輸出資訊,並返回給客戶端了。客戶端在進行遠端調用時,同樣不需要代入該參數,而是通過 {{{getOutput}}} 方法來獲取遠端的輸出資訊。
輸出重定向參數與前面提到的 {{{request}}}、{{{session}}} 和 {{{context}}} 參數可以同時定義,它們之間沒有衝突,它們之間的前後順序也是任意的,只要保證這兩個參數的都在其他需要客戶端傳入的參數的後面就可以了。
當你需要返回給客戶端許多字串形式的內容時,我們推薦你用這種輸出重定向方式來返回,因為它不需要伺服器端序列化和客戶端反序列化的過程,可以大大提高伺服器和客戶端的處理速度。
!!如何在伺服器端方法執行出錯時,返回更多的調試資訊?
在發佈服務時,只需要先執行 {{{setDebugMode}}} 方法,將參數值設置為 {{{true}}},然後再執行 {{{start}}} 方法就可以返回更加詳細的出錯調試資訊了。
!!如何啟用伺服器端的壓縮輸出特性?
PHPRPC 服務是使用 HTTP 協定來傳輸資訊的,因此它同樣支援 HTTP 協議中的壓縮輸出特性,只需要先執行 {{{setEnableGZIP}}} 方法,將參數值設置為 {{{true}}},然後再執行 {{{start}}} 方法就可以啟用壓縮輸出特性了。但通常我們不推薦啟用伺服器端的壓縮輸出特性,因為壓縮輸出會佔用伺服器更多的 CPU 資源,增加伺服器的處理負擔。
另外一個關於啟用壓縮輸出特性的問題是:如果你是通過 JSP 來發佈 PHPRPC 服務的話,在某些伺服器上(例如 Tomcat 5.0),你啟用壓縮輸出後,可能會發生下列錯誤:
{{{
java.lang.IllegalStateException: getOutputStream() has already been called for this response
}}}
解決方法是,執行 {{{start}}} 方法後,再執行:
<code java>
out.clear();
out = pageContext.pushBody();
</code>
就可以了。
!!如何使用 Servlet 發佈服務?
把發佈服務的代碼寫到 Servlet 的 {{{service}}} 方法中就可以了,你也可以放在 {{{doGet}}} 和 {{{doPost}}} 方法中。
使用 Servlet 發佈服務時,你還可以在 {{{init}}} 方法中使用 {{{PHPRPC_Server}}} 的 {{{addGlobal}}} 方法來添加要發佈的方法,這樣只在 Servlet 啟動時添加一次,就可以全局可用,可以使你的伺服器工作的更有效率,不過需要注意,你要處理好共用資料訪問時可能遇到衝突的問題。
!!如何調用 PHPRPC 服務?
我們先通過一個簡單的例子,來介紹如何調用 PHPRPC 服務:
<code java>
import org.phprpc.*;
import java.io.*;
interface IUpload {
public int open(String filename);
public void close(int handler);
public void write(int handler, byte[] b, int off, int len);
public void write(int handler, byte[] b);
}
public class UploadClient {
public static void main(String[] args) throws IOException {
if (args.length > 0) {
PHPRPC_Client client = new PHPRPC_Client("http://192.168.0.1/upload.jsp");
client.setProxy("10.1.0.1", 8000);
client.setKeyLength(512);
client.setEncryptMode(1);
String filename = args[0];
System.out.println(filename);
IUpload upload = (IUpload)client.useService(IUpload.class);
FileInputStream is = new FileInputStream(filename);
int handler = upload.open(filename);
byte[] bytes = new byte[102400];
int read = 0;
while ((read = is.read(bytes)) >= 0) {
upload.write(handler, bytes, 0, read);
}
upload.close(handler);
}
}
}
</code>
上面這個 {{{UploadClient}}} 例子中,我們調用的是前面發佈的那個 Upload 服務。
首先我們定義了一個介面 {{{IUpload}}},這個介面中的方法聲明跟前面服務中 Upload 類所提供的方法的聲明是一致的,但是在這個介面中不需要聲明拋出錯誤。
然後,我們創建了一個 {{{PHPRPC_Client}}} 物件 {{{client}}},創建時的參數是 PHPRPC 伺服器的 URL,如果我們在創建 {{{client}}} 物件時不指定伺服器的 URL,後面我們也可以在調用 {{{useService}}} 方法時再指定伺服器的 URL。
接下來,通過 {{{client}}} 的 {{{setProxy}}} 方法,我們可以設置連接到伺服器所經過的 HTTP 代理伺服器,代理伺服器的設置除了可以單獨指定 IP 位址和埠號以外,還可以直接通過 http://10.1.0.1:8000/ 這種 URL 方式來指定代理伺服器。另外,{{{setProxy}}} 方法也支援 Basic 方式認證,只要將用戶名和密碼作為參數帶入即可。
{{{PHPRPC_Client}}} 物件的 {{{setKeyLength}}} 和 {{{setEncryptMode}}} 這兩個方法是跟加密傳輸有關的。
{{{setKeyLength}}} 方法用於設置加密傳輸前[[密鑰長度]],默認長度是 128。
{{{setEncryptMode}}} 方法用於設置[[加密模式]],預設值為 0。
上面設置代理、設置密鑰長度、加密模式都是可選項,如果你不需要這些功能,可以直接忽略它們。
接下來,我們調用了 {{{client}}} 的 {{{useService}}} 方法,因為前面在創建 {{{client}}} 時已經指定了伺服器端的 URL,這裏調用 {{{useService}}} 時,我們只要指定返回的代理物件的介面類型就可以了。它的返回值我們通過轉型操作,將它轉型為 {{{IUpload}}} 的一個物件 {{{upload}}}。
在最後的代碼裏面,我們直接通過 {{{upload}}} 的 {{{open}}}、{{{write}}} 和 {{{close}}} 方法來調用遠端過程,看上去跟調用本地的方法沒有什麼區別。
通過這個例子,我想你已經可以掌握 PHPRPC for Java 客戶端的基本使用方法了。
!!如何在調用 PHPRPC 服務時,進行引用參數傳遞?
我們可以直接調用 {{{PHPRPC_Client}}} 物件的 {{{invoke}}} 方法進行引用參數傳遞的遠端程序呼叫。上面的例子中,我們是通過先聲明介面,然後返回代理物件,再通過代理物件來作遠端調用的,而代理物件本身實際上也是通過調用 {{{PHPRPC_Client}}} 的 {{{invoke}}} 方法來實現遠端調用的。下面我們來通過一個簡單的例子,看一下如何直接用 {{{invoke}}} 方法來調用遠端過程,並實現引用參數傳遞。這個例子中我們用 Java 版本的客戶端來調用 PHP 版本的伺服器端。下面是伺服器端的 PHP 代碼:
<code php:firstline[0]>
<?php
include('phprpc_server.php');
function inc(&$n) {
$n++;
}
$phprpc_server = new PHPRPC_Server();
$phprpc_server->add('inc');
$phprpc_server->start();
?>
</code>
這個伺服器發佈了一個 {{{inc}}} 方法,該方法是將參數值加一。這個方法是需要引用參數傳遞的。下面我們來看看如何在 Java 中調用這個遠程過程:
<code java>
import org.phprpc.*;
import java.io.*;
public class IncClient {
public static void main(String[] args) {
if (args.length > 0) {
PHPRPC_Client client = new PHPRPC_Client("http://192.168.0.1/inc.php");
Object[] a = new Object[1];
a[0] = args[0];
System.out.println(a[0]);
client.invoke("inc", a, true);
System.out.println(a[0]);
}
}
}
</code>
這個例子中,我們定義的 {{{Object}}} 陣列 {{{a}}} 就是要傳給伺服器的參數列表,因為只有一個參數,所以這個初始化時,只給這個陣列分配了一個元素的空間。我們首先把給參數賦值,我們是通過命令行方式來給它賦值的,在調用之後,我們再次輸出 {{{a[0]}}} 的值,我們會發現它確實被加了 1。這裏我們還發現,我們通過命令行傳給 {{{a[0]}}} 的實際是個字串,但是遠端程序呼叫卻把它作為數值處理了,而且返回的 {{{a[0]}}} 也變成了數值。你可以通過在調用前和調用後,分別加上:
<code java:nocontrols>
System.out.println(a[0].getClass().toString());
</code>
來驗證我們的結果。也就是說,PHPRPC 的遠端調用是弱類型參數傳遞的,返回結果也是弱類型的,PHPRPC 會根據實際調用來自動進行類型轉換。
{{{invoke}}} 有兩個重載方法:
<code java:nocontrols>
Object invoke(String funcname, Object[] args);
Object invoke(String funcname, Object[] args, boolean byRef);
</code>
第一個參數是遠端方法名,第二個參數是遠端方法的參數列表,第三個參數表示是否進行引用參數傳遞。當我們調用有 3 個參數的重載方法,並將第三個參數 {{{byRef}}} 設置為 {{{true}}} 時,就可以進行引用參數傳遞了。
第二個參數 {{{args}}} 在進行引用參數傳遞後,會返回修改後的參數值列表。在直接使用 {{{invoke}}} 進行遠端調用時,我們需要自己處理返回值的轉型操作。
!!如何得到伺服器端輸出重定向的內容?
前面在介紹伺服器端輸出重定向時,我們已經提到過如何在客戶端獲取輸出內容的方法了。這裏我們詳細介紹一下。
當我們通過代理物件的方法或直接通過 {{{PHPRPC_Client}}} 物件的 {{{invoke}}} 方法進行遠端調用之後,我們可以使用 {{{PHPRPC_Client}}} 物件的 {{{getOutput}}} 方法來得到伺服器端輸出的內容。如果伺服器端沒有輸出內容,該方法會返回空字串。
!!如何來得到遠端過程執行的錯誤資訊?
PHPRPC 把錯誤分為兩種,一種是致命錯誤,一種是警告性錯誤。
當遠端過程執行發生致命錯誤時,如果客戶端是通過 {{{invoke}}} 方法直接調用的,則 {{{invoke}}} 方法的返回值是一個 {{{PHPRPC_Error}}} 類型的物件,它其中包含了遠端過程執行時發生的致命錯誤資訊。如果客戶端是通過代理物件的方法來進行的遠端程序呼叫,則會拋出一個錯誤,錯誤物件的類型也是 {{{PHPRPC_Error}}} 類型。
當遠端過程執行發生警告性錯誤時,不管客戶端使用何種方式調用,都會返回結果值,但是你可以通過 {{{PHPRPC_Client}}} 物件的 {{{getWarning}}} 方法來得到警告錯誤,{{{getWarning}}} 方法的返回的值也是 {{{PHPRPC_Error}}} 類型的物件。如果沒有發生警告錯誤,{{{getWarning}}} 方法返回 {{{null}}}。
另外,如果通過代理物件的方法來進行的遠端程序呼叫,你可能會得到更多的錯誤資訊,例如當返回值的類型跟代理物件介面聲明的返回類型不相容時,就會拋出類型轉換錯誤。因此當進行非引用參數傳遞的遠端程序呼叫時,我們推薦使用通過聲明代理物件介面的方式來進行遠端調用。這樣不但讓你的程式更直觀,也會讓你更加方便調試。
!!PHPRPC for Java 用戶端支援非同步調用嗎?
前面介紹的方法都是同步調用,在大多數情況下,同步調用即可滿足您的需要。但是有 2 種情況下用非同步調用會更加方便一些:
首先是當做圖像介面編程時,要防止單線程下同步調用出現卡死介面的現象,可以使用非同步調用,但這種情況下,採用同步調用加多線程技術也可以解決。
另一種情況是,當使用同步調用加多線程時,如果你要獲取伺服器端輸出重定向的內容或者要獲取警告錯誤,則可能或獲取到錯誤的內容,原因是另外的線程執行時也有可能改寫 {{{output}}} 和 {{{warning}}} 屬性的值。這種情況下,使用非同步調用可以輕鬆解決。
因此,在 3.0.2 版本之後我們增加了對非同步調用的支援。下面我們就看看如何實現非同步調用。
!!如何實現非同步調用?
非同步調用跟同步調用類似,你可以通過介面方式,也可以通過直接 {{{invoke}}} 方式完成。不同的是,非同步調用時,可以通過介面方式實現引用參數傳遞和為某個非同步調用指定加密方式。而同步調用在介面方式下不支援這一點。
下面我們來看一下非同步調用的例子(這個例子中也有同步調用):
<code java>
import org.phprpc.*;
interface IHello {
// 同步調用
public String hello(String name);
// 非同步調用
public void hello(String name, PHPRPC_Callback callback);
// 可以設置引用參數傳遞的非同步調用
public void hello(String name, PHPRPC_Callback callback, boolean byRef);
// 可以設置引用參數傳遞和指定加密方式的非同步調用
public void hello(String name, PHPRPC_Callback callback, boolean byRef, int encryptMode);
}
public class HelloWorld {
public static void main (String [] args) {
final PHPRPC_Client client = new PHPRPC_Client ("http://127.0.0.1:8080/index.aspx");
final IHello clientProxy = (IHello) client.useService(IHello.class);
client.setKeyLength(1024);
client.setEncryptMode(2);
// 回調方法的定義(handler1 到 handler4 都是回調方法)
PHPRPC_Callback callback = new PHPRPC_Callback() {
public void handler1(String result) {
System.out.println("handler1:");
System.out.println(result);
System.out.println();
}
public void handler2(String result, Object[] args) {
System.out.println("handler2:");
System.out.println(result);
System.out.println(args[0]);
System.out.println();
}
public void handler3(String result, Object[] args, String output) {
System.out.println("handler3:");
System.out.println(result);
System.out.println(args[0]);
System.out.println(output);
System.out.println();
}
public void handler4(String result, Object[] args, String output, PHPRPC_Error warning) {
System.out.println("handler4:");
System.out.println(result);
System.out.println(args[0]);
System.out.println(output);
System.out.println(warning);
System.out.println();
}
// 專門用於錯誤處理的回調方法,默認行為是什麼都不做
public void errorHandler(Throwable error) {
System.out.println(error.toString());
}
};
// 非同步調用
clientProxy.hello("World1", callback);
// 功能同上
clientProxy.hello("World2", callback, false);
// 加密級別設為三級
clientProxy.hello("World3", callback, false, 3);
// 引用參數傳遞
clientProxy.hello("World4", callback, true);
// 引用參數傳遞,加密級別設為無
clientProxy.hello("World5", callback, true, 0);
// 採用直接 invoke 方式的非同步調用
client.invoke("hello", new Object[] {"World6"}, callback, false, 3);
// 同步調用
System.out.println(clientProxy.hello("World7"));
}
}
</code>
上面這個例子中,{{{IHello}}} 介面定義了 1 個同步調用方法,3 個非同步調用方法,當然這 4 個方法對於的伺服器端方法都是同一個 {{{hello}}}。
後面在回調方法定義部分,我們採用了創建匿名類物件的方式,其中 {{{handler1}}} 到 {{{handler4}}} 這些方法名都是隨意取的,它們叫什麼名字都無所謂(比如叫 {{{abc}}}、{{{efg}}} 都是可以的),回調方法的個數也不是固定,回調方法可以一個也不定義,也可以定義許多個,沒有個數限制,所以這些方法都會作為回調方法被執行。回調方法的參數個數是 1 到 4 個,參數順序分別是 {{{result}}}、{{{args}}}、{{{output}}}、{{{warning}}}。
{{{result}}} 是伺服器執行返回的結果,它的類型聲明跟伺服器端的類型應該相容,否則會產生轉型錯誤,關於錯誤處理後面我們再介紹。
{{{args}}} 是方法的參數,當方法為非同步調用時,參數值可能會有所改變。它的元素的實際類型與發起調用時也不一定相同,應該用 {{{org.phprpc.util.Cast}}} 的 {{{cast}}} 方法轉換元素到實際類型。
{{{output}}} 是伺服器端輸出重定向的內容,它是字串類型的。
{{{warning}}} 是伺服器端發生的經過錯誤,它是 {{{PHPRPC_Error}}} 類型的。
在回調函數參數列表中,上述參數可以不用全部指定,但必須按上述順序指定,中間不能跳過。
而 {{{errorHandler}}} 是一個特殊的回調方法,它只在發生錯誤時被執行,它的參數是 {{{Throwable}}} 類型的,你可以不用重新定義它,不過它的默認行為是什麼都不做。
上面的程式在正確的情況下執行結果可能是這樣的:
{{{
handler1:
hello World5
handler2:
hello World5
[B@70329f3d
handler3:
hello World5
[B@70329f3d
output: hello World5
handler1:
hello World6
handler4:
hello World5
handler2:
hello World6
World6
[B@70329f3d
output: hello World5
null
handler3:
hello World6
World6
output: hello World6
handler4:
hello World6
World6
output: hello World6
null
handler1:
hello World4
handler2:
hello World4
[B@b749757
handler3:
hello World4
[B@b749757
output: hello World4
handler4:
hello World4
[B@b749757
output: hello World4
null
handler1:
hello World3
handler2:
hello World3
World3
handler3:
hello World3
World3
output: hello World3
handler4:
hello World3
World3
output: hello World3
null
handler1:
hello World1
hello World7
handler2:
hello World1
World1
handler3:
hello World1
World1
output: hello World1
handler4:
hello World1
World1
output: hello World1
null
handler1:
hello World2
handler2:
hello World2
World2
handler3:
hello World2
World2
output: hello World2
handler4:
hello World2
World2
output: hello World2
null
}}}
因為是非同步調用,所以你會發現輸出的順序並不是按照執行的順序來的。另外,你會注意到 World4 和 World5 的調用在輸出 {{{args[0]}}} 時,變成了一個位元組陣列位址,原因就是引用參數傳遞後,資料類型也是可以發生了變化的。
現在我們應該理解如何來進行非同步調用了。
另外,在 {{{J2ME}}} 版本中,非同步調用也不支援代理方式。另外,回調方法也只能定義一個,且名字和參數個數,參數類型都是固定的,具體格式參見 {{{J2ME}}} 版本的 {{{PHPRPC_Callback}}} 的定義。
!!遠端程序呼叫中的超時問題如何解決?
PHPRPC for Java 客戶端在進行遠端程序呼叫時,默認的超時時間為 30 秒,如果你的網路線路不好,或者需要傳遞的資料量較大,或者伺服器端處理資料的時間較長,則在執行過程中會發生超時錯誤。這時,只需要調用 {{{PHPRPC_Client}}} 物件的 {{{setTimeout}}} 方法來設置超時時間就可以了,單位是毫秒。
!!遠程程序呼叫中參數長度是否有限制?
PHPRPC 客戶端本身沒有對參數長度做限制,但是因為 PHPRPC 協定是通過 HTTP 協定進行傳輸的,如果 PHPRPC 伺服器端的設置限制了 HTTP POST 資料的長度(例如 Tomcat 默認設置最大允許的 POST 資料量為 2M),則調用過程中,如果參數序列化之後的長度大於了這個限制,就會調用失敗。
另外,如果伺服器沒有限制 POST 資料的大小,則客戶端可以提交任意大小的資料,直到伺服器記憶體耗盡才會出錯。
PHPRPC 傳輸的資料量通常小於同樣調用的 Web Service 傳輸的資料量的。因此,PHPRPC 調用所允許的參數長度一般比 Web Service 調用所允許的參數長度更大。
!!什麼情況下才需要設置字元集?
PHPRPC 客戶端和伺服器都允許通過 {{{setCharset}}} 方法來設置字元集,但通常是不必要的,默認情況下,字元集為 ~UTF-8。字元集設置通常是跟單字節字串語言(例如 PHP、Perl)進行通訊時才需要設置的,例如:當我們要在 Java 中遠端調用一個 PHP 語言提供的 PHPRPC 服務時,而這個服務使用的是 GBK 字元集,我們可以通過 {{{PHPRPC_Client}}} 的 {{{setCharset}}} 方法將字元集設置為 GBK,然後就可以正確的調用了。
但通常我們不推薦使用 ~UTF-8 以外的其他字元集。即使是 PHP 這種語言所提供的服務,我們也推薦使用 ~UTF-8 字元集,因為有許多客戶端(~JavaScript,~ActionScript)或者伺服器(ASP)實現只支援 ~UTF-8 字元集,如果我們使用其他字元集,將不能跟這些伺服器和客戶端進行通訊。
PHPRPC for Java 中除了伺服器和客戶端實現之外,還有一些工具類,這些工具類除了在伺服器和客戶端的實現中被調用之外,你也可以單獨使用它們。下面是關於這些工具類的常見問題解答。
!!~AssocArray 類如何使用?
3.0.1 版中新增加了一個 {{{AssocArray}}} 類,該類主要用於優化反序列化聯合陣列類型時的效率。3.0.1 版本之前反序列化聯合陣列時,是根據下標類型來自動判斷是 {{{ArrayList}}} 類型還是 {{{HashMap}}} 類型的,但這樣做的缺點是,如果聯合陣列中包含對其自身的引用,則反序列化將會重複多次遞迴操作來完成,效率較低。而且其自動判斷的類型也不一定準確。3.0.1 版本中的 {{{AssocArray}}} 類就是為解決這個問題而加入的。當反序列化聯合陣列時,直接反序列化為 {{{AssocArray}}} 類型的物件,而不再進行類型判斷和遞迴,大大提高了反序列化的效率。並且,{{{AssocArray}}} 物件提供了 {{{toArrayList}}} 和 {{{toHashMap}}} 兩個方法,可以在使用時轉換為你需要的物件類型。引入 {{{AssocArray}}} 物件的另一個好處是在某些語言中對於數位下標的陣列序列化時並不一定是按照數位順序排序的,這種情況下,原來的實現將會反序列化為 {{{HashMap}}} 類型,而原來的 {{{Cast}}} 類中的 {{{cast}}} 方法不能將 {{{HashMap}}} 轉換為 {{{ArrayList}}} 或陣列物件,而 {{{AssocArray}}} 物件則可以接受不按數位順序序列化的陣列類型,並且當你將它轉換為 {{{ArrayList}}} 時,將自動返回排好序的 {{{ArrayList}}}。
!!Cast 類如何使用?
PHPRPC for Java 包中的 {{{org.phprpc.util.Cast}}} 靜態類提供了 PHPRPC 中相容類型(關於相容類型的解釋請參見[[PHPRPC for Java 常見問題解答]]部分)的轉化功能。其中最重要的方法是 {{{cast}}} 方法,它有幾個重載,你可以根據需要來使用它進行類型轉換,例如將位元組陣列轉換為字串,或者將字串轉換位元組陣列。你也可以通過該方法將 {{{AssocArray}}} 轉換為 {{{ArrayList}}}、{{{HashMap}}} 或者陣列。
該類是在 PHPRPC 進行遠端調用時,進行自動類型轉換用的,在使用代理方式調用時一般不需要直接使用。只有當使用直接方式調用或者返回結果是容器類型時,你需要用該類來轉換結果類型或者容器元素的類型。
!!~PHPSerializer 類如何使用?
該類提供了將 Java 物件和 PHP 序列化格式文本相互轉換的功能。因為 Java 中的字串在內部都是以 ~UTF16 編碼表示的,而 PHP 序列化文本實際上是無字元編碼集的位元組流,因此在轉換過程中需要指定字元集才能夠進行正確的轉換,默認使用 ~UTF-8 字元集在 Java 字串和位元組流之間進行轉換。你可以通過 {{{setCharset}}} 方法來設置轉換字元集。{{{PHPSerializer}}} 類中最重要的兩個方法是 {{{serialize}}} 方法和 {{{unserialize}}} 方法,{{{serialize}}} 方法很簡單,只要帶入要序列化的物件,就可以返回序列化後的位元組陣列了。{{{unsierialize}}} 方法的參數除了需要序列化後的位元組陣列以外,還需要一個物件類型,如果沒有帶入這個物件類型參數,則返回的物件需要自己用 {{{Cast}}} 類的 {{{cast}}} 方法來轉換為你需要的結果類型。
!!Base64 類如何使用?
{{{Base64}}} 是個簡單實用的靜態類,它有兩個靜態方法,一個是 {{{encode}}},另一個是 {{{decode}}}。{{{encode}}} 是編碼方法,參數是要編碼的位元組陣列,返回值是 {{{Base64}}} 字串;{{{decode}}} 是 {{{encode}}} 的逆操作,參數是 {{{Base64}}} 字串,返回值是原位元組陣列。
!!XXTEA 類如何使用?
{{{XXTEA}}} 類是用來加密解密資料的靜態類,它也有兩個靜態方法,一個是 {{{encrypt}}},另一個是 {{{decrypt}}}。{{{encrypt}}} 是加密資料的方法,第一個參數是原文資料,第二個參數是密鑰;{{{decrypt}}} 是解密資料的方法,第一個參數是密文資料,第二個參數是密鑰。這兩個方法的參數和返回值都是 {{{byte[]}}} 類型。
!!方法重載需要注意什麼問題?
前面我們已經提到過,PHPRPC for Java 支持方法的重載,但是有幾點問題需要注意。
最重要的一點是最好避免發佈重載的方法。因為伺服器處理重載方法比非重載方法效率要低。另外,如果你不發佈重載的方法,也就不需要注意下面這些問題了。
首先,應儘量避免重載參數個數相同且參數類型相容的方法。原因是 PHPRPC 支援弱類型參數傳遞。如果你的伺服器端的方法有類型相容的重載方法,最後客戶端實際調用的方法可能不是你希望調用的方法。
如本指南一開始的例子中,我們發佈了一個 {{{Math.min}}} 方法,它有 4 個重載版本,參數類型分別是 {{{double}}}、{{{float}}}、{{{int}}} 和 {{{long}}}。這四種類型是相容的,因此,如果客戶端調用該方法時,如果帶入的參數是浮點類型,伺服器端有可能調用 {{{double}}} 或 {{{float}}} 類型的方法,也有可能調用到 {{{int}}} 或 {{{long}}} 類型的方法,伺服器端檢測的順序是跟 JDK 實現有關的,因此很難說最後調用到的是哪個。如果不幸調用了 {{{int}}} 類型的方法,那麼返回的結果就不對了。
如果遇到這種情況如何避免呢?因為這四個方法都是在 {{{Math}}} 類中的,因此使用 PHPRPC for Java 伺服器端的別名機制是無法解決的。但我們可以重新定義一個類,來給這四個方法取不同的名字。例如:
<code java>
public class MyMath {
public static double doubleMin(double a, double b) {
return Math.min(a, b);
}
public static float floatMin(float a, float b) {
return Math.min(a, b);
}
public static int intMin(int a, int b) {
return Math.min(a, b);
}
public static long longMin(long a, long b) {
return Math.min(a, b);
}
}
</code>
這樣,我們發佈 {{{MyMath}}} 的 {{{doubleMin}}}、{{{floatMin}}}、{{{intMin}}} 和 {{{longMin}}} 這四個方法,就可以避免相容類型的重載帶來的問題了。這時,客戶端再調用伺服器端的 {{{doubleMin}}},就會按照聲明的 {{{double}}} 類型進行參數傳遞了。
第二個需要注意的問題是,不要重載除了隱含參數以外,其他參數都相同或相容的方法。這裏說的隱含參數指的是前面我們提到的 {{{session}}}、{{{request}}}、{{{context}}} 和伺服器端輸出重定向參數。
例如下面的重載方法發佈後,在客戶端調用時會產生問題:
<code java>
public class Hello {
public static String hello(String a) {
return "hello " + a;
}
public static void hello(String a, java.io.PrintStream out) {
out.println("hello " + a);
}
}
</code>
當客戶端調用這個 {{{hello}}} 方法時,因為 {{{out}}} 這個參數不是在客戶端傳入的,因此,當伺服器端調用該方法時,可能調用了第一個方法,也可能調用了第二個方法。這種情況下,你就無法確定你調用的是哪個方法了。
!!哪些類型是相容類型?
PHPRPC 是支持弱類型(不是無類型,這一點一定要區分開)參數傳遞的,這是它跟 Web Service、Java RMI 等遠端程序呼叫機制的一個重要區別。弱類型參數傳遞的支援對於目前動態腳本語言來說是一個很大的特徵,這個特徵會讓你的程式設計變得更加靈活。
對於弱類型語言來說,相容類型之間是可以根據實際調用情況來自動轉換的,而且這個轉換是雙向的而不是單向的。例如,數位可以轉換為字串,字串也可以轉換為數位。
PHPRPC 中支持的所有簡單類型都是相容類型,它們包括整數、實數、布林類型和字串。在 PHPRPC for Java 中,所有的原生類型以及 {{{Byte}}}、{{{Short}}}、{{{Integer}}}、{{{Long}}}、{{{Float}}}、{{{Double}}}、{{{Boolean}}}、{{{String}}} 類型都是相容類型。
另外,{{{String}}}、{{{StringBuilder}}},{{{char[]}}} 和 {{{byte[]}}} 之間是相容類型。但 {{{byte[]}}} 類型更適合傳輸 binary 類型的資料,例如檔、圖片等。
陣列類型、所有實現了 {{{List}}}、{{{Map}}}、{{{Set}}}、{{{Collection}}} 介面的類型(比如 {{{ArrayList}}}、{{{HashMap}}})和 {{{AssocArray}}} 是相容類型。如果 {{{Collection}}}(包括 {{{List}}}、{{{Set}}} 及其它們的子類)中存放的類型是單一的資料類型,那麼它跟該資料類型的陣列是相容的。如果 {{{Map}}} 中的索引鍵都是整數,並且是從零開始遞增的,那麼它與 {{{Collection}}}(包括 {{{List}}}、{{{Set}}} 及其它們的子類)是相容的。所有實現了 {{{Collection}}} 和 {{{Map}}} 的類型都可以通過 {{{AssocArray}}} 的構造方法轉換為 {{{AssocArray}}} 類型。
上面所說的這些類型都是可以通過 PHPRPC 直接跟其他語言交互的。
另外,{{{java.util.Date}}} 類型比較特殊,PHPRPC 會作為一個特殊的物件對它序列化,序列化後的類名為 {{{PHPRPC_Date}}},該類與其他語言中相應的日期類型相容。
!!PHPRPC for Java 是否支援自定義類型?
PHPRPC for Java 當然也支援自定義類型,並且自定義類型也可以跟其他語言交互。關於自定義類型需要注意以下幾個問題。
自定義類型必須是可序列化的類型。也就是實現了 {{{java.io.Serializable}}} 介面,並且該類型當中所有欄位(不包括方法)都是可序列化的類型。否則,在調用過程中,傳遞該類型的參數或結果時會拋出空指針異常({{{NullPointerException}}})。
只有相同包中的同名類型才被認為是同一類型,如果兩個類型同名,並且定義也完全相同,但是分屬於兩個不同的包,那麼它們就是不同的類型。如果客戶端所傳遞的類型和伺服器端接收的類型是這樣的兩個分屬於不同包中的同名類型,調用時會拋出類型轉換錯誤({{{ClassCastException}}})。
Java 中的自定義類型跟其他語言交互時,如果另一種語言不支援包機制和名稱空間機制(比如 PHP 或 ~JavaScript),那麼在該語言中定義的類名與Java 中點號分割符和美元分割符被替換成下滑線分割符後的自定義類型全名相同。例如:Java 中如果定義了一個 {{{demo.data.MyData}}} 類,那麼在 PHP 中應該定義名為 {{{demo_data_MyData}}} 的類來跟 Java 中的 {{{demo.data.MyData}}} 進行交互。並且,應該保證 {{{demo_data_MyData}}} 中的所有欄位和 Java 中的 {{{demo.data.MyData}}} 中的所有欄位名稱一致。
事實上,PHPRPC 在傳遞 Java 物件時,對 Java 類的全名的序列化處理就是將點號和美元符號都替換成下滑線。因此,當還原回 Java 物件時,自然要對類名進行相反操作的替換匹配。因此,在 Java 中不要定義全名中除點號和美元符號外,其他部分都完全相同的類,否則,這些類中只有一個可以被還原。
!!PHPRPC for Java 是否支持 Java 中的其他內建類型?
只要是實現了序列化介面的 Java 類,都可以通過 PHPRPC 傳遞,不管是內建類型還是自定義類型。不過如果要跟其他語言進行交互。你需要在其他語言中實現跟 Java 中的內建類型定義相同的類,包括內部的各個欄位名稱和類型都要一致(或相容)才可以。要實現這一點,對用戶來說比較困難。因此不推薦使用其他的 Java 內建類型作為參數或結果類型傳遞,除非你只打算把這個服務提供給 Java 客戶端來調用。
!!org.phprpc.util.Serializable 這個介面是做什麼用的?
PHPRPC 除了支持簡單的繼承 {{{java.io.Serializable}}} 介面這種序列化方式以外,還支援用戶自定義的序列化方式。而且用戶自定義序列化方式也分兩種。這兩種都跟 PHP 語言所提供的用戶自定義序列化方式類似。{{{org.phprpc.util.Serializable}}} 介面就是其中的一種方式,該方式跟 ~PHP5 中所提供的用戶自定義序列化方式相容。
{{{org.phprpc.util.Serializable}}} 中定義了兩個方法,如果一個類實現了 {{{java.io.Serializable}}} 介面(這是一個很關鍵的前提)並且同時實現了 {{{org.phprpc.util.Serializable}}} 介面中定義的兩個方法,那麼它就具有了自定義序列化方式的能力。
{{{org.phprpc.util.Serializable}}} 中的 {{{serialize}}} 方法是在物件被序列化時自動觸發的,你可以在 serialize 方法中對該物件做任何方式的序列化,例如 SOAP、JSON 或者 WDDX 等,只要返回的結果是位元組陣列就可以了。
{{{org.phprpc.util.Serializable}}} 中的 {{{unserialize}}} 方法是在物件被反序列化時自動觸發的,傳入的參數就是前面 {{{serialize}}} 方法的返回內容。你可以用同樣的方式將它反序列化。{{{unserialize}}} 方法沒有返回值。
一般情況下,我們沒有必要使用這種方式來自定義序列化,如果你只是想要將類中的部分欄位序列化,或者只是想在物件被序列化之前做一些清除操作的話,那麼你可以實現 {{{__sleep}}} 和 {{{__wakeup}}} 這兩個魔術方法來實現。
!!{{{__sleep}}} 和 {{{__wakeup}}} 這兩個魔術方法如何使用?
{{{__sleep}}} 和 {{{__wakeup}}} 方法也是與 PHP 自定義序列化相相容的一種方式。這兩個方法不需要從某個介面繼承,直接在類中實現即可。
{{{__sleep}}} 方法也是在物件被序列化之前自動觸發,但是它的返回值不是序列化後的內容,而是指定哪些欄位需要序列化。它的返回值類型是一個字串陣列,陣列的每一個元素是需要序列化的欄位名。你除了在 {{{__sleep}}} 方法中返回這些欄位之外,你也可以作其他一些操作,比如關閉 socket 連接或者關閉資料庫連接等操作。
{{{__wakeup}}} 方法是在物件被反序列化之後自動觸發的,它被觸發時,所有序列化的欄位都已經被反序列化完畢,所以,你在 {{{__wakeup}}} 中要做的是反序列化的事後工作,比如恢復資料庫連接或者 socket 連接等。
{{{__sleep}}} 和 {{{__wakeup}}} 方法無需同時實現,你只需把你用到的方法實現即可。
另外,{{{__sleep}}} 和 {{{__wakeup}}} 方法和前面提到的 {{{org.phprpc.util.Serializable}}} 介面中的 {{{serialize}}} 和 {{{unserialize}}} 方法都無需定義成 {{{public}}} 方法,定義成 {{{private}}} 即可。
!!~J2ME 版本如何支援自定義物件?
只要從 {{{stdClass}}} 繼承的就可以了。你甚至不需要去實現它,你可以通過 {{{get}}} 去獲取它的屬性,{{{set}}} 去設置它的屬性。但如果你要打算重新實現 {{{get}}} 或 {{{set}}} 方法,那麼你需要重寫 {{{getProperty}}} 和 {{{setProperty}}} 方法,而不是重寫 {{{get}}} 和 {{{set}}} 方法。
!!~ArrayList 和 ~HashMap 中的元素資料類型如何轉換?
{{{ArrayList}}} 和 {{{HashMap}}} 作為結果返回或者作為參數傳遞給伺服器處理的時候,對它們當中元素類型的處理最好不要用直接的轉型操作,因為資料的真實類型可能不是你所認為的,所以直接轉型的操作很可能會失敗。最好的方式是使用 {{{Cast}}} 類的 {{{cast}}} 方法來進行轉型。
!! 為什麼有時候用介面類型來聲明發佈方法的返回值和參數類型會出錯?
不管是在伺服器端定義發佈方法時,還是在客戶端定義代理介面時,都不建議用介面類型來聲明返回值和參數類型。因為介面是抽象的不能被實例化,因此對於容器類型的參數和返回結果來說,介面並不能表明究竟該轉換為何種具體類型。不過對於自定義類型可以用介面,只要傳遞的類型實現了該介面即可。
不過在 3.0.2 版之後,對 {{{List}}}、{{{Map}}}、{{{Set}}}、{{{Collection}}} 這四個容器介面也提供了支援,如果使用這四個介面類型來接收資料,那麼 {{{List}}}、{{{Collection}}} 會被轉換成 {{{ArrayList}}} 類型,{{{Set}}} 會被轉換成 {{{HashSet}}} 類型,{{{Map}}} 會被轉成 {{{HashMap}}} 類型。
!!客戶端是否支持 Android ?
對於現在的 Android SDK,可以完美支援!
但是因為老版本的 Android SDK 的 {{{java.lang.reflect.Proxy}}} 類不能正常工作,所以,只能使用 {{{invoke}}} 方法來調用遠端過程,而不能使用聲明代理物件介面的方式。
新的(從 android-sdk_m5-rc15 之後)Android SDK 的 {{{java.lang.reflect.Proxy}}} 類已經可以正常工作,所以,現在開發 Android 程式跟開發普通的 ~J2SE 程式中使用 PHPRPC 的方法已經沒有區別了。
!!編譯
PHPRPC for Java 有 2 個版本,一個是 for ~J2SE、~J2EE 的版本,另一個是 for ~J2ME 的版本。 後面我們所說的 PHPRPC for Java 就是指 for ~J2SE、~J2EE 的版本,而不包含 for ~J2ME 的版本。
PHPRPC for Java 支持在 JDK 1.2 及其更高版本上編譯。編譯前,你需要將你使用的 ~J2EE 伺服器中的 servlet.jar(不同的伺服器,這個檔案名稱也不同,比如 Tomcat 中叫做 servlet-api.jar)複製到 PHPRPC for Java 目錄下,取代默認的 servlet.jar,然後執行 make.bat(Windows 下)或者 make(unix/linux 下),就會自動生成 phprpc.jar 和 phprpc_client.jar 了。如果你只需要使用客戶端版本 phprpc_client.jar,直接用 make 批次處理或腳本編譯就可以使用,不需要替換 servlet.jar 檔,在這種情況下,雖然仍會生成 phprpc.jar,但是它的伺服器端是不可用的。phprpc.jar 包含伺服器端和客戶端兩部分,而 phprpc_client.jar 只包括客戶端部分。
PHPRPC for ~J2ME 支持在 JDK 1.2 下編譯,編譯時需要安裝 WTK。並修改 make.bat 中 WTK 的路徑設置。實際上,你不需要自己編譯,dist 目錄下已經有編譯好的版本了。
!!安裝
不需要特別安裝,直接複製到你的專案的庫目錄下就可以直接使用了。
PHPRPC 對 ~JavaScript 的支持是非常早的,最初協定的設計中對資料的編碼方式,返回資料的格式以及回調參數這些內容都充分考慮了 ~JavaScript 的特性。大部分 PHPRPC 用戶最初也是把 PHPRPC 作為一個優秀的 Ajax 方案來使用的。但是 PHPRPC for ~JavaScript 不同於其他那些專門用於 Ajax 的 RPC 方案(如 DWR、xajax 等),PHPRPC for ~JavaScript 客戶端與伺服器是鬆散耦合的,它不依賴於某種特定語言編寫的伺服器。並且它還可以同目前許多優秀的 Ajax 框架(如 ~JQuery、YUI、~MooTools 等)一起使用,而不會有任何衝突。並且它還有一個最大的好處,那就是可以讓你輕鬆實現跨域調用。
下面我們就一起開始 PHPRPC for ~JavaScript 之旅吧!
*[[PHPRPC for JavaScript 的安裝]]
*[[PHPRPC for JavaScript 客戶端]]
*[[PHPRPC for JavaScript 常見問題解答]]
PHPRPC for ~JavaScript 的 js 版本和 ajs 版本除了前面安裝部分所介紹的引入方法有所不同之外,在用法上基本沒有區別。下面我們先來看一下基本用法。
!!如何調用 PHPRPC 服務
我們先通過一個簡單的例子,來介紹如何調用 PHPRPC 服務。
<code js>
var client = new PHPRPC_Client('http://localhost:8080/index.aspx', ['add', 'sub']);
client.setKeyLength(256);
client.setEncryptMode(2);
client.add(1, 2, function (result, args, output, warning) {
alert(result);
});
client.sub(1, 2, function (result, args, output, warning) {
alert(result);
});
</code>
{{{PHPRPC_Client}}} 物件的 {{{setKeyLength}}} 和 {{{setEncryptMode}}} 這兩個方法是跟加密傳輸有關的。
{{{setKeyLength}}} 方法用於設置[[密鑰長度]]。
{{{setEncryptMode}}} 方法用於設置[[加密模式]]。
上面設置密鑰長度、加密模式都是可選項,如果你不需要這些功能,可以直接忽略它們。
PHPRPC 3.0 for ~JavaScript 客戶端與 Java、.NET 客戶端不同,它不需要使用 {{{useService}}} 來返回指定介面的遠端代理物件,~JavaScript 客戶端本身就是一個代理物件。所以,上面例子中 {{{client.add}}} 和 {{{client.sub}}} 這兩個調用實際上調用的就是遠端方法,對於 ~JavaScript 客戶端來說,遠端方法名雖然可以不事先聲明,但這樣只能在 {{{client.onready}}} 事件發生後或者 {{{client.getReady()}}} 等於 true 時才能進行調用,但這種做法是為了保持與舊版本相容而提供的,屬於過時的方法,所以不推薦這種做法,而是強烈建議像上面那樣直接在客戶端代碼中指定所需要調用的遠端方法名。
回調函數有四個參數,你可以認為它們是伺服器端方法執行之後返回的內容。
第一個參數 {{{result}}} 是伺服器端方法(函數)的返回值,它可以是任意類型。
第二個參數 {{{args}}} 是方法調用的參數,如果這個調用是一個引用參數傳遞的調用,參數也有可能被修改,這時候,你可以通過 {{{args}}} 來獲得修改後的參數,關於引用參數傳遞的調用我們後面會做進一步說明。
第三個參數 {{{output}}} 是伺服器端輸出的內容,它是字串類型的。
第四個參數 {{{warning}}} 是伺服器端發生的警告錯誤(目前只有 PHP 伺服器會產生警告錯誤),一般只調試過程中可能會用到。
通過這個例子,我想你已經可以掌握 PHPRPC for ~JavaScript 客戶端的基本使用方法了。
!!如何在調用 PHPRPC 服務時,進行引用參數傳遞?
引用參數傳遞實際上非常簡單,看下面這個例子,首先來看 PHP 的伺服器端:
<code php>
<?php
include('phprpc_server.php');
function inc(&$n) {
$n++;
}
$phprpc_server = new PHPRPC_Server();
$phprpc_server->add('inc');
$phprpc_server->start();
?>
</code>
這個伺服器發佈了一個 {{{inc}}} 方法,該方法是將參數值加一。這個方法是需要引用參數傳遞的。下面我們來看看如何在 JavaScript 中調用這個遠程過程:
<code js>
var client = new PHPRPC_Client('http://localhost/index.php', ['inc']);
client.inc(1, function (result, args, output, warning) {
alert(args[0]);
}, true);
</code>
其實很簡單,只要在回調函數之後跟一個 {{{true}}} 參數就可以了。這個 {{{true}}} 就是表示啟用引用參數傳遞。
!!如何來得到遠端過程執行的錯誤資訊?
PHPRPC 把錯誤分為兩種,一種是致命錯誤,一種是警告性錯誤。
當遠端過程執行發生致命錯誤時,遠端程序呼叫的返回值是一個 {{{PHPRPC_Error}}} 類型的物件,它其中包含了遠端過程執行時發生的致命錯誤資訊。
當遠端過程執行發生警告性錯誤時,你可以通過回調函數的第四個參數 {{{warning}}} 得到警告錯誤,{{{warning}}} 的值也是 {{{PHPRPC_Error}}} 類型的物件。如果沒有發生警告錯誤,warning 為 {{{null}}}。
!! PHPRPC for ~JavaScript 是否支援自定義類型傳輸?
當然支持!所有在 ~JavaScript 中通過非匿名 {{{function}}} 定義的類型(對構造函數做 {{{new}}} 操作之後會得到該類型的物件)都可以序列化傳輸。
在 ~JavaScript 中通過匿名 {{{function}}} 定義的類型也支援序列化,但是反序列化後的類型為 {{{Object}}} 類型(對應於其他語言的字典或聯合陣列類型)。
自定義類型的名稱與 {{{function}}} 定義的構造函數名相同。
~JavaScript 中沒有名字空間的概念,不過仍然可以通過類比的方式實現。但如果要讓 PHPRPC 支持這種類比的名稱空間,需要在定義構造函數名時遵守以下約定:將包含名稱空間的完整類型名稱中的“.”號用“_”替換,將替換之後的字串作為構造函數名。例如:
<code js>
// top namespace
var MySpace = {};
// sub namespace
MySpace.util = {};
// full type name
MySpace.util.MyType = (function () {
// Hide MySpace_util_MyType
return function MySpace_util_MyType() {
...
}
})();
</code>
當這個類型的物件序列化後,就會被序列化為類型名稱為 {{{MySpace_util_MyType}}} 的一個物件了,而反序列化時,他會自動尋找 {{{MySpace.util.MyType}}} 這個名字來進行反序列化。這樣反序列化後,還可以得到原來該類型中定義的方法和屬性,因此通過這種方式,~JavaScript 中定義的類型就可以與 java、.NET 這種帶有名稱空間的語言中定義的類型進行交換了。這種命名約定在 PHPRPC 3.0 for ASP 中同樣適用。
PHPRPC 還支援自定義序列化方式,自定義序列化方式有兩種,一種是通過在自定義類型中實現 {{{__sleep}}} 和 {{{__wakeup}}} 這兩個方法來自定義序列化方式,這種較為簡單,但這種方式只能限制對哪些欄位進行序列化,而不能選擇序列化格式。另一種是在其他支援介面繼承的語言中通過為自定義類型實現 {{{Serializable}}} 介面來實現自定義序列化方式,這種較為複雜,允許自定義序列化格式。但是因為 ~JavaScript 是不支援介面繼承的,不過它仍然兩種方式都支援。只是第二種方式下,只需要在自定義類型中實現 {{{serialize}}} 和 {{{unserialize}}} 這兩個方法就可以了。
下面我們分別對這兩種方式進行一下簡要的介紹:
{{{__sleep}}} 方法是在物件被序列化之前自動觸發的。它的返回值類型是一個字串陣列,陣列的每一個元素是需要序列化的欄位名。你除了在 {{{__sleep}}} 方法中返回這些欄位之外,你也可以作其他一些操作,比如關閉檔或者關閉資料庫連接等操作。
{{{__wakeup}}} 方法是在物件被反序列化之後自動觸發的,它被觸發時,所有序列化的欄位都已經被反序列化完畢,所以,你在 {{{__wakeup}}} 中要做的是反序列化的事後工作,比如打開檔或著恢復資料庫連接等操作。
{{{__sleep}}} 和 {{{__wakeup}}} 方法無需同時實現,你只需把你用到的方法實現即可。
{{{serialize}}} 方法也是在物件被序列化時自動觸發的,你可以在 {{{serialize}}} 方法中對該物件做任何方式的序列化,例如 SOAP、JSON 或者 WDDX 等,只要返回的結果是 binray 字串就可以了。
{{{unserialize}}} 方法是在物件被反序列化時自動觸發的,傳入的參數就是前面 {{{serialize}}} 方法的返回內容。你可以用同樣的方式將它反序列化。{{{unserialize}}} 方法沒有返回值。
這兩種方式最多只能任選其一,如果兩種方式都實現的話,只有 {{{serialize}}} 和 {{{unserialize}}} 方式生效。
!!PHPRPC for ~JavaScript 是如何實現跨域調用的?
PHPRPC for ~JavaScript 的兩個版本都可以實現跨域調用,但是它們採用了不同的方法。
對於純 ~JavaScript 版本,你只需要一個 phprpc_client.js (壓縮版本的)檔就可以使用它,它採用的是動態添加和刪除 script 標籤的方法實現的跨域調用,該方法支持目前大多數現代流覽器,例如 IE 5.0+、Netscape 7.0+、Mozilla 1.0+、Firefox 1.0+、Opera 7.5+、Safari 2.0+、Konqueror 4.0+、Chrome 等。不過,採用該方法實現跨域調用時,傳遞的參數長度會有所限制,一旦長度超過流覽器(或伺服器)的限制後,將會調用失敗。因此,它比較適合用於做查詢操作的遠端調用,而不適合做提交資料和修改資料操作的遠端調用。
對於 ~JavaScript 和 Flash 混合實現的版本,你需要一個 phprpc_client.js(壓縮版本的)檔和一個 flashrequest.swf 檔,還需要在網頁裏包含 phprpc_flash.js 文件來插入 flashrequest.swf。另外,要實現跨域調用,伺服器端還必須要在根目錄下部署 crossdomain.xml。原理是用 Flash 代替 ~XMLHttpRequest 來實現跨域調用,雖然看上去比純 ~JavaScript 實現的要稍微麻煩一些,但是它支持所有的支持 ~FlashPlayer 9、10 的流覽器。而且對於參數長度沒有限制,另外,伺服器端的 crossdomain.xml 檔還可以對跨域做特殊限制。如果你需要跨域提交較大的資料,採用這個版本是一個比較合適的選擇。
PHPRPC for ~JavaScript 有 2 個版本的實現,一個是純 ~JavaScript 實現的版本(js 版本),另一個是 Flash 與 ~JavaScript 混合實現的版本(ajs 版本```這裏 ajs 的意思是 Another ~JavaScript 的意思,你也可以理解為 ~ActionScript & ~JavaScript 的意思。```)。
PHPRPC for ~JavaScript 兩個版本的安裝使用方法略有不同,我們下面分別來介紹一下。
!!js 版本的安裝
js 目錄下是純 ~JavaScript 版本的源代碼,其中的 compressed 目錄下是壓縮後的版本,壓縮後的版本有 2 個,直接存放在 compressed 目錄下的是不相容 Windows IE 5.0 的版本(但相容 Windows IE 5.5 及其以上版本),ie5compat 目錄下是相容 Windows IE 5.0 流覽器的版本。通常情況下,您不需要使用 ie5compat 的版本,這並不是因為 ie5compat 版本更大一些,運行效率更低一些。而是因為現在還在使用 IE 5.0 的用戶基本上已經不存在了。另一個更重要的原因是你頁面中的 HTML 部分可能根本就無法在 IE 5.0 上正常顯示,其他的(除了 PHPRPC 以外的)腳本也無法在 IE 5.0 上面正常運行。因為 IE 5.0 對 HTML 的顯示和 ~JavaScript 的支援實在是相當差勁。
你如果要在你的頁面中引用 PHPRPC for ~JavaScript,只需要把 compressed 下的 phprpc_client.js 複製到你的 Web 目錄的腳本目錄下,然後在你的頁面中像引用其他外部腳本一樣引用該腳本檔就可以了,引用的代碼看上去像下面這樣:
<code html>
<script type="text/javascript" src="scripts/phprpc_client.js"></script>
</code>
上面這句,你可以放在 head 中,也可以放在 body 中,只要放在你創建 ~PHPRPC_Client 物件之前就可以了。
!!ajs 版本的安裝
ajs 目錄下是 ~JavaScript 和 Flash 混合實現版本的源代碼。同樣,你只需要關心 compressed 下的檔(如果你不打算修改源代碼的話)。你需要做的同樣是將 phprpc_client.js 複製到你的 Web 目錄的腳本目錄下,同時把 phprpc_flash.js 也複製過去,而 flashrequest.swf 檔你可以放到 Web 目錄下的任何你覺得合適的目錄下。然後你需要將 phprpc_flash.js 中的 flashrequest.swf 路徑修改為你實際的 Web 路徑,修改後的代碼可能會像這樣:
<code js>
document.write(['<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" ',
'type="application/x-shockwave-flash" ',
'codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" ',
'width="0" height="0" id="flashrequest_as3">',
'<param name="movie" value="/scripts/flashrequest.swf" />',
'<param name="allowScriptAccess" value="always" />',
'<param name="quality" value="high" />',
'<embed src="/scripts/flashrequest.swf" type="application/x-shockwave-flash" ',
'width="0" height="0" name="flashrequest_as3" allowScriptAccess="always" />',
'</object>'].join(''));
</code>
最後,在你的頁面中引用這兩個腳本,其中 phprpc_client.js 可以放在 head 或者 body 中,而 phproc_flash.js 需要放到 body 中,但不要放到 form 中。
PHPRPC 最早就是在 PHP 語言中實現的,並且在 [[PHPRPC 資料表示]]中序列化資料部分就是採用的 PHP 語言中提供的序列化方式,所以這也是最初命名為 PHPRPC 的原因。不過現在 PHPRPC 支援的語言已經多達十幾種,並且因為它的序列化效率遠遠高於 XML 和 JSON,所以 PHPRPC 在命名解釋上又有了新的含義,那就是完美的高性能遠端程序呼叫(Perfect High Performance Remote Procedure Call)。不過既然 PHP 是讓 PHPRPC 誕生的最初語言,那麼我們就先來介紹 PHP 語言的 PHPRPC 實現吧。
本章的內容比較多,放在一整頁裏不便於閱讀,所以這裏列了一個目錄,你只要點擊相應的條目,就可以進入你想看的章節了。
*[[PHPRPC for PHP 的安裝]]
*[[PHPRPC for PHP 伺服器]]
*[[PHPRPC for PHP 客戶端]]
*[[PHPRPC for PHP 工具類]]
*[[PHPRPC for PHP 常見問題解答]]
!!發佈函數
在[[快速入門]]一章中,我們已經在 [[HelloWorld]] 的例子中看到過如何發佈一個函數了,這裏我們主要談一下哪些函數可以作為 PHPRPC 服務發佈。
實際上大部分函數都是可以作為 PHPRPC 服務發佈的,甚至包括 PHP 中的內置的函數。但如果參數或結果中包含有資源類型(比如 {{{mysql_connect}}},{{{mysql_query}}} 等),那麼這種函數就不能夠發佈。
你可以同時發佈多個函數,不論是你自定義的,還是 PHP 內置的都可以。例如:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
function hello($name) {
return 'Hello ' . $name;
}
$server = new PHPRPC_Server();
$server->add(array('hello', 'md5', 'sha1'));
$server->add('trim');
$server->start();
?>
</code>
在上面這個例子中我們發佈了 4 個方法,其中 {{{hello}}} 是我們自定義的函數,另外三個則是 PHP 帶的函數。我們可以把方法名放到陣列中,用 {{{add}}} 一次添加多個,也可以以字串作為 {{{add}}} 方法的參數一個一個的添加。
!!發佈方法
PHPRPC for PHP 也支援發佈類的靜態方法和物件的實例方法,如下例:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
class Example1 {
static function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}
$server = new PHPRPC_Server();
$server->add('foo', 'Example1');
$server->add('bar', new Example1());
$server->start();
?>
</code>
這裏 {{{foo}}} 是一個靜態方法,所以添加時第二個參數是類名。而 {{{bar}}} 是一個實例方法,所以添加時是一個 Example1 的實例物件。如果你以發佈靜態方法的方式發佈了一個實例方法的話,並不會報告錯誤,不過在調用時,你可能會得到警告錯誤。```當然如果你只關注結果的話,你並看不到警告錯誤,因為它不是像對話方塊一樣彈出來的,而是以程式可見的方式返回一個錯誤物件,只有當你去查看這個警告錯誤物件時,你才會發現它。```
現在你可能會有這樣的疑問,如果要同時發佈 2 個不同類中的同名方法的話,會不會有衝突呢?如何來避免衝突呢?
!!別名機制
確實會遇到這種情況,就是當發佈的方法同名時,後添加的方法會將前面添加到方法給覆蓋掉,在調用時,你永遠不可能調用到先添加的同名方法。不過 PHPRPC 提供了一種別名機制,可以解決這個問題。要用自然語言來解釋這個別名機制的話,不如直接看代碼示例更直接一些:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
function hello($name) {
return 'Hello ' . $name;
}
class Example1 {
static function foo() {
return 'foo';
}
function bar() {
return 'bar';
}
}
class Example2 {
function foo() {
return 'foo, too';
}
function bar() {
return 'bar, too';
}
}
$server = new PHPRPC_Server();
$server->add('hello', NULL, 'hi');
$server->add('foo', 'Example1', 'ex1_foo');
$server->add('bar', new Example1(), 'ex1_bar');
$server->add(array('foo', 'bar'), new Example2(), array('ex2_foo', 'ex2_bar'));
$server->start();
?>
</code>
從上面這個例子,我們就會發現不論是函數還是方法都可以通過別名來發佈,別名就是第三個參數。如果要發佈的函數使用別名的話,那麼第二個參數設置為 {{{NULL}}} 就可以了。同時添加多個方法(或函數)時,別名也應該是多個,並且個數要跟方法(或函數)名個數相同,且一一對應。
最後要注意的一點是,通過別名發佈的方法(或函數)在調用時如果用原方法(或函數)名調用是調用不到的,也就是說只能用別名來調用。
!!使用會話(Session)
在上面的例子中,雖然發佈了物件的實例方法,但是在那些方法中我們並沒有用到 {{{$this}}} 來訪問物件中的屬性。這樣的實例方法在實際中是很少見到的,它們基本上跟靜態方法沒什麼區別```除了以靜態方法來調用它們時會發出警告以外```。但如果你真的在實例方法中訪問了實例屬性後,你會發現你發佈的實例方法可能並沒有如你預期的那樣工作,例如:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
class ExampleCounter {
var $_count = 0;
function inc() {
$this->_count += 1;
}
function count() {
return $this->_count;
}
}
$server = new PHPRPC_Server();
$server->add(array('inc', 'count'), new ExampleCounter());
$server->start();
?>
</code>
這裏發佈了 {{{ExampleCounter}}} 實例物件的 2 個方法,但是不論你在客戶端調用了多少次 {{{inc}}} 後再調用 {{{count}}},返回的結果都是 0。
原因在於 PHP 的執行方式,每次遠端調用伺服器端時,伺服器端的頁面都是被重新載入執行的。因此,每次都會重新創建一個 {{{ExampleCounter}}} 物件實例,當一個調用結束後,這個實例物件就連同整個頁面內容一同自動銷毀了。
如果希望能夠像在本地調用中一樣使用物件,那麼你需要將這個物件保存到會話中,例如:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
class ExampleCounter {
var $_count = 0;
function inc() {
$this->_count += 1;
}
function count() {
return $this->_count;
}
}
if (!isset($_SESSION['counter'])) {
$_SESSION['counter'] = new ExampleCounter();
}
$server = new PHPRPC_Server();
$server->add(array('inc', 'count'), $_SESSION['counter']);
$server->start();
?>
</code>
這樣,通過客戶端調用時,就會得到你想要的結果了。你還可以在物件內通過存取 Session 來做到同樣的效果:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
class ExampleCounter {
function ExampleCounter() {
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
}
}
function inc() {
$_SESSION['count'] += 1;
}
function count() {
return $_SESSION['count'];
}
}
$server = new PHPRPC_Server();
$server->add(array('inc', 'count'), new ExampleCounter());
$server->start();
?>
</code>
到這裏,你可能已經明白了,這跟用 PHP 做動態網頁時使用 Session 是沒有什麼區別的。因此,如果你想要存取整個應用程式生命週期而不是在會話週期內使用的資料的話,就可以利用資料庫或者 Memcache 等來作為輔助工具實現了。但是因為資料庫和 Memcache 已經超出了 PHPRPC 的範圍,所以,我們就不再繼續討論了。
!!服務發佈選項
伺服器發佈還有幾個選項,通常情況下,你不需要對它們進行設置,因為它們的預設值就是最佳配置。下面是一個使用這些選項的例子:
<code php:firstline[0]>
<?php
include('php/phprpc_server.php');
function hello($name) {
return 'Hello ' . $name;
}
$server = new PHPRPC_Server();
$server->add('hello');
$server->setCharset('UTF-8');
$server->setDebugMode(true);
$server->setEnableGZIP(true);
$server->start();
?>
</code>
例子中,setCharset 是用來設置字元集的方法,實際上預設值就是 {{{'UTF-8'}}},所以,你只有要設置為其他字元集時才有必要用這個方法。但 ~UTF-8 字元集是在各種語言之間互通性最好的。
setDebugMode 是用來設置伺服器是否工作在調試模式下,調試模式下發佈的方法會返回更詳細的資訊錯誤資訊,比如出錯的檔、行號等資訊。一般來說,在最終發佈給調用者時,這個選項應該是關閉的,因為這樣可以讓你的伺服器更安全。所以預設值是 false。
setEnableGZIP 是用來啟動用 PHP 壓縮輸出的開關。啟動壓縮輸出雖然可以讓傳輸的資料量減少,但是它會佔用更多的記憶體和 CPU,因此它默認是關閉的。實際上很多 Web 伺服器(比如 Apache、lighttpd、nginx 等)也提供壓縮輸出的功能,所以一般情況下,沒有必要打開這個開關。
在[[快速入門]]的 HelloWorld 例子中,我們已經看過一個簡單的客戶端調用伺服器端的例子了。這裏我們來看一個更複雜一點的例子:
<code php:firstline[0]>
<?php
include ("php/phprpc_client.php");
$client = new PHPRPC_Client();
$client->setProxy(NULL);
$client->useService('http://127.0.0.1/server.php');
$client->setKeyLength(1000);
$client->setEncryptMode(3);
$client->setCharset('UTF-8');
$client->setTimeout(10);
echo $client->hi('PHPRPC'), "\r\n";
echo $client->getKeyLength(), "\r\n";
echo $client->getEncryptMode(), "\r\n";
echo $client->getCharset(), "\r\n";
echo $client->getTimeout(), "\r\n";
?>
</code>
這裏,我們調用的是在上一節中發佈的那個 {{{hi}}} 方法。這裏你會發現,我們在創建 {{{PHPRPC_Client}}} 的實例物件時,並沒有帶任何參數,這樣後面就需要用 useService 來指定伺服器位址。
setProxy 方法是用來設置 HTTP 代理伺服器位址的,當為 NULL,表示不使用代理伺服器。設置代理伺服器有 2 種方式,一種是通過 URL 形式:{{{http://username:password@host:port}}},這裏面 username 和 password 是可選的,port 也是可選的。這樣只要將第一個參數設置為這樣的字串就可以了。另一種是通過傳遞四個參數來設置這些資訊,按照順序分別是 host,port,username,password,其中 port,username,password 也都是可選的。
useService 方法用來設置伺服器位址,如果已經在初始化物件時設置了位址,這個方法就不需要調用了。但你可以通過這個方法來隨時改變客戶端物件中設置的伺服器位址。
setKeyLength 方法用來設置[[密鑰長度]]。
setEncryptMode 方法用來設置[[加密模式]]。
setCharset 方法用來設置客戶端的字元集。
setTimeout 方法用來設置超時時間,在 PHP 中這個超時時間的單位是秒。
getKeyLength 方法是用來獲取密鑰長度的,只有當進行過一次遠端調用後,你才能得到協商後的值。否則,就是你通過 setKeyLength 設定的值(或者是預設值 128)。
getEncryptMode 方法是用來獲取加密模式的,當進行過一次遠端調用後,如果伺服器不支持加密傳輸,該方法返回的結果為 0,否則為你設置的值。
getCharset 方法用來返回字元集,當進行過一次遠端調用後,該方法將返回伺服器端設置的字元集,否則返回你設置的字元集。
getTimeout 方法返回你設置的超時時間或者默認的超時時間(默認為 30 秒)。
!!引用參數傳遞
PHPRPC 客戶端在通過方法名直接調用伺服器端的方法時,是不採用引用參數傳遞的,原因在於遠端程序呼叫跟本地程序呼叫的引用參數傳遞從本質上是不一樣的。對於本地程序呼叫的引用參數傳遞,實際上是傳遞參數的地址(指針),這樣本地過程才可能修改引用參數的值(不是修改它的位址指針)。因為本地引用參數傳遞是通過指標來實現的,所以這種調用的開銷大部分情況下甚至比值傳遞還要低。而遠端程序呼叫時,就完全不是這樣了,我們不可能傳遞一個本地資料的指標到遠端去,因為在遠端伺服器上的同一個記憶體位址上是找不到我們的本地資料的。因此,遠端程序呼叫的參數傳遞,一定是將本地資料的值通過某種方式序列化後傳遞到遠端的,也就是值傳遞,那麼要想實現類似於本地程序呼叫中引用參數傳遞的效果(就是可以修改參數值),那麼就要將伺服器端修改後的參數的值也已某種方式序列化後再傳回到本地。因此遠端程序呼叫中的引用參數傳遞要比值傳遞的開銷還要大。這也是在 PHPRPC 客戶端實現中默認不採用引用參數傳遞的原因。
要想使用引用參數傳遞也不難,但這需要用 invoke 方法來調用遠端服務。我們來看一個例子。假設我們的伺服器端是用 PHP 發佈了一個 PHP 內置的對陣列排序的函數 sort,下面是我們在客戶端對它的調用:
<code php:firstline[0]>
<?php
include ("php/phprpc_client.php");
$client = new PHPRPC_Client('http://127.0.0.1/server.php');
$fruits = array("lemon", "orange", "banana", "apple");
$args = array(&$fruits, SORT_STRING);
print_r($fruits);
$client->invoke('sort', $args, true);
print_r($fruits);
?>
</code>
執行後,我們會看到如下結果:
{{{
Array
(
[0] => lemon
[1] => orange
[2] => banana
[3] => apple
)
Array
(
[0] => apple
[1] => banana
[2] => lemon
[3] => orange
)
}}}
很好,它成功了。
這裏我們要看到需要注意的三點:
第一,invoke 方法的第二個參數是一個引用參數,因此它必須是一個變數,所以我們不可以寫成:
<code php:nocontrols>
$client->invoke('sort', array(&$fruits, SORT_STRING), true);
</code>
否則,你會得到類似於這樣的錯誤資訊:
{{{
Fatal error: Cannot pass parameter 2 by reference in ...
}}}
第二,如果在給 {{{$args}}} 賦值時,{{{$fruits}}} 如果不是一個引用的話,那麼 {{{invoke}}} 調用後 {{{print_r($fruits);}}} 你並不會看到改變後的值,但如果你 {{{print_r($args);}}} 時,會發現 $args 的第一個元素仍然被改變了,也就是說,引用參數傳遞還是成功了。
第三,也是最重要的一點,invoke 的第三個參數,就是覺得是否引用參數傳遞的關鍵,如果它是 true,就是引用參數傳遞,如果它是 false,就是值傳遞。該參數的預設值是 false。也就是說,對於非引用參數傳遞的遠端過程,你也可以使用 invoke 來進行調用,只是看上去不那麼直觀。但是當你真正需要通過字串表示的方法名來調用遠端方法時,它就是你最好的選擇了。
!!PHPRPC for PHP 中的日期類
在 PHP 中雖然提供了不少的日期、時間的函數,但是這些函數都是用來操作字串或數位類型表示的日期時間的。在 PHP 中一直沒有提供一個用來表示日期和時間的類,直到 PHP 5.2 之後才加入了一個用來表示日期物件的 ~DateTime 類,而關於這個 ~DateTime 在官方文檔中也僅僅是[[一筆帶過|http://www.php.net/manual/en/migration52.datetime.php]],並未做詳細介紹。
為了能夠與其他語言中的日期類型進行資料交換,在 PHPRPC 中將日期類型統一為 ~PHPRPC_Date 這樣一個類。在其他語言中實現序列化和反序列化時,就將日期類都按照類名為 ~PHPRPC_Date 的物件進行處理,這樣不同的語言就可以直接交換日期類型的資料了。
!!!建立 ~PHPRPC_Date 物件
要獲得一個 ~PHPRPC_Date 物件可以通過以下四種方式:
#~PHPRPC_Date 構造函數
#~PHPRPC_Date 靜態方法 now
#~PHPRPC_Date 靜態方法 today
#~PHPRPC_Date 靜態方法 parse
這四種方式有區別又有聯繫,在不帶參數調用 ~PHPRPC_Date 構造函數時,得到的結果跟靜態方法 now 的返回結果相同。today 的返回的結果與 now 結果的日期部分相同,只是時間部分都是 0。~PHPRPC_Date 構造函數還支援整數和字串參數,parse 方法也支援這兩種類型的參數。整數是Unix 時間戳,這一點在構造函數和 parse 方法中是相同的。對於字串參數他們支援的不同,構造函數支援的字串格式與 strtotime 函數支援的字串格式相同。而 parse 支持的格式只有以下三種:
| 短格式 |2008-10-21 |
| 長格式 |2008-10-21 12:13:34 |
| 全格式 |2008-10-21 12:13:34.167 |
而對於最後一種,strtotime 是不支持的。另外,parse 的參數還可以是 ~PHPRPC_Date 物件,它將直接返回該參數本身。
!!!~PHPRPC_Date 的實例屬性
|{{{year}}}|年|
|{{{month}}}|月|
|{{{day}}}|日|
|{{{hour}}}|時|
|{{{minute}}}|分|
|{{{second}}}|秒|
|{{{millisecond}}}|毫秒|
!!!~PHPRPC_Date 的實例方法
|{{{addMilliseconds($milliseconds)}}} |將對象增加 {{{$milliseconds}}} 毫秒,{{{$milliseconds}}} 是整數,可以是正的,也可以是負的。|
|{{{addSeconds($seconds)}}} |將對象增加 {{{$seconds}}} 秒,{{{$seconds}}} 是整數,可以是正的,也可以是負的。|
|{{{addMinutes($minutes)}}} |將對象增加 {{{$minutes}}} 分,{{{$minutes}}} 是整數,可以是正的,也可以是負的。|
|{{{addHours($hours)}}} |將對象增加 {{{$hours}}} 小時,{{{$hours}}} 是整數,可以是正的,也可以是負的。|
|{{{addDays($days)}}} |將對象增加 {{{$days}}} 天,{{{$days}}} 是整數,可以是正的,也可以是負的。|
|{{{addMonths($months)}}} |將對象增加 {{{$months}}} 月,{{{$months}}} 是整數,可以是正的,也可以是負的。|
|{{{addYears($years)}}} |將對象增加 {{{$years}}} 月,{{{$years}}} 是整數,可以是正的,也可以是負的。|
|{{{after($when)}}} |如果當前物件表示的時間比 {{{$when}}} 要晚,則返回 {{{true}}},否則返回 {{{false}}}。{{{$when}}} 為 {{{parse}}} 可以解析的值。|
|{{{before($when)}}} |如果當前物件表示的時間比 {{{$when}}} 要早,則返回 {{{true}}},否則返回 {{{false}}}。{{{$when}}} 為 {{{parse}}} 可以解析的值。|
|{{{equals($when)}}} |如果當前物件表示的時間與 {{{$when}}} 相同,則返回 {{{true}}},否則返回 {{{false}}}。{{{$when}}} 為 {{{parse}}} 可以解析的值。|
|{{{set($year, $month, $day[, $hour, $minute, $second[, $millisecond]])}}} |設置當前物件的值。|
|{{{time()}}} |返回當前對象的 Unix 時間戳表示。|
|{{{toString()}}} |返回當前物件的字串表示,它與 {{{parse}}} 支持的全格式相同。|
|{{{dayOfWeek()}}} |返回當前物件表示的日期是星期幾,值為 0 - 6,0 表示星期日,1 表示星期一,…… 6 表示星期六。|
|{{{dayOfYear()}}}|返回當前物件表示的日期是當年內的第幾天。|
!!!~PHPRPC_Date 的靜態方法
|{{{dayOfWeek($year, $month, $day)}}}|返回指定的日期是星期幾,值為 0 - 6,0 表示星期日,1 表示星期一,…… 6 表示星期六。|
|{{{dayOfYear($year, $month, $day)}}}|返回指定的日期是年內的第幾天。|
|{{{isLeapYear($year)}}}|返回是否是閏年。|
|{{{daysInMonth($year, $month)}}}|返回指定的月份有多少天。|
|{{{isValidDate($year, $month, $day)}}}|指定日期如果是有效日期就返回 true,否則返回 false。|
|{{{isValidTime($hour, $minute, $second)}}}|指定時間如果是有效時間就返回 true,否則返回 false。|
!!問:
<<<
為何發佈伺服器時出現
{{{
Warning: Cannot modify header information - headers already sent ...
}}}
這樣的錯誤?
<<<
!!答:
<<<
一般來說,在使用 PHPRPC 發佈服務時,{{{include('php/phprpc_server.php');}}} 是放在第一句的,這是因為在 phprpc_server.php 中有初始化 Session 和輸出緩存區的語句,把它放在開始,就可以保證服務啟動時不會因為其他程式輸出而導致出現上面的了```這個錯誤是在直接用流覽器打開這個服務頁時看到的,而不是在客戶端調用時,所以要測試伺服器是否正常,最好的方式是先用流覽器看一下有沒有 {{{functions="...";}}} 這樣的輸出,其中 ... 表示一些看上去亂七八糟的大小寫不一且與數位混雜的字串,實際上那是 base64 編碼過的伺服器端方法名稱列表,如果你看到這個列表,那麼表示你的客戶端已經可以調用你發佈的服務了。```。但是如果在 {{{include('php/phprpc_server.php');}}} 之前包含的其他 PHP 程式都不會有輸出的話,那麼也是可以的。
如果你保證它已經放在了第一句或者之前沒有輸出,仍然出現上面那種錯誤資訊的話,那麼請檢查 {{{<?php}}} 之前是不是有字元,在 {{{<?php}}} 之前出現的任何字元都會影響伺服器的正常發佈。有時候這種字元在你的編輯器(比如 Windows 的記事本)中是不可見的,比如 ~UTF-8 的 BOM 資訊,這時候你需要用一個可以去掉 BOM 資訊的編輯器來重新保存你的程式。
<<<
----
!!問:
>如何使用輸出重定向?
!!答:
>伺服器端很簡單,只要發佈的方法中有 {{{echo}}}、{{{print}}}、{{{print_r}}} 之類的輸出,這些輸出就可以重定向到客戶端,而客戶端通過 {{{getOutput}}} 方法就可以獲取到它。它是一個字串值。如果伺服器端沒有輸出,{{{getOutput}}} 方法將返回一個空字串。
----
!!問:
>如果與其他語言之間傳遞自定義類型的物件?
!!答:
<<<
大部分語言是支援名空間的,而 PHP 語言卻是不支援名空間的(雖然 PHP 5.3 支持名空間,但是因為目前還是 alpha 版本,所以它對帶有名空間的類如何序列化尚未明朗),那麼如何讓它們當中定義的類能相互識別呢?PHPRPC 在這方面做了一些變通。對於支援名空間的語言,它在序列化帶有名空間的類時,會將名空間與類名之間的 {{{.}}} 變成 {{{_}}},例如假設 Java 中有這樣一個類 {{{MyNameSpace.MySubNameSpace.MyClass}}},那麼在 PHP 中這樣定義 {{{MyNameSpace_MySubNameSpace_MyClass}}} 就可以跟 Java 中的類進行資料交換了。
<<<
該版本直接解壓後就可以使用,其中
*bigint.php
*compat.php
*phprpc_date.php
*xxtea.php
屬於公共檔。不論是客戶端還是伺服器端都需要這些檔。
*phprpc_client.php
是客戶端檔,如果你只需要使用客戶端,那麼只要有上面那些公共檔和這個檔就可以使用了,使用時,直接在你的程式中包含 phprpc_client.php 就可以,公共檔不需要單獨包含。
*dhparams
*dhparams.php
*phprpc_server.php
這三個檔是伺服器端需要的檔。
其中 dhparams 目錄中包含的是加密傳輸時用來生成密鑰的參數,如果你需要更詳細的解釋,請參見 [[PHPRPC 加密傳輸]]。
dhparams.php 是用來讀取 dhparams 目錄中檔的類。
phprpc_server.php 是伺服器端,如果你要使用 PHP 來發佈 PHPRPC 服務,只需要包含這個檔就可以了。公共文件和 dhparams.php 都不需要單獨包含。
!!運行環境
*PHP 4.3+、PHP 5、PHP 6
*客戶端要求開啟 socket 擴展。
*伺服器端需要有 IIS、Apache、lighttpd 等可以運行 PHP 程式的 Web 伺服器。
*如果伺服器端需要加密傳輸的能力,必須要保證 session 配置正確。
如果需要加密傳輸能力,最好開啟大整數計算擴展,PHPRPC 支援的大整數計算擴展按照計算速度(由快到慢)排名是 gmp、big_int、bcmath。如果這些擴展你一個都沒有開啟,則密鑰交換將使用 PHP 腳本來類比大整數運算,這並不影響加密傳輸的功能,只是速度會慢一些。
如果你需要更快的加密處理能力,除了開啟上面所說的大整數計算擴展以外,你還可以下載並安裝 [[xxtea 的 PECL 擴展|http://www.phprpc.org/pecl/xxtea-1.0.3.zip]],該擴展採用 C 編寫,它能夠有效的提高加密速度。
!!PECL 版本的 xxtea 擴展安裝方法
安裝方法有多種,下面介紹最常用的三種安裝方法:
#跟 PHP 一同編譯安裝
#使用 phpize 工具編譯安裝
#在 Windows 下使用 Microsoft Visual C(.NET 或 6.0)編譯安裝
!!!方法一:跟 PHP 一同編譯安裝
#在 PHP 源碼檔夾下創建 ext/xxtea 檔夾,將所有檔複製到新創建的檔夾下。
#運行 {{{./buildconf}}} 重新構建 PHP 的配置腳本。
#帶選項編譯 PHP:{{{--enable-xxtea}}} 將作為 PHP 內置模組編譯,{{{--enable-xxtea=shared}}} 將作為動態載入模組編譯。
!!!方法二:使用 phpize 工具編譯安裝
#解壓縮該包內容。
#運行 {{{phpize}}} 腳本,為編譯 XXTEA 包準備環境。
#運行 {{{./configure --enable-xxtea=shared}}} 生成 makefile。
#運行 {{{make}}} 編譯 XXTEA 擴展庫,它將被放置在 ./modules 文件夾下。
#運行 {{{make install}}} 安裝 XXTEA 擴展庫到 PHP。
!!!方法三:在 Windows 下使用 Microsoft Visual C(.NET 或 6.0)編譯安裝
#在 PHP 源碼檔夾下創建 ext/xxtea 檔夾,將所有檔複製到新創建的檔夾下。
#從你所使用的版本的 PHP 檔夾下複製 php4ts.lib(PHP 4 使用)或 php5ts.lib(PHP 5 使用)靜態庫到 ext/xxtea 檔夾下。
# 打開 php_xxtea.sln(MS VC.NET 的解決方案文件)或者 php_xxtea.dsw(MS VC 6.0 的工作環境文件)。嘗試編譯 Release_php4 (PHP 4 使用) 或 Release_php5(PHP 5 使用)配置。
#從 ext/xxtea/Release_php4 或 ext/xxtea/Release_php5 下複製 php_xxtea.dll 到你所使用的 PHP 擴展檔夾下。擴展檔夾的路徑在 php.ini 中可以找到。
# 在 php.ini 中添加這一行 {{{extension=php_xxtea.dll}}}
2008 年至今最熱的話題應該就是雲計算 (cloud computing)了,Google、Amazon、Alibaba、IBM、Microsoft 都提出了其各自的雲計算方案,目前最便宜、最好用的雲計算當屬 Google 的 Google App Engine(免費額度足以讓你營運每個月 500 萬 PV)。不過要用純 Python```Google App Engine 目前只提供對 Python 的支援。``` 開發完整的雲端應用,也確實有些困難。不過現在有了 PHPRPC for Python,你也可以輕鬆漫步在雲端,體驗雲計算了~
讓我們開始漫步在雲端吧~
*[[PHPRPC for Python 的安裝]]
*[[PHPRPC for Python 伺服器]]
*[[PHPRPC for Python 客戶端]]
*[[PHPRPC for Python 常見問題解答]]
!!最簡單的獨立 PHPRPC 伺服器
<code python>
from phprpc import PHPRPC_Server # 引入 PHPRPC Server
import datetime
def helloworld():
return 'helloworld'
def hi(name):
return 'hi %s' % name
server = PHPRPC_Server()
server.add(helloworld)
server.add('hi')
server.add(hi, 'hello')
server.add(datetime.datetime.now)
server.debug = True
server.start()
</code>
你會發現它跟Ruby、PHP 版本的伺服器代碼基本上一樣。不是嗎?
下面來看一下更通用的 {{{PHPRPC_WSGIApplication}}} 如何使用。
!!以 WSGI 方式發佈 PHPRPC 服務
下面這個例子中我們使用一下 [[flup|http://trac.saddi.com/flup/wiki/FlupMiddleware]] 這個中間件,它提供了 Session 支援的能力(有了 Session 支援才能提供加密傳輸的能力)。當然你還可以使用其他的 WSGI 的 Session 中間件(比如 [[beaker|http://pypi.python.org/pypi/Beaker]] 或者其他相容的 Session 中間件),這裏就不在舉例。
<code python>
from flup.middleware.session import MemorySessionStore, SessionMiddleware
from flup.middleware.gzip import GzipMiddleware
from phprpc import PHPRPC_WSGIApplication, UrlMapMiddleware, PHPRPC_Server
import datetime
def helloworld():
return 'helloworld'
def hi(name):
return 'hi %s' % name
app = PHPRPC_WSGIApplication()
app.add(helloworld)
app.add('hi')
app.add(hi, 'hello')
app.add(datetime.datetime.now)
app.debug = True
app = UrlMapMiddleware([('/', app)])
sessionStore = MemorySessionStore()
app = SessionMiddleware(sessionStore, app)
app = GzipMiddleware(app)
PHPRPC_Server(app = app).start()
</code>
上面這個 WSGI 的 app 最終還是使用 {{{PHPRPC_Server}}} 來發佈的。但是因為這個 app 是個一個標準的 WSGI 應用程式,所以,你還可以用任何支援 WSGI 的伺服器發佈它,比如下面的 Google App Engine。
!!在 Google App Engine 上發佈 PHPRPC 服務
<code python>
from phprpc import PHPRPC_WSGIApplication, UrlMapMiddleware
from google.appengine.ext.webapp.util import run_wsgi_app
import datetime
def helloworld():
return 'helloworld'
def hi(name):
return 'hi %s' % name
app = PHPRPC_WSGIApplication()
app.add(helloworld)
app.add('hi')
app.add(hi, 'hello')
app.add(datetime.datetime.now)
app.debug = True
app = UrlMapMiddleware([('/', app)])
run_wsgi_app(app)
</code>
這裏有一點要注意,Google App Engine 實際上是以分散式的 CGI 方式來運行的,所以你不能使用 flup 的基於記憶體 Session 中間件,但你可以使用其他支援 Google App Engine 的 Session 中間件。
!!發佈函數
如上面代碼中所看到的,你可以如此發佈函數
<code python>
app.add(helloworld)
app.add('hi')
</code>
直接用函數名或者函數名的字串表示即可。
!! 發佈方法
你可以發佈系統內置物件的方法,也可以是自定義的類或物件的方法。
系統內置物件的方法:
<code python>
app.add(datetime.datetime.now)
</code>
自定義類的方法:
<code python>
class Test(object):
def __init__(self):
pass
def hi(self, name):
return 'hi : ' + name
test = Test()
app.add(test.hi)
</code>
除了可以一個一個的添加外,還可以把方法名、函數名(或其字串表示)放到陣列中,用 add 一次添加多個。例如:
<code python>
app.add([helloworld, "hi", datetime.datetime.now])
</code>
!!將函數或方法以別名方法發佈
<code python>
app.add(hi, 'hello') # 將 hi 以別名 hello 發佈
</code>
!!如何發佈 lambda
<code python>
app.add(lambda x,y: x + y, 'add')
</code>
!!如何在發佈的函數或方法中使用 Session
只要你發佈的函數或方法的參數列表中,最後一個參數是 session 命名的參數,當客戶端調用該函數或方法時,伺服器就會自動將 Session 物件傳入了。不過要注意,客戶端不要傳入這個 session 參數。
現在就開始和 PHPRPC 雲端漫步吧!
!!客戶端實例
<code python>
from phprpc import PHPRPC_Client
client = PHPRPC_Client('http://bawbaw.icittys.com/rpc_server.php')
clientProxy = client.useService()
print client.add(1, 2) # 將顯示3
print clientProxy.add(1, 2) # 另一種呼叫方法
</code>
這裏 {{{clientProxy}}} 唯一的用處就是當伺服器端發佈的方法與客戶端 {{{client}}} 提供的方法或屬性重名的時候,{{{clientProxy}}} 可以讓你方便且正確的調用遠端方法。
!!加密傳輸
<code python>
from phprpc import PHPRPC_Client
client = PHPRPC_Client('http://bawbaw.icittys.com/rpc_server.php')
client.keylength = 256 # 加密長度
client.encryptmode = 2 # 雙向加密
print client.add(1, 2) # 將顯示3
</code>
keylength 用於設置[[密鑰長度]]。
encryptmode 用於設置[[加密模式]]。
!!Google App Engine 上的 PHPRPC 客戶端
由於 Google App Engine 禁用了{{{urllib}}} 等 HTTP Client,因此在 Google App Engine 中引入 {{{PHPRPC_Client}}} 的方法有所不同。
<code python>
from phprpc.GoogleAppEngine import PHPRPC_Client
</code>
另外,Google App Engine 的 {{{PHPRPC_Client}}} 不支援 HTTP 代理伺服器設置功能,而普通的 {{{PHPRPC_Client}}} 支援。
不過從 Google App Engine 1.1.9 開始,已經提供了對 Python 標準 web 請求 {{{urllib}}},{{{urllib2}}} 和 {{{httplib}}} 的支援,因此普通的 {{{PHPRPC_Client}}} 可以直接在 Google App Engine 1.1.9 及其之後的版本中使用,而無需從 {{{phprpc.GoogleAppEngine}}} 名空間中導入。
!!PHPRPC for Python 是否支援自定義類型?
支援,只要名稱空間(模組名)、類名和實例變數定義可以與其他語言中定義的相對應,就可以跨語言傳遞。例如下面這段 python 程式
<code python>
class MyClass:
def __init__(self):
self.name = 'PHPRPC'
self.age = 3
self.ok = True
</code>
如果它定義在 ~MyModule.py 中,則它的模組名就是 {{{MyModule}}},那麼這個 {{{MyClass}}} 與下面這段 Java 程式
<code java>
package MyModule;
class MyClass implements java.io.Serializable {
private String name;
protected int age;
public boolean ok;
MyClass() {
name = "PHPRPC";
age = 3;
ok = true;
}
}
</code>
中的 {{{MyClass}}} 就屬於 PHPRPC 支援的可以跨語言傳輸的自定義資料類型。
上面你還會注意到,Java 中不管是私有成員、保護成員還是公共成員都是可以跟 Python 中的實例變數進行資料交換的。
當然,更複雜一些,即使名稱空間和類名不相同,但是符合以下規則也可以進行跨語言傳輸:
如果將名稱空間與類名之間的分隔符號(例如 {{{.}}},{{{::}}} 等)全部替換為 {{{_}}} 之後,可以得到相同的名稱,則這兩個類型在 PHPRPC 中也被認為是可以跨語言傳輸的類型。
之所以這樣設計是為了可以與不支援名稱空間的語言進行交互,例如下面這段 python 程式
<code python>
class MyClass:
def __init__(self):
self.name = 'PHPRPC'
self.age = 3
self.ok = True
</code>
我們仍然假設它定義在 ~MyModule.py 中,它的模組名是 {{{MyModule}}},那麼這個 {{{MyClass}}} 就可以跟下面這段 PHP 程式
<code php>
class MyModule_MyClass {
var $name;
var $age;
var $ok;
}
</code>
中的 {{{MyModule_MyClass}}} 通過 PHPRPC 進行相互交換了。
!!PHPRPC for Python 如何與 Java 中實現了 org.phprpc.util.Serializable 介面的自定義類型進行交互?
目前 PHPRPC 所支援的語言中,對所有支援介面的語言都有類似於 Java 中 {{{org.phprpc.util.Serializable}}} 介面的機制。而 Python 語言本身並沒有介面機制,那麼是否意味著將不能與實現了這種機制的語言交換這種類型的資料了呢?
不是的,在 PHPRPC for Python 中你同樣可以實現這種自定義序列化機制,只不過不再需要介面,而是利用鴨子類型的機制。也就是說,只要在你定義的類中,實現 {{{serialize}}} 和 {{{unserialize}}} 方法,就可以與其他實現了這種自定義序列化機制的語言進行這種類型的資料交互了。
同樣,{{{__sleep}}} 和 {{{__wakeup}}} 方法的序列化機制也支援。
!!為何從客戶端傳來的列表或元組參數在伺服器端會變成字典呢?
!!為何從伺服器端返回的列表或元組到客戶端也會變成字典呢?
!!我該如何讓它們變回列表或元組?
這三個問題實際上是同一個問題,因為 PHP 序列化會將列表、元組和字典序列化為同一種類型的資料表示,因此在反序列化時都會被反序列化為字典類型的資料。對於其他強類型語言來說,可以根據參數類型或返回結果類型聲明來自動進行類型轉換。但是 Python 是弱類型語言,因此無法獲知參數或結果是何種類型,因此只能以不丟失結果的字典類型來返回。
如果你希望得到列表,那麼你可以使用 {{{phpformat}}} 中的 {{{dict_to_list}}} 方法來得到。如果你希望得到元組,那麼你可以使用 {{{phpformat}}} 中的 {{{dict_to_tuple}}} 方法來得到。
直接從
{{{
http://www.phprpc.org/download/
}}}
或
{{{
http://pypi.python.org/pypi/phprpc/
}}}
下載 PHPRPC for Python 源碼,之後解壓將 py25 裏的檔案放置到專案中即可。
近期的 Web 開發領域出現一項開發利器 ~RoR(Ruby on Rails),但是 Rails 學習曲線及新舊版不相容問題很令人頭痛!!!
在此風潮下,順勢推出 PHPRPC for Ruby,現在不用學習 Rails 也可以使用 Ruby 開發 Web 了~
PHPRPC for Ruby 可以讓你更開心,更快樂的編程。
*[[PHPRPC for Ruby 的安裝]]
*[[PHPRPC for Ruby 伺服器]]
*[[PHPRPC for Ruby 客戶端]]
*[[PHPRPC for Ruby 常見問題解答]]
PHPRPC for Ruby 伺服器支持 mongrel、thin、ebb 或 webrick 這四種獨立伺服器,同時還支持以 cgi、fcgi、scgi、lsapi 方式運行。但是伺服器端程式的寫法都是一樣的,不同的僅僅是啟動參數。所以這裏主要以獨立伺服器方式運行來介紹。
Webrick 是 Ruby 自帶的一個 Web 伺服器,調試程式時可以使用它,但如果要在實際環境中部署則推薦 Mongrel 和 Thin 這兩個 Web 伺服器,雖然 ebb 伺服器號稱是最快的(比 Thin 還要快),但是目前的 ebb 還不夠穩定。另外,在 Windows 下推薦 mongrel,Linux/Unix 下則推薦 Thin。因為 Thin 在 Windows 下也是不夠穩定的。
如未安裝 Mongrel 或 Thin 先安裝(擇一安裝或兩種都安裝)
{{{
gem install mongrel
gem install thin
}}}
!!發佈函數
實際上大部分函數都是可以作為 PHPRPC 服務發佈的,甚至包括 Ruby 中的內置的函數。你可以同時發佈多個函數,不論是你自定義的,還是 Ruby 內置的都可以。例如:
<code ruby>
#!/usr/bin/env ruby
require 'rubygems'
require 'phprpc'
def add(a, b)
a + b
end
def sub(a, b)
a - b
end
def hello(s)
"hello: " << s
end
server = PHPRPC::Server.new
server.debug = true
server.add(["add", "sub", "hello"]) #將上列的function發佈
server.start
</code>
我們可以把方法名放到陣列中,用 add 一次添加多個,也可以以字串作為 add 方法的參數一個一個的添加。
!!如何啟動 PHPRPC for Ruby 伺服器
1、使用 Mongrel 發佈 PHPRPC
{{{ruby server.rb mongrel}}}
2、使用 Thin 發佈 PHPRPC
{{{ruby server.rb thin}}}
如何?很簡單吧。
此時發佈網址為 http://localhost:3000/
如果想要改變一些參數,請用
{{{ruby server.rb mongrel --help}}}
或
{{{ruby server.rb thin --help}}}
就可以知道如何改變及設定啟動參數
除了提供基本函數發佈外,也可以將類中的方法或者物件方法發佈,另外還可以將函數或方法以別名方式發佈。
!!將 function 以別名方法發佈
假設要將 hello 以另一個名稱 hihi 發佈方法,就使用
<code ruby:nocontrols>
server.add("hello",nil,"hihi")
</code>
!!將類或物件方法發佈
先寫一個名為 Test 的 class:
<code ruby>
class Test
def initialize()
end
def hi(str)
"hi " << str
end
def time
Time.now
end
def self.showtime
'it is a show time'
end
end
</code>
此時要將 Test 的 hi 和 time,showtime 發佈很簡單,看下面示例:
<code ruby>
server.add("time", Test.new)
server.add("hi", Test.new)
server.add("showtime", Test) #不用將Test實例化
server.add("hi", Test.new, 'hi2') #別名
</code>
!!如何發佈 block 以及 lambda
直接可以發佈區塊的匿名方法,直觀又不會命名衝突,是個不錯的選擇:
<code ruby:nocontrols>
server.add("good") {|str| str<<" is good"}
</code>
假設有一個 lambda 是要計算兩個數的總合:
<code ruby:nocontrols>
sum = lambda {|a,b| a+b}
</code>
此時要使用 PHPRPC for Ruby 發佈只要使用:
<code ruby:nocontrols>
server.add("sum",nil,nil,&sum)
</code>
使用&符號在 sum 前頭,這樣子就可以發佈了。
!!如何在發佈的函數或方法中使用 Session
只要你發佈的函數或方法的參數列表中,最後一個參數是 session 命名的參數,當客戶端調用該函數或方法時,伺服器就會自動將 Session 物件傳入了。不過要注意,客戶端不要傳入這個 session 參數。
現在大家就動手測試吧!
客戶端使用更加簡單,這裏直接以代碼實例進行說明:
!!客戶端程式碼
<code ruby>
#!/usr/bin/env ruby
require 'rubygems'
require ‘phprpc’
rpc_url="http://bawbaw.icittys.com/rpc_server.php"
client = PHPRPC::Client.new(rpc_url) # 初始化一個PHPRPC client
puts client.add(1, 2) # 調用遠端函數add(1,2)將列印出 3
puts client.sub(1, 2) # 調用遠端函數sub(1,2)將列印出 -1
puts client.hello('Ma Bingyao') #調用遠端函數 hello('Ma Bingyao') 將列印出 hello Ma Bingyao
</code>
!!加密傳輸
<code ruby>
#!/usr/bin/env ruby
require 'rubygems'
require ‘phprpc’
rpc_url="http://bawbaw.icittys.com/rpc_server.php"
client = PHPRPC::Client.new(rpc_url) #初始化一個PHPRPC client
client.keylength = 256 # 交換密鑰長度 256
client.encryptmode = 2 # 雙向加密
puts client.add(1, 2) # 調用遠端函數 add(1,2) 將列印出 3
puts client.sub(1, 2) # 調用遠端函數 sub(1,2) 將列印出 -1
puts client.hello('Ma Bingyao') # 調用遠端函數 hello('Ma Bingyao') 將列印出 hello Ma Bingyao
</code>
keylength 用於設置[[密鑰長度]]。
encryptmode 用於設置[[加密模式]]。
沒看過這麼簡單的 Web Service 吧?安全又快速!
!!PHPRPC for Ruby 是否支援自定義類型?
支援,只要名稱空間(模組或上層類名)、類名和實例變數定義可以與其他語言中定義的相對應,就可以跨語言傳遞。例如下面這段 ruby 程式
<code ruby>
module Test1
class Test2
class Test3
attr_accessor :a, :b, :c
end
end
end
</code>
中的 {{{Test3}}} 與下面這段 Java 程式
<code java>
package Test1.Test2;
class Test3 implements java.io.Serializable {
private int a;
protected String b;
public boolean c;
Test3() {
a = 1;
b = "Hello";
c = true;
}
}
</code>
中的 {{{Test3}}} 就屬於 PHPRPC 支援的可以跨語言傳輸的自定義資料類型。
上面你還會注意到,Java 中不管是私有成員、保護成員還是公共成員都是可以跟 Ruby 中的實例變數進行資料交換的。
當然,更複雜一些,即使名稱空間和類名不相同,但是符合以下規則也可以進行跨語言傳輸:
如果將名稱空間與類名之間的分隔符號(例如 {{{.}}},{{{::}}} 等)全部替換為 {{{_}}} 之後,可以得到相同的名稱,則這兩個類型在 PHPRPC 中也被認為是可以跨語言傳輸的類型。
例如:
例如下面這段 ruby 程式
<code ruby>
module Test1
class Test2
class Test3
attr_accessor :a, :b, :c
end
end
class Test2_Test3
attr_accessor :a, :b, :c
end
end
</code>
中的 {{{Test3}}} 和 {{{Test2_Test3}}} 在通過 PHPRPC 傳輸時將被認為是相同的類型,因此在同一個程式中定義類名時要注意避免這種情況發生。
之所以這樣設計是為了可以與不支援名稱空間的語言進行交互,例如下面這段 ruby 程式
<code ruby>
module Test1
class Test2
class Test3
attr_accessor :a, :b, :c
end
end
end
</code>
中的 {{{Test3}}} 是可以跟下面這段 PHP 程式
<code php>
class Test1_Test2_Test3 {
var $a;
var $b;
var $c;
}
</code>
{{{Test1_Test2_Test3}}} 通過 PHPRPC 進行相互交換的。
!!PHPRPC for Ruby 如何與 Java 中實現了 org.phprpc.util.Serializable 介面的自定義類型進行交互?
目前 PHPRPC 所支援的語言中,對所有支援介面的語言都有類似於 Java 中 {{{org.phprpc.util.Serializable}}} 介面的機制。而 Ruby 語言本身並沒有介面機制,那麼是否意味著將不能與實現了這種機制的語言交換這種類型的資料了呢?
不是的,在 PHPRPC for Ruby 中你同樣可以實現這種自定義序列化機制,只不過不再需要介面,而是利用鴨子類型的機制。也就是說,只要在你定義的類中,實現 {{{serialize}}} 和 {{{unserialize}}} 方法,就可以與其他實現了這種自定義序列化機制的語言進行這種類型的資料交互了。
同樣,{{{__sleep}}} 和 {{{__wakeup}}} 方法的序列化機制也支援。
!!為何從客戶端傳來的陣列參數在伺服器端會變成 Hash 呢?
!!為何從伺服器端返回的陣列到客戶端也會變成 Hash 呢?
!!我該如何讓他們變回陣列?
這三個問題實際上是同一個問題,因為 PHP 序列化會將陣列和 Hash 序列化為同一種類型的資料表示,因此在反序列化時都會被反序列化為 Hash 類型的資料。對於其他強類型語言來說,可以根據參數類型或返回結果類型聲明來自動進行類型轉換。但是 Ruby 是弱類型語言,因此無法獲知參數或結果是何種類型,因此只能以不丟失結果的 Hash 類型來返回。
如果你希望得到陣列,那麼你可以使用 Hash 物件的 values 方法來得到。
從 Gem 安裝,在命令行中輸入:
{{{gem install phprpc}}}
如果你看到輸出:
{{{
Successfully installed phprpc-3.0.4
1 gem installed
}}}
就說明已經安裝好,可以用了!沒錯就是這麼的簡單!!
!!伺服器端發佈的函數列表
PHPRPC 不同於 Web Service,它沒有 WSDL 這樣的服務定義描述檔,也不需要這樣的東西。這樣既方便了伺服器的部署,又方便了弱類型與強類型語言之間的互通。但是你仍然可以得到伺服器端發佈的函數列表,但是這個函數列表中僅包含函數名,而不包含有函數參數與結果的描述,因為這樣可以更方便的支持變參傳遞和弱類型傳遞。當客戶端請求中不包含發起調用或密鑰交換的必要參數時,則伺服器端應返回發佈的函數列表,其格式為:
{{{
phprpc_functions="<functions_list>";
}}}
其中<functions_list>為序列化並編碼後的函數名陣列。其編碼規則取決於請求中 {{{phprpc_encode}}} 的值,如果請求中包含有 {{{phprpc_callback}}} 參數,也應按照調用時做同樣處理。
!!版本號
PHPRPC 客戶端可以通過 HTTP 頭~User-Agent 來標示自己的版本。但伺服器不應該將其作為識別客戶端版本的依據。伺服器端只需依據客戶端的請求的行為來做成正確的回應即可。
PHPRPC 伺服器通過 HTTP 頭 ~X-Powered-By 來設置版本號,其格式為:PHPRPC Server/3.0。
客戶端可以以此作為伺服器版本的判斷依據,但不應作為伺服器端支援特徵的判斷依據。客戶端應以伺服器回應的行為作為伺服器支持特性的判斷依據,例如如果客戶端發起密鑰交換請求時,伺服器端返回上面所提到的伺服器端發佈的函數列表,而不是返回用於生成密鑰的資訊,客戶端便可以以此來判斷伺服器端不支援加密傳輸。
!!字元集
無論是客戶端還是伺服器端對字串與位元組陣列之間的轉換(例如 Java、.NET 等雙位元組字元編碼的語言實現的伺服器)都是以伺服器端的字元集設置為准。客戶端的請求中可以包含字元集設置,但是應與伺服器端設置相同,在與伺服器端設置不同的情況下,按照伺服器端設置的字元集進行處理。客戶端的字元集選項僅用於第一次與伺服器通訊前,在客戶端未知伺服器字元集的情況下,讓用戶去設置與伺服器端相同的字元集。另外,建議統一使用 ~UTF-8 字元集,在這種情況下,可以保證所有伺服器與客戶端正常通訊,如設置其他字元集,只能保證兩個相同字元集設置的伺服器和客戶端可以正常通訊。
字元集通過 HTTP 頭 ~Content-Type 來設置。
!!跨域存取 Cookie
確切的說,這不是 PHPRPC 協定範圍中的內容。但卻對 PHPRPC 實現跨域訪問很有必要。對於非 IE 流覽器來說,跨域存取 Cookie 是沒有問題了,但是對於 IE 流覽器來說,需要伺服器端發送:
{{{
P3P: CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
}}}
這樣一個 HTTP 頭才可以允許客戶端接收跨域的 Cookie。因此,在實現 PHPRPC 伺服器時,建議伺服器端發送該頭。
!!緩存
PHPRPC 是不允許緩存的,因為遠端程序呼叫的結果是未知可變的,也就是說遠端程序呼叫不能保證是冪等性(idempotency)操作,大多數情況下都是非冪等性操作的。
因此 PHPRPC 建議伺服器在輸出回應時,應該發送禁止緩存的頭資訊,例如:
{{{
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
}}}
!!加密調用
前面所描述的是在非加密調用情況下的請求和回應,而在加密調用的情況下,則請求中的 {{{phprpc_args}}} 會在序列化之後,先進行加密,再進行編碼,即:B(C(S(參數陣列))):其中 B 為 Base64 編碼,C 為加密操作,S 為 [[PHP 序列化]]。
回應中的某些資訊在加密調用的情況下也會增加加密操作,但是究竟要對那些資訊進行加密是根據請求中的 {{{phprpc_encrypt}}} 參數決定的。{{{phprpc_encrypt}}} 表示加密模式。
<<tiddler [[加密模式]]>>
預設值為 0,也就是非加密調用。
當 {{{phprpc_encrypt}}} 為 1 時,回應中的 {{{phprpc_args}}} 在序列化之後,先進行加密,再進行編碼,即:E(C(S(參數陣列))),其中 E 為編碼操作(Base64 或 ~JavaScript 字串編碼),C 為加密操作,S 為 [[PHP 序列化]]。
當 {{{phprpc_encrypt}}} 為 2 時,回應中的 {{{phprpc_args}}} 同上。回應中的 {{{phprpc_result}}} 也同上,即:E(C(S(結果值))),E,C,S 含義同上。
當 {{{phprpc_encrypt}}} 為 3 時,回應中的 {{{phprpc_args}}} 和 {{{phprpc_result}}} 同上。回應中的 {{{phprpc_output}}} 先加密,再編碼,即:E(C(輸出資訊)),E,C 含義同上。
這裏的加密操作使用的是 [[XXTEA 加密演算法]]。
!!密鑰交換第一階段
但加密操作的前提是要有密鑰,因此當我們開始第一次加密調用之前,還需要有一個生成密鑰的過程。在 PHPRPC 協議中,密鑰生成是通過 [[Diffie-Hellman 密鑰交換演算法]]完成的。下面是 DH 密鑰交換演算法在 PHPRPC 協定中實現的過程:
當客戶端要發起一個加密調用時,如果還沒有生成密鑰,則首先向伺服器發起一個密鑰交換的請求,請求中最主要的參數也是 {{{phprpc_encrypt}}},它的值為 true。如果伺服器端支援加密傳輸,則返回用於密鑰交換的資訊:
{{{
phprpc_encrypt="<info>";
}}}
其中 <info> 為包含了 [[Diffie-Hellman 密鑰交換演算法]]中的 p、g、Ya 的三個元素的陣列,它們的下標分別為 p、g、y,其值為由十進位數字字組成的字串。該陣列也是通過序列化並編碼的方式表示的,編碼方式也由請求中的 {{{phprpc_encode}}} 的值所決定。
你還可以指定以下三個可選參數:
其中 {{{phprpc_encode}}} 在上面剛剛提到,這裏就不在重複。
{{{phprpc_callback}}} 參數同遠端程序呼叫時的作用一樣,也是為了讓 ~JavaScript 客戶端可以進行回調操作,所以伺服器端的對該參數的處理方式是相同的。
{{{phprpc_encode}}} 和 {{{phprpc_callback}}} 在密鑰交換第一階段和第二階段都會出現。
{{{phprpc_keylen}}} 參數僅在密鑰交換的第一階段出現,它表示密鑰交換的位數,默認為 128。例如:{{{phprpc_keylen=512}}} 表示客戶端要求與伺服器端進行 512 位的密鑰交換。如果伺服器支持客戶端所指定的位數,則進行密鑰交換時,除了在回應中返回用於密鑰交換的資料外,還要在回應中返回相同的[[密鑰長度]]參數,但格式按照回應的格式返回,例如:
{{{
phprpc_keylen="512";
}}}
如果伺服器端不支持客戶端所指定的位數,則選擇支持的最接近客戶端所指定的位數進行密鑰交換,並且在回應中給出實際的位數,例如:
{{{
phprpc_keylen="160";
}}}
如果伺服器端使用 128 位來進行密鑰交換,則回應中可以不給出密鑰交換的位數,客戶端也應該按照 128 位來進行處理。
!!密鑰交換第二階段
當客戶端收到第一階段伺服器的響應後,根據 p、g、y 及客戶端生成的亂數 Xb 來生成 Yb 和密鑰 k。Yb 的值(由十進位數字字組成的字串)直接作為密鑰交換第二階段請求的 {{{phprpc_encrypt}}} 參數值進行提交。
當伺服器接收到密鑰交換第二階段請求時,根據請求中 {{{phprpc_encrypt}}} 所包含的 Yb 和伺服器端保存的 Xa```Xa 通常保存在會話(session)當中,關於會話管理的內容將在下一章詳細描述。```來生成密鑰 k‘。如果密鑰交換成功,則客戶端的密鑰 k 將與伺服器端的密鑰 k' 完全相同。
密鑰交換成功的回應資訊中僅包含請求中 {{{phprpc_callback}}} 解碼後內容。如果密鑰交換失敗(例如伺服器端會話丟失),則返回 {{{phprpc_errno}}},{{{phprpc_errstr}}} 這兩項並在最後附加請求中 {{{phprpc_callback}}} 解碼後內容,其中 {{{phprpc_errstr}}} 的編碼取決於請求中 {{{phprpc_encode}}} 的值。
但目前已有的客戶端實現並不會理會這次請求後伺服器的應答資訊,即使伺服器端密鑰生成失敗。因為在緊接著發起的加密調用中,會因為這次的密鑰交換失敗而導致調用的失敗,客戶端實現將在那時再處理錯誤。
!!密鑰生成
前面的密鑰交換演算法生成的密鑰需要做一些轉化後才能生成最終用於 [[XXTEA 加密演算法]] 的密鑰,下面是用於加密解密的密鑰生成的規則:
如果密鑰交換採用的是 128 位(默認長度),則交換後的密鑰(長整數)按照小端位元組序(little endian byte order)轉化為位元組陣列,如果位元組陣列長度不足 16 個位元組,則在前面補 \0,直到長度等於 16 位元組。
如果密鑰交換採用其他長度,將交換後的密鑰首先轉化為 十進位數字字字串,然後對其進行 ~MD5 操作生成 16 位元組的位元組陣列。
通過上面的方法,就得到了用於 [[XXTEA 加密演算法]] 的密鑰。伺服器端和客戶端都採用上述方法來生成密鑰。之後就可以進行加密傳輸了。
PHPRPC 是一個的遠端程序呼叫協議。你可以認為它類似於 [[XMLRPC|http://www.xmlrpc.com/]],但是它們的工作方式有很大的不同。
PHPRPC 3.0 是使用 HTTP 協定作為傳輸協定的。以後的版本可能會提供更多傳輸協議的綁定(如 TCP、UDP 等),但 3.0 及其以前的版本只提供了 HTTP 傳輸綁定的工作方式。
以下所介紹的內容全部是針對 PHPRPC 3.0 協議的,所以不再單獨指明版本號。
PHPRPC 僅使用 HTTP 的 GET 和 POST 兩種請求方式,不對 HTTP 協定的其他請求方式(如 HEAD、PUT、DELETE 等)提供支援。
PHPRPC 推薦的請求方式是 POST,僅在無法使用 POST 進行請求操作時,才應該考慮使用 GET。因為 PHPRPC 請求不保證是冪等性(idempotency)操作,使用 GET 請求做非冪等性操作,會破壞 GET 的原始語義。儘管在 PHPRPC 中,HTTP 協定僅作為傳輸協定使用,它的原始語義並不是最重要的,但如果在能夠滿足它原始語義的條件下,最好還是不要破壞。
遠端程序呼叫中,參數和返回結果是通過 [[PHP 序列化]]形式表示的。傳輸編碼採用 Base64 或 ~JavaScript 字串方式編碼。密鑰交換採用 [[Diffie-Hellman 密鑰交換演算法]]。資料加密採用 [[XXTEA 加密演算法]]。
請求的格式採用標準的 application/x-www-form-urlencoded 格式,因為這種格式對於任何 HTTP 客戶端和伺服器來說都是容易構造和解析的,且可以保持 GET 和 POST 請求的構造和解析的一致性。
回應的格式採用純文本,所以伺服器可以設置回應的 ~Content-Type 為 text/plain,並設置明確的字元集。
回應格式類似於多行 ~JavaScript 賦值語句,例如:
{{{
phprpc_result="b:1;";
phprpc_errno="0";
phprpc_errstr="";
phprpc_output="";
}}}
唯一的例外是,如果請求中包含 phprpc_callback 參數,則回應的尾部會附加上 phprpc_callback 解碼後的值。但此參數僅在 ~JavaScript 客戶端中使用。
這種回應格式為 ~JavaScript 客戶端實現跨域調用提供了必要的基礎。
對於請求和響應的細節,我們在後面章節中再詳細說明。
PHPRPC 的加密傳輸是依賴會話保存密鑰的,因此會話對 PHPRPC 協議有著重要的作用。本章就來介紹 PHPRPC 協議中關於會話管理的內容。
!!依賴於 Cookie 的會話
PHPRPC 是以 HTTP 協定作為傳輸協定的,一般的 Web 伺服器都提供依賴於 Cookie 的會話機制,即利用 Cookie 來保存和傳遞會話標示(~SessionID)的會話機制。因此不論是 PHPRPC 伺服器還是客戶端只要按照 HTTP 協議來處理 Cookie 即可使用依賴於 Cookie 的會話。
!!Cookie-less 會話
但是對於某些特殊的客戶端來說,在某些情況下可能無法使用 Cookie。例如運行在流覽器中的 ~JavaScript 客戶端會受到流覽器的限制,如果流覽器禁用了 Cookie```許多手機上的流覽器默認就是禁用 Cookie 的```,則 ~JavaScript 客戶端就無法使用基於 Cookie 的會話了。在這種情況下,如果 PHPRPC 僅提供依賴於 Cookie 的會話機制,則無法滿足這種特殊情況下的需要。
因此為了使 PHPRPC 的流覽器客戶端在禁用 Cookie 的情況下仍然能夠進行密鑰交換和加密傳輸,PHPRPC 提供了對 Cookle-less 會話的支援。其實現過程是:
伺服器在密鑰交換第一階段,如果檢測到客戶端可能不支持基於 Cookie 的會話(例如可以檢測建立的是否是一個新的會話)時,在輸出密鑰交換參數({{{phprpc_encrypt}}})和密鑰長度({{{phprpc_keylen}}})選項後,再增加一項輸出 {{{phprpc_url}}},例如:
{{{
phprpc_url="http://localhost:8080/test/test.jsp;jsessionid=896CE4204292E70D28D27FE7C4503232";
}}}
其中 {{{phprpc_url}}} 的值是在原服務位址上增加了伺服器會話標示(~SessionID)的地址。會話標示的附加方式由伺服器端根據實際情況來決定,如果原客戶端位址本身帶有 url 請求參數,應該保留原有參數。例如,如果原伺服器地址為 {{{http://localhost:8080/test/test.jsp?serverid=2342&action=do}}},則回應中的 phprpc_url 應為如下格式:
{{{
phprpc_url="http://localhost:8080/test/test.jsp;jsessionid=896CE4204292E70D28D27FE7C4503232?serverid=2342&action=do";
}}}
伺服器僅應該過濾掉請求位址中以 {{{phprpc_}}} 開頭的請求參數。另外,該值的編碼方式與 {{{phprpc_errstr}}} 的編碼方式相同,即由請求中 {{{phprpc_encode}}} 的值來決定。該回應僅在密鑰交換第一階段返回。
客戶端在收到該響應後,應使用該位址替換原來的服務位址。之後所有的調用(包括密鑰交換第二階段)都使用新的位址,從而實現在 Cookie 被禁用的情況下,仍然可以使會話一直保持下去,保證密鑰交換和加密傳輸順利進行。
!!避免流覽器客戶端之間的會話共用造成的密鑰衝突
對於在流覽器中同一個頁面上或者不同頁面但在同一個會話中的多個客戶端(例如 ~JavaScript 客戶端、Flash 客戶端、~SilverLight 客戶端等)來說,如果使用加密傳輸,而沒有特殊方法處理的話,會產生密鑰衝突(即一個客戶端的密鑰會破壞掉另一個客戶端的密鑰)。為了避免這種情況的發生,故在請求中增加一個參數:{{{phprpc_id}}},該參數的值由客戶端生成,用於唯一標示該客戶端。伺服器端根據該值來區別同一會話中的多個客戶端。對於同一個客戶端來說該參數在所有的請求中都必須存在,並保持一致。但對於非共用會話的其他客戶端來說,請求中無需包含該參數。
該參數的值沒有統一的生成規則,但下面是推薦的生成規則:
*首先以客戶端類型開頭,例如:~JavaScript 客戶端可以以 js 開頭,~SilverLight 客戶端可以以 sl 開頭。
*之後是時間戳的數位字串表示。
*最後是隨機整數,亂數取值範圍越大越好,儘量保證不重複。
*三者之間用下劃線分割。
這樣基本上就可以保證標示的唯一性了。
伺服器通過該標示來存取密鑰交換資訊和最終密鑰。
你可以直接從本站的[[下載頁|http://www.phprpc.org/download]]中下載到每種語言最新版本的 PHPRPC,也可以通過本站[[主頁|http://www.phprpc.org]]上左面的 ''[Download Lastest Release]'' 按鈕直接下載所有語言的非測試版本的 PHPRPC。
目前所有版本的 PHPRPC 都是提供源代碼的,對於一些腳本語言來說,直接解壓縮之後就可以使用了,不需要什麼安裝步驟。另外一些需要編譯的語言,則提供了編譯用的 shell 檔(Linux/Unix 下使用)和 bat 檔(Windows 下使用),或者直接提供編譯好的二進位庫檔。
不過為了讓讀者能夠更清楚如何安裝,我們還是對每種語言的安裝都做詳細的講解,你可以在<<tag 安裝>>列表裏找到你感興趣的語言的安裝方法。
!!羽量級
PHPRPC 與 SOAP 不同,雖然 SOAP 號稱是簡單物件存取協定,但是實際上它一點都不簡單,甚至可以說,它的協定內容是一般人所無法理解的```如果您確認您真的能夠理解 SOAP 協定的所有內容,那麼您一定是這方面的專家教授或者是具有同樣能力的人了。```。除了複雜的定義,它還具有複雜且混亂的實現,且不要說在異構系統中部署應用,即使在同一種語言下部署,它也絕對算得上是重量級的```可能你會有不同意見,比如在 .NET 環境下,基於 SOAP 的 Web Service 應用開發部署是相當簡單的,我承認微軟在這點上做的非常出色,因為最初我也是被它所吸引來的。可是在其他語言中卻完全不是這麼回事了。```。而 PHPRPC 卻是羽量級的,它的協議相當簡單,從僅靠我一人之力就可以實現出十幾種語言的版本這個事實就可以很容易的看出來。它的資料表示也相當高效,不論在處理還是傳輸上都比 SOAP 要快得多。它在部署和使用時,更加簡單,它的 .NET 版本甚至比微軟為 .NET 提供的 ~WebService 都容易使用,你甚至不需要借助 Visual Studio 就可以輕鬆構建 PHPRPC for .NET 的應用。其他語言版本的部署和使用跟 .NET 版本差不多,甚至更加簡單。
!!安全
在安全方面,PHPRPC 並沒有像 SOAP 那樣專門制定一個 [[WS-Security|http://www.oasis-open.org/committees/tc_home.php?wg_abbrev=wss]] 來解決安全性問題。而是內置了加密傳輸機制,雖然這個加密機制沒有 ~WS-Security 那麼複雜```一個安全機制在實現或者應用時如果複雜到實現者或者應用者都無法完全理解的程度,那麼它很可能會被非安全的實現或應用!```,但絕對是從眾多的安全加密機制中精選出來的。它可以保證你的資料在非加密的 HTTP 網路```這裏是指非 HTTPS 的網路```上安全傳輸。
!!跨網際
PHPRPC 是工作於 HTTP 協定之上的遠端調用協定,因此它具有與 SOAP 同樣的好處——它可以自由的在 Internet 上被廣泛的部署應用。在這個互聯網應用佔據主導地位的年代,它比那些基於私有通訊協定的遠端調用在應用上更具優勢。
!!跨語言、跨平臺、跨環境
PHPRPC 目前支援十幾種編程語言,而且這些支援的編程語言都是目前被廣泛使用的主流編程語言。這些語言支援不同的平臺與環境,因此在跨語言的同時,也實現了跨平臺,跨環境。而且隨著 PHPRPC 的發展,你將發現 PHPRPC 會支援更多的語言,包括你所見過、使用過,以及從來沒有聽說過的語言。
!!跨域
PHPRPC 最大的一個亮點就是它支持在流覽器環境中通過 ~JavaScript 來調用伺服器端的函數與方法,它將你以前用傳統的 ajax 方式做不到或者很難做到的事情變得輕而易舉!而在這個亮點中更大的亮點是,它還支援在流覽器環境中直接跨域調用。而且在跨域方面,它還針對不同的應用作了不同的實現。這是 SOAP 所做不到的```至少目前它沒有做到```。
!!複雜物件傳輸
你可以在各種不同的語言之間通過 PHPRPC 自由的交換資料,不論是基本資料類型,還是複雜物件,它都可以幫你輕鬆傳輸。雖然 SOAP 也聲稱可以在各種語言之間交換複雜資料,可是實際應用卻完全不是那麼回事,在許多語言中你甚至不得不手工去構造那些晦澀難懂的 XML,來傳輸所謂的自定義類型,這種意義上的自定義類型傳輸甚至還不如字串傳輸更容易被理解。而在 PHPRPC 中你卻完全不需要做這種無意義且損傷腦細胞的事情,你會發現你將可以用前所未有的簡單方式在不同的語言之間傳輸複雜物件,簡單到就像在同一個程式中傳輸它們一樣。
!!引用參數傳遞
PHPRPC 還支持引用參數傳遞,你除了可以通過遠端函數或方法的返回值得到結果外,你還可以通過參數來返回資料,儘管大多數情況下你用不到這個特性,不過一旦需要,你會發現非常有用。
!!內容輸出重定向
除了通過返回值和參數來得到返回資料以外,你還可以通過輸出的方式來返回資料,而且通過該方式返回較大量的資料時,比通過返回值來返回資料還要高效。
!!分級錯誤處理
在遠端調用時,對於某些語言來說,並不是所有的錯誤都會影響結果,因為這些錯誤可能只是警告類資訊,在這種情況下,PHPRPC 在返回結果的同時,也會返回警告資訊。只有當發生致命錯誤時,才會只返回錯誤資訊。這種處理方式可以讓調用者得到更多想要的資訊。
!!會話支援
對於支援會話的伺服器,你可以在你的遠端函數或方法中使用會話,這將十分有效的幫助你對狀態進行管理。
到這裏,我們已經對 PHPRPC 的特點做了一個大體的介紹,現在你可能非常想要瞭解 PHPRPC 到底該怎麼用了。下面我們就進入[[快速入門]],來對它進行一個快速且實質性的瞭解吧。
如果不是因為頭腦發熱心血來潮,那麼一定是為了解決某些問題才有了 PHPRPC。好吧,我承認是因為我受夠了那些大企業所鼓吹的強大無比的 SOAP(~WebService)之後才開始考慮寫 PHPRPC 的。如果你也是一個需要類似於 SOAP 所鼓吹的能力,而實際上又被 SOAP 折磨的痛苦不堪卻又無所適從的人的話,或許 PHPRPC 就是你的最佳選擇。
!!序列化與編碼
遠端程序呼叫當中,我們需要傳遞的參數和得到的返回值可能是整數、浮點數、字串這樣的基本類型資料,也可能是陣列、字典或者自定義物件這樣的複合類型資料。要想將這些資料通過網路傳輸,就需要將它們序列化為一個文本或者位元組流。序列化的方式多種多樣,比如 XML 方式```XMLRPC、SOAP、Burlap 等都是採用 XML,不過它們所定義的 XML 格式是不同的。```,JSON 方式,二進位位元組流形式``` Java RMI、.NET Remoting、Hessian 等都是採用二進位位元組流,不過它們對二進位位元組流定義的格式也不同。```。
PHPRPC 採用的序列化方式是 [[PHP 序列化]]。你可以認為它是一種半文本格式,因為文本和二進位位元組流的特徵它都具有。比如表示數位時,它採用的是數位的文本格式,而不是二進位的機器存儲格式,這樣它就可以在不同的平臺上傳遞而無須擔心位元組序引起的解析問題。而它又像二進位位元組流那樣不允許在資料當中插入多餘的空白(空格、回車、換行等字元),並且對於字串、陣列、字典、物件還有長度描述資訊。這些特徵決定了它既有純文本格式的通用性,又具有二進位位元組流的快速解析能力。
PHPRPC 是以 HTTP 協定作為底層傳輸協定並且以 application/x-www-form-urlencoded 格式提交請求的,而傳輸的內容在序列化甚至再加密之後將會包含有很多需要 urlencode 的特殊字元,這樣編碼之後的長度將會增加 2 - 3 倍,並且還可能因為字元集的問題,造成在伺服器無法正確解析。因此,我們在進行 urlencode 之前,先對序列化或加密後的資料進行 Base64 編碼,然後再將 Base64 編碼之後的資料中的 {{{+}}} 替換為 {{{%2B}}} 即可完成 urlencode。經過這種變化後,長度僅增加 1/3,並且可以避免字元集引起的解析問題。
同樣為了避免因為字元集而可能引起的對序列化資料中二進位字元串和加密內容解析錯誤,對於響應中的需要序列化或加密的內容,我們也要做 Base64 編碼。
不過雖然請求中資料編碼必須使用 Base64 方式,但是作為回應內容,PHPRPC 除了提供 Base64 編碼方式以外,還提供了另外一種編碼方式,那就是 ~JavaScript 字串編碼方式。這種編碼類似於 C 語言的字元轉義表示,比如:{{{\r}}} 表示回車,{{{\0x12}}} 表示 ASCII 碼值為 18(十六進位為 12)的字元,{{{\012}}} 表示 ASCII 碼值為 10(八進制為 12)的字元。這種編碼格式對於 ~JavaScript 客戶端來說無需再另外解析。因此這種格式可以大大加快 ~JavaScript 客戶端處理回應的速度。由此可見,PHPRPC 協議本身已經針對 ~JavaScript 客戶端做了充分優化。
下面我們來具體說明一下在基本調用中的請求參數和回應內容。
!!請求參數
調用中的請求參數共有 7 個,其中只有 1 個是必須的,其他 6 個都是可選的。它們是:{{{phprpc_func}}},{{{phprpc_args}}},{{{phprpc_callback}}},{{{phprpc_encode}}},{{{phprpc_ref}}},{{{phprpc_encrypt}}} 和 {{{phprpc_id}}}。其中 {{{phprpc_encrypt}}} 跟加密傳輸有關,{{{phprpc_id}}} 跟會話管理有關,我們後面在相關章節再介紹它們。這裏我們只介紹前 5 個。
|!參數名稱|!取值|!預設值|!說明|
|{{{phprpc_func}}}| 字串 | 無 |該值為要調用的遠端函數名,函數名為明文字串,無大小寫區分(建議用小寫),函數名後不跟括弧。|
|{{{phprpc_args}}}| B(S(陣列)) | B(S(空陣列)) |表示所調用的遠端函數所需的參數,參數以序列化並 Base64 編碼的陣列格式表示,第一個參數的陣列下標為 0,第二個參數的陣列下標為 1,以此類推。|
|{{{phprpc_callback}}}| B(字串) | 空字串 |僅用於 ~JavaScript 客戶端,表示遠端程序呼叫後,客戶端執行的 ~JavaScript 語句。其值為經過 Base64 編碼的 ~JavaScript 語句。|
|{{{phprpc_encode}}}| 布林 | true |僅用於 ~JavaScript 客戶端,表示遠端程序呼叫後,回應內容是否進行 Base64 編碼。true 為 Base64 編碼,false 為 ~JavaScript 字串編碼。|
|{{{phprpc_ref}}}| 布林 | true |表示是否引用參數傳遞。設為 true 時,回應中包含遠端程序呼叫後的參數資訊,設為 false 時不包含。|
上表中的 {{{B()}}} 表示 Base64 編碼,{{{S()}}} 表示 [[PHP 序列化]]。布林值為 true 或 false 的字串形式。
{{{phprpc_func}}} 是必須的。可選項缺省時,伺服器端應按照它們的預設值處理。
!!回應內容
調用中的回應包含有五項,它們分別是:{{{phprpc_result}}},{{{phprpc_args}}},{{{phprpc_errno}}},{{{phprpc_errstr}}},{{{phprpc_output}}}。
|{{{phprpc_result}}}|遠端程序呼叫的返回結果,結果首先被序列化,之後以請求中 {{{phprpc_encode}}} 指定的方式來進行編碼。|
|{{{phprpc_args}}}|遠程程序呼叫的參數,參數在調用過程中可能會被修改,該回應用來返回修改後的參數值。回應中的參數與請求中的參數一樣,都是陣列,該陣列首先被序列化,之後以請求中 {{{phprpc_encode}}} 指定的方式來進行編碼。如果請求中 {{{phprpc_ref}}} 被設置為 false,則回應中不包含該項。|
|{{{phprpc_errno}}}|遠端程序呼叫中的錯誤碼,沒有錯誤時為 0。發生錯誤時為非 0,具體錯誤號跟伺服器端的語言相關,PHPRPC 協定中不做規定。因為錯誤號為純數位,因此不對其做序列化處理,也不進行編碼。|
|{{{phprpc_errstr}}}|遠端程序呼叫中的錯誤資訊,沒有錯誤時為空字串。發生錯誤時為錯誤資訊,具體錯誤資訊跟伺服器端的語言相關,PHPRPC 協定中不做規定。錯誤資訊不做序列化處理,但會以請求中 {{{phprpc_encode}}} 指定的方式來進行編碼。|
|{{{phprpc_output}}}|遠端程序呼叫中產生的輸出資訊,沒有輸出時為空字串。輸出資訊不做序列化處理,但會以請求中 {{{phprpc_encode}}} 指定的方式來進行編碼。|
當調用過程發生致命錯誤(非警告錯誤)時,回應中不包含 {{{phprpc_result}}} 和 {{{phprpc_args}}} 這兩項。
另外,如果請求中包含 {{{phprpc_callback}}} 參數,則回應的尾部會附加上 {{{phprpc_callback}}} 解碼後的值。
RawByteString 是在 Delphi 2009 中引入的類型,它與之前版本的 ~AnsiString 同義。在 Delphi 2009 中它的定義為:
<code delphi>
type
RawByteString = type AnsiString($FFFF);
</code>
為了與 Delphi 2009 程式相容,我們在 PHPRPC for Delphi 中為 Delphi 2009 之前的版本也引入了這一定義:
<code delphi>
type
{$IFNDEF DELPHI2009}
RawByteString = type AnsiString;
{$ENDIF}
</code>
這樣,就可以無差別的使用二進位字元串了。
<script>
var here=story.findContainingTiddler(place); if (!here) return;
if (here.ondblclick) {
here.setAttribute("editKey","none");
if ("$1"=="shift" || "$1"=="ctrl" || "$1"=="alt")
here.setAttribute("editKey","$1"+"Key");
var trigger=("$2"=="click")?"onclick":"ondblclick";
here.save_dblclick=here.ondblclick;
here.ondblclick=null;
if (here.getAttribute("editKey")!="none")
here[trigger]=function(e) {
var ev=e?e:window.event;
if (ev[this.getAttribute("editKey")])
this.save_dblclick.apply(this,arguments);
}
}
</script>
//{{{
var old_lewcid_splash_restart=restart;
restart = function()
{ if (document.getElementById("SplashScreen"))
document.getElementById("SplashScreen").style.display = "none";
if (document.getElementById("contentWrapper"))
document.getElementById("contentWrapper").style.display = "block";
old_lewcid_splash_restart();
}
//}}}
TArrayList 定義如下:
<code delphi>
TArrayList = class(TPHPObject)
private
FCount: Integer;
FCapacity: Integer;
FList: TVariantDynArray;
protected
function DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoProcedure(const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoSerialize(const Buffer: TStringBuffer = nil;
const ObjectContainer: TArrayList = nil): RawByteString; override;
procedure DoUnSerialize(const Buffer: TStringBuffer;
const ObjectContainer: TArrayList; StringAsByteArray: Boolean); override;
function Get(Index: Integer): Variant; virtual;
function GetList: TVariantDynArray; virtual;
procedure Grow; virtual;
procedure Put(Index: Integer; const Value: Variant); virtual;
procedure SetCapacity(NewCapacity: Integer); virtual;
procedure SetCount(NewCount: Integer); virtual;
procedure SetList(const Value: TVariantDynArray); virtual;
public
constructor Create; overload; override;
constructor Create(AOwner: TComponent); overload; override;
constructor Create(Capacity: Integer; AOwner: TComponent = nil); reintroduce; overload; virtual;
constructor Create(const ArrayList: TArrayList; AOwner: TComponent = nil); reintroduce; overload; virtual;
destructor Destroy; override;
class function New(Capacity: Integer): Variant; overload;
class function New(const ArrayList: TArrayList): Variant; overload;
function Add(const Value: Variant): Integer; virtual;
procedure AddAll(const ArrayList: TArrayList); overload; virtual;
procedure AddAll(const Container: Variant); overload; virtual;
procedure Clear; virtual;
function Contains(const Value: Variant): Boolean; virtual;
function Delete(Index: Integer): Variant; virtual;
procedure Exchange(Index1, Index2: Integer); virtual;
function IndexOf(const Value: Variant): Integer; virtual;
procedure Insert(Index: Integer; const Value: Variant); virtual;
procedure Move(CurIndex, NewIndex: Integer); virtual;
function Remove(const Value: Variant): Integer; virtual;
property Items[Index: Integer]: Variant read Get write Put; default;
published
property Count: Integer read FCount write SetCount;
property Capacity: Integer read FCapacity write SetCapacity;
property List: TVariantDynArray read GetList write SetList;
end;
</code>
THashMap 定義如下:
<code delphi>
THashMap = class(TPHPObject)
private
FRefCount: Integer;
FKeys: TArrayList;
FValues: TArrayList;
function GetCount: Integer;
protected
function DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoProcedure(const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoSerialize(const Buffer: TStringBuffer = nil;
const ObjectContainer: TArrayList = nil): RawByteString; override;
procedure DoUnSerialize(const Buffer: TStringBuffer;
const ObjectContainer: TArrayList; StringAsByteArray: Boolean); override;
function Get(const Key: Variant): Variant; virtual;
procedure Put(const Key, Value: Variant); virtual;
public
procedure AfterConstruction; override;
constructor Create; overload; override;
constructor Create(AOwner: TComponent); overload; override;
constructor Create(Capacity: Integer; AOwner: TComponent = nil); reintroduce; overload; virtual;
constructor Create(const ArrayList: TArrayList; AOwner: TComponent = nil); reintroduce; overload; virtual;
constructor Create(const HashMap: THashMap; AOwner: TComponent = nil); reintroduce; overload; virtual;
constructor Create(const Container: Variant; AOwner: TComponent = nil); reintroduce; overload; virtual;
destructor Destroy; override;
procedure Clear; virtual;
function ContainsKey(const Key: Variant): Boolean; virtual;
function ContainsValue(const Value: Variant): Boolean; virtual;
class function New(Capacity: Integer): Variant; overload;
class function New(const ArrayList: TArrayList): Variant; overload;
class function New(const HashMap: THashMap): Variant; overload;
class function New(const Container: Variant): Variant; overload;
procedure PutAll(const ArrayList: TArrayList); overload; virtual;
procedure PutAll(const HashMap: THashMap); overload; virtual;
procedure PutAll(const Container: Variant); overload; virtual;
function Delete(const Key: Variant): Variant; virtual;
function ToArrayList: TArrayList; virtual;
property Items[const Key: Variant]: Variant read Get write Put; default;
published
property Count: Integer read GetCount;
property Keys: TArrayList read FKeys;
property Values: TArrayList read FValues;
end;
</code>
THashedArrayList 定義如下:
<code delphi>
THashedArrayList = class(TArrayList)
private
FHashBucket: THashBucket;
function IndexCompare(Index: Integer; const Value: Variant): Boolean;
protected
procedure Put(Index: Integer; const Value: Variant); override;
procedure SetList(const Value: TVariantDynArray); override;
public
constructor Create(Capacity: Integer; AOwner: TComponent = nil); overload; override;
constructor Create(const ArrayList: TArrayList; AOwner: TComponent = nil); overload; override;
destructor Destroy; override;
function Add(const Value: Variant): Integer; override;
procedure Clear; override;
function Delete(Index: Integer): Variant; override;
procedure Exchange(Index1, Index2: Integer); override;
function IndexOf(const Value: Variant): Integer; override;
procedure Insert(Index: Integer; const Value: Variant); override;
end;
</code>
TPHPObject 定義如下:
<code delphi>
{$M+}
TPHPObject = class(TComponent)
private
function GetProp(const Name: String): Variant;
procedure SetProp(const Name: String; const Value: Variant);
protected
function DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; virtual;
function DoProcedure(const Name: string;
const Arguments: TVarDataArray): Boolean; virtual;
function DoSerialize(const Buffer: TStringBuffer = nil;
const ObjectContainer: TArrayList = nil): RawByteString; virtual;
procedure DoUnSerialize(const Buffer: TStringBuffer;
const ObjectContainer: TArrayList; StringAsByteArray: Boolean); virtual;
function GetProperty(var Dest: TVarData;
const Name: string): Boolean; virtual;
function SetProperty(const Name: string;
const Value: TVarData): Boolean; virtual;
function __sleep: TStringDynArray; virtual;
procedure __wakeup; virtual;
function ToBoolean: Boolean; virtual;
function ToDate: TDateTime; virtual;
function ToDouble: Double; virtual;
function ToInt64: Int64; virtual;
function ToInteger: Integer; virtual;
public
function Equal(const Right: TPHPObject): Boolean; overload; virtual;
class function Equal(const Left, Right: Variant): Boolean; overload;
constructor Create; reintroduce; overload; virtual;
class function New: Variant;
class function FromVariant(const V: Variant): TPHPObject;
class function AliasName: string;
class function GetClass(const AliasName: string): TPHPClass;
class procedure RegisterClass(const AliasName: string = '');
procedure MoveComponentsTo(AComponent: TComponent);
function HashCode: Integer; virtual;
function ToString: string; {$IFDEF DELPHI2009}override{$ELSE}virtual{$ENDIF};
function ToVariant: Variant; virtual;
property Properties[const Name: string]: Variant read GetProp write SetProp; default;
end;
{$M-}
</code>
TPHPRPC_Client 的定義如下:
<code delphi>
TPHPRPC_Client = class(TPHPObject)
private
FIdHTTP: TIdHTTP;
FURL: string;
FKey: RawByteString;
FKeyLength: Integer;
FEncryptMode: Integer;
FCharset: string;
FOutput: RawByteString;
FWarning: TPHPRPC_Error;
FVersion: Currency;
FStringAsByteArray: Boolean;
procedure SetKeyLength(Value: Integer);
procedure SetEncryptMode(Value: Integer);
procedure SetCharset(const Value: string);
function GetProxy: TIdProxyConnectionInfo;
procedure SetProxy(const Value: TIdProxyConnectionInfo);
function GetTimeout: Integer;
procedure SetTimeout(const Value: Integer);
procedure SetURL(const Value: string);
procedure KeyExchange;
function Post(const ReqStr: RawByteString): THashMap;
function Decrypt(const Data: RawByteString; Level: Integer): RawByteString;
function Encrypt(const Data: RawByteString; Level: Integer): RawByteString;
protected
function DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; override;
public
constructor Create(); overload; override;
constructor Create(AOwner: TComponent); overload; override;
constructor Create(const AURL: string; AOwner: TComponent = nil); reintroduce; overload;
destructor Destroy; override;
function UseService(const AURL: string): Variant; overload;
function Invoke(const FuncName: string; const Args: TVariantDynArray; byRef: boolean = False): Variant;
property Output: RawByteString read FOutput;
property Warning: TPHPRPC_Error read FWarning;
published
property KeyLength: Integer read FKeyLength write SetKeyLength default 128;
property EncryptMode: Integer read FEncryptMode write SetEncryptMode default 0;
property Charset: string read FCharset write SetCharset;
property Proxy: TIdProxyConnectionInfo read GetProxy write SetProxy;
property Timeout: Integer read GetTimeout write SetTimeout;
property URL: string read FURL write SetURL;
property StringAsByteArray: Boolean read FStringAsByteArray write FStringAsByteArray default False;
end;
</code>
TStringBuffer 定義如下:
<code delphi>
TStringBuffer = class(TPHPObject)
private
FDataString: RawByteString;
FPosition: Integer;
FCapacity: Integer;
FLength: Integer;
procedure Grow;
procedure SetPosition(NewPosition: Integer);
procedure SetCapacity(NewCapacity: Integer);
protected
function DoFunction(var Dest: TVarData; const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoProcedure(const Name: string;
const Arguments: TVarDataArray): Boolean; override;
function DoSerialize(const Buffer: TStringBuffer = nil;
const ObjectContainer: TArrayList = nil): RawByteString; override;
procedure DoUnSerialize(const Buffer: TStringBuffer;
const ObjectContainer: TArrayList; StringAsByteArray: Boolean); override;
public
constructor Create; overload; override;
constructor Create(Capacity: Integer); reintroduce; overload;
constructor Create(const AString: RawByteString); reintroduce; overload;
class function New(Capacity: Integer): Variant; overload;
class function New(const AString: RawByteString): Variant; overload;
function Read(var Buffer; Count: Longint): Longint;
function ReadString(Count: Longint): RawByteString;
function Write(const Buffer; Count: Longint): Longint;
procedure WriteString(const AString: RawByteString);
function Insert(const Buffer; Count: Longint): Longint;
procedure InsertString(const AString: RawByteString);
function Seek(Offset: Longint; Origin: Word): Longint;
function ToString: string; override;
{$IFDEF DELPHI2009}
function ToRawByteString: RawByteString;
{$ENDIF}
published
property Position: Integer read FPosition write SetPosition;
property Length: Integer read FLength;
property Capacity: Integer read FCapacity write SetCapacity;
property DataString: RawByteString read FDataString;
end;
</code>
TVariantDynArray 定義如下:
<code delphi>
type
TVariantDynArray = array of Variant;
</code>
XXTEA 加密演算法是微型加密演算法(TEA)目前最為安全的一個變種。
TEA 及其相關變種(XTEA,Block TEA,XXTEA) 都是分組加密演算法,它們很容易被描述,實現也很簡單(典型的幾行代碼)。
TEA 演算法最初是由劍橋電腦實驗室的 David Wheeler 和 Roger Needham 在 1994 年設計的。該演算法使用 128 位元的密鑰為 64 位元的資訊塊進行加密,它需要進行 64 輪迭代,儘管作者認為 32 輪已經足夠了。該演算法使用了一個神秘常數(Magic Number)δ 作為倍數,它來源於黃金比率,以保證每一輪加密都不相同。但 δ 的精確值似乎並不重要,TEA 把它定義為 <html>δ=「(√<span style="text-decoration: overline;">5</span> - 1)2<sup>31</sup>」</html>(具體數值為 0x9E3779B9)。
之後 TEA 演算法被發現存在缺陷,作為回應,設計者提出了一個 TEA 的升級版本——XTEA(有時也被稱為“tean”)。XTEA 跟 TEA 使用了相同的簡單運算,但它採用了截然不同的順序,為了阻止密鑰表攻擊,四個子密鑰(在加密過程中,原 128 位的密鑰被拆分為 4 個 32 位的子密鑰)採用了一種非正規的方式進行混合,但速度較之 XTEA 略慢。
在與描述 XTEA 演算法的同一份報告中,還介紹了另外一種被稱為 Block TEA 演算法的變種,它可以對 32 位元大小任意倍數的變數塊進行操作。該演算法將 XTEA 輪循函數依次應用於塊中的每個字,並且將它附加於它的鄰字。該操作重複多少輪依賴於塊的大小,但至少需要 6 輪。該方法的優勢在於它無需操作模式(CBC,OFB,CFB 等),密鑰可直接用於資訊。對於長的資訊它可能比 XTEA 更有效率。
在 1998 年,~Markku-Juhani Saarinen 給出了一個可有效攻擊 Block TEA 演算法的代碼,但之後很快 David J. Wheeler 和 Roger M. Needham 就給出了 Block TEA 演算法的修訂版,這個演算法被稱為 [[XXTEA|http://www.cix.co.uk/~klockstone/xxtea.pdf]]。XXTEA 使用跟 Block TEA 相似的結構,但在處理塊中每個字時利用了相鄰字。它利用一個更複雜的 MX 函數代替了 XTEA 輪循函數,MX 使用了 2 個輸入量。
XXTEA 演算法的具體描述可以參見上面鏈結的文檔,也可以參見 PHPRPC 已有實現中的代碼,這裏不再單獨給出。
但是 XXTEA 演算法只定義了如何對 32 位元的資訊塊陣列(實際上是 32 位元無符號整數陣列)進行加密,而並沒有定義二進位位元組流的加密方法,因此我們需要定義一個將二進位位元組流安全轉換為 XXTEA 演算法支援的 32 位元無符號整數陣列的方法。這裏所說的安全是指能夠完整的相互轉換,而不僅僅是單向轉換。
XXTEA 演算法中,要加密的資料的 32 位元無符號整數陣列至少要有 2 個元素,作為密鑰的 32 位元無符號整形陣列為 4 個元素。因此,我們將要加密的二進位位元組流轉為 32 位元無符號整數陣列時,首先記錄下原二進位位元組流的長度 L,該長度用一個 32 位元無符號整型數表示。之後通過在二進位位元組流尾部添加 \0(ASCII 碼值為 0 的位元組)的方式將長度補齊到 4 的倍數,接下來按照小端位元組序(little endian byte order)方式轉換為無符號整數陣列,並保證陣列的長度 La 與充填後二進位位元組流的長度 L' 滿足以下關係:
{{{
La = (L' / 4) + 1
}}}
最後,將原二進位位元組流的長度值 L,充填到陣列的最後一個元素中(按照上面的方法生成的陣列,最後正好會保留一個用於充填該值的元素)。
通過這種方式,只要二進位位元組流不為空,則至少會生成 2 個元素的 32 位元無符號整數陣列。而如果為空,則不進行上述操作,直接退出,也就是不對空資料進行加密。
因為資料中包含了長度資訊,因此解密後,可以根據長度資訊還原為原始長度的二進位位元組流。並且還可以根據解密後最後一個元素的資訊來大致判斷是否解密成功。
因為密鑰不涉及到解密還原的問題,因此對密鑰的轉換我們採用直接補齊的到 16 個位元組,然後按照小端位元組序(little endian byte order)方式轉換為無符號整數陣列,即可得到含有 4 個元素的 32 位元無符號整數陣列。
在實現 XXTEA 演算法時,密鑰也是採用在二進位位元組流''尾部''添加 \0 的方式補齊。但是要注意的是在 PHPRPC 中,調用 XXTEA 演算法前,必須首先將密鑰通過''開頭''補 \0 的方式充填到 16 個位元組。因此,在 XXTEA 演算法中密鑰''尾部''補 \0 對 PHPRPC 來說沒有作用,但在實現 XXTEA 演算法時,推薦仍然按照''尾部''補 \0 方式實現,以便於在非 PHPRPC 應用中使用 XXTEA 演算法加密資料時也可以正常交互。
//{{{
config.footnotesPlugin = {
backLabel: "back",
prompt:"show footnote"
};
config.formatters.unshift( {
name: "footnotes",
match: "```",
lookaheadRegExp: /```((?:.|\n)*?)```/g,
handler: function(w)
{
this.lookaheadRegExp.lastIndex = w.matchStart;
var lookaheadMatch = this.lookaheadRegExp.exec(w.source);
if (lookaheadMatch && lookaheadMatch.index == w.matchStart) {
var tiddler = story.findContainingTiddler(w.output);
if (tiddler) {
if (!tiddler.notes)
tiddler.notes = [];
var title = tiddler.getAttribute("tiddler");
tiddler.notes.pushUnique(lookaheadMatch[1]);
var pos = tiddler.notes.indexOf(lookaheadMatch[1]) + 1;
createTiddlyButton(w.output,pos,config.footnotesPlugin.prompt,function(){var x = document.getElementById(title+"ftn"+pos);window.scrollTo(0,ensureVisible(x)+(ensureVisible(x)<findScrollY()?(findWindowHeight()-x.offsetHeight):0));return false;},"ftnlink",title+"ftnlink"+pos);
w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
}
}
}
});
old_footnotes_refreshTiddler = Story.prototype.refreshTiddler;
Story.prototype.refreshTiddler = function(title,template,force)
{
var tiddler = old_footnotes_refreshTiddler.apply(this,arguments);
if (tiddler && tiddler.notes && tiddler.notes.length)
{
var holder = createTiddlyElement(null,"div",null,"footnoteholder");
var list = createTiddlyElement(holder,"ol",title+"footnoteholder");
for (var i=0; i<tiddler.notes.length; i++)
{
var ftn = createTiddlyElement(list,"li",title+"ftn"+(i+1),"footnote");
wikify(tiddler.notes[i]+" ",ftn);
createTiddlyButton(ftn,"["+config.footnotesPlugin.backLabel+"]",config.footnotesPlugin.backLabel,function(){window.scrollTo(0,ensureVisible(document.getElementById(this.parentNode.id.replace("ftn","ftnlink"))));return false;},"ftnbklink");
}
var count = tiddler.childNodes.length;
for (var j=0; j<count; j++){
if(hasClass(tiddler.childNodes[j],"viewer")){
var viewer = tiddler.childNodes[j];
}
}
viewer.appendChild(holder);
tiddler.notes = [];
}
return tiddler;
};
setStylesheet(
".tiddler a.ftnlink {vertical-align: super; font-size: 0.8em; color:red;}\n"+
".tiddler a.ftnlink:hover, .tiddler .footnoteholder a.ftnbklink:hover{color:#fff;background:red;}\n"+
".tiddler div.footnoteholder{margin:1.8em 1.0em; padding:0.1em 1.0em 0.1em 1.0em ;border-left: 1px solid #ccc;}"+
".tiddler footnoteholder ol {font-size: 0.9em; line-height: 1.2em;}\n"+
".tiddler .footnoteholder li.footnote {margin: 0 0 5px 0;}\n"+
".tiddler .footnoteholder a.ftnbklink{color:red;}\n","FootNotesStyles");
//}}}
PHPRPC 將調用加密分為四種模式(或者叫做級別也可以),它們分別用 0 - 3 這四個整型數字表示。
0 表示不加密傳輸。
1 表示調用的參數加密傳輸(不管是傳到伺服器端還是從伺服器端傳回,都是加密的)。
2 表示調用的參數和伺服器端方法返回的結果加密傳輸。
3 表示調用的參數、伺服器端方法返回的結果以及伺服器端的輸出重定向內容都加密傳輸。
字串類型序列化方式分為三種:
# 非轉義二進位字元串序列化
# 轉義二進位字元串序列化
# Unicode 字串序列化
其中第一種是 PHP 4、PHP 5 提供的序列化方式,也是最好的序列化方式,因為它可以非常快速的構造和解析,佔用的空間也最少。後兩種是 PHP 6 中將要引進的序列化方式,其中轉義二進位字元串序列化是用來取代 PHP 4、PHP 5 非轉義二進位字元串序列化的。但該序列化方式不管從構造還是解析方面都沒有第一種方式高效,而且佔用空間也更大。不過 PHP 6 仍然支援對第一種方式的反序列化。Unicode 序列化也是在 PHP 6 中將要引入的序列化方式,因為在 PHP 6 之前字串都是以二進位字元串存儲的,而 PHP 6 中將提供 Unicode 字串類型,因此序列化方式也提供了一種新的 Unicode 序列化形式,但是該形式與 PHP 4、PHP 5 均不相容。因此對於其他語言來說,只需要實現第一種方式的序列化即可,對後兩種方式只要提供反序列化支援,就可以跟 PHP 6 相容。不推薦在其他語言中實現後兩種方式的序列化,因為它們與 PHP 4 和 PHP 5 是不相容的,並且效率也更低。
!!非轉義二進位字元串序列化
非轉義二進位字元串序列化形式為:
{{{
s:<length>:"<value>";
}}}
其中 <length> 是 <value> 的長度,<length> 是非負整數。<value> 為字串值,這裏的每個字元都是單字節字元,其範圍與 ASCII 碼的 0 - 255 的字元相對應。每個字元都表示原字元含義,沒有轉義字元,<value> 兩邊的引號("")是必須的,但不計算在 <length> 當中。這裏的 <value> 相當於一個位元組流,而 <length> 是這個位元組流的位元組個數。
它與各語言的對應關係如下表:
| 語言 | 序列化 | 反序列化 |
| PHP | string(PHP 4 & PHP 5) | string |
| Java | byte[]/char/char[]/String/~StringBuffer | byte[]/String |
| C# | byte[]/char/char[]/string/~StringBuilder | byte[]/string |
| VB(.NET) | Byte()/Char/Char()/String/~StringBuilder | Byte()/String |
| ~JavaScript | String | String |
| ~ActionScript 2 | ~ByteArray/String | ~ByteArray/String |
| ~ActionScript 3 | ~ByteArray/String | ~ByteArray/String |
| Delphi | ~TByteDynArray/~RawByteString/~AnsiString | ~RawByteString |
| Ruby | String/Symbol | String |
| Python | String | String |
上表中有些語言的反序列化有兩種對應類型,那是因為在這些語言中,如果要反序列化的是物件屬性名或者陣列鍵名的話,就反序列化為字串類型,否則就反序列化為位元組陣列。這些語言中都已經提供了將位元組陣列轉換為字串、字元陣列等類型的工具(有些是 PHPRPC 提供的),所以你可以根據需要將反序列化的值轉換為你希望的類型。
!!轉義二進位字元串序列化
轉義二進位字元串序列化形式為:
{{{
S:<length>:"<value>";
}}}
其中 <length> 是原始字串的長度,而非 <value> 的長度。<length> 是非負整數。<value> 為經過轉義之後的字串。
轉義規則:對於 ASCII 碼小於 128 的字元(但不包括 \),按照單個位元組寫入(與非轉義二進位字元串序列化相同),對於 128~255 的字元和 \ 字元,則將其 ASCII 碼值轉化為 16 進制編碼的字串,以 \ 作為開頭,後面兩個位元組分別是這個字元的 16 進制編碼,順序按照由高位到低位元排列,也就是第 8-5 位所對應的16進制數位字元(abcdef 這幾個字母是小寫)作為第一個位元組,第 4-1 位元作為第二個位元組。依次編碼下來,得到的就是 <value>。
目前,只有 PHP 6 支援該方式序列化,其他語言只提供反序列化支援。
| 語言 | 序列化 | 反序列化 |
| PHP 4/ PHP 5 | 無 | 無 |
| PHP 6 | string | string |
| Java | 無 | byte[]/String |
| C# | 無 | byte[]/string |
| VB(.NET) | 無 | Byte()/String |
| ~JavaScript | 無 | String |
| ~ActionScript 2 | 無 | ~ByteArray/String |
| ~ActionScript 3 | 無 | ~ByteArray/String |
| Delphi | 無 | ~RawByteString |
| Ruby | 無 | String |
| Python | 無 | String |
!!Unicode 字串序列化
Unicode 字串序列化形式為:
{{{
U:<length>:"<value>";
}}}
其中 <length> 是指原 Unicode 字串的長度,而不是 <value> 的長度。<length> 是非負整數。<value> 為經過轉義之後的字串。
儘管 <length> 是原 Unicode 字串的長度,但不是指它的位元組數,也不完全是指它的字元數,確切的說是指它的字元單位數。因為 Unicode 字串中採用的是 ~UTF16 編碼,這種編碼方式使用 16 位元來表示一個字元的,但是並不是所有的字元都可以用 16 位元表示,因此有些字元需要兩個 16 位來表示。在 ~UTF16 編碼中,16 位元字元算作一個字元單位,一個實際的字元可能是一個字元單位,也有可能由兩個字元單位組成。因此, Unicode 字串中字元數並不總是等於字元單位數,而這裏的 <length> 指的就是字元單位數,而不是字元數。
轉義規則:對於 Unicode 編碼小於 128 的字元(但不包括 \),按照單個位元組寫入,對於大於 128 的字元和 \ 字元,則轉化為 16 進制編碼的字串,以 \ 作為開頭,後面四個位元組分別是這個字元單位的 16 進制編碼,順序按照由高位到低位元排列,也就是第 16-13 位所對應的16進制數位字元(abcdef 這幾個字母是小寫)作為第一個位元組,第 12-9 位元作為第二個位元組,第 8-5 位元作為第三個位元組,最後的第 4-1 位元作為第四個位元組。依次編碼下來,得到的就是 <value>。
目前,除了 PHP 6 支援該方式序列化,只有極少數其他語言提供這種方式的序列化支援,其他語言只提供反序列化支援。
| 語言 | 序列化 | 反序列化 |
| PHP 4/ PHP 5 | 無 | 無 |
| PHP 6 | ~UnicodeString | ~UnicodeString |
| Java | 無 | String |
| C# | 無 | string |
| VB(.NET) | 無 | String |
| ~JavaScript | 無 | String |
| ~ActionScript 2 | 無 | String |
| ~ActionScript 3 | 無 | String |
| Delphi | ~WideString | ~WideString |
| Ruby | 無 | String |
| Python | Unicode | Unicode |
容器序列化形式為:
{{{
a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}
}}}
其中 <n> 表示容器元素的個數,<key 1>、<key 2>……<key n> 表示容器鍵名,<value 1>、<value 2>……<value n> 表示與鍵名相對應的元素的值。
鍵名可以是整型或字串型,序列化後的格式跟整型和字串型資料序列化後的格式相同。
元素值可以是任意類型,其序列化後的格式與其所對應的類型序列化後的格式相同。
它與各語言的對應關係如下表:
| 語言 | 序列化 | 反序列化 |
| PHP | array | array |
| Java | ~AssocArray/Array/List/Collection/Map | ~AssocArray |
| C# | ~AssocArray/Array/~IList/~IDictionary/~ICollection | ~AssocArray |
| VB(.NET) | ~AssocArray/Array/~IList/~IDictionary/~ICollection | ~AssocArray |
| ~JavaScript | Array/Object | Array |
| ~ActionScript 2 | Array/Object | Array |
| ~ActionScript 3 | Array/Object | Array |
| Delphi | 動態陣列/~TArrayList/~THashedArrayList/~THashMap | ~THashMap |
| Ruby | Array/Hash | Hash |
| Python | List/Tuple/Dict | Dict |
以上各語言的 PHPRPC 實現中都提供了在這些容器類型之間相互轉換的方法,因此可以很方便的將反序列化後的資料轉換為合適的容器類型。
PHPRPC 加密傳輸的密鑰是通過 [[Diffie-Hellman 密鑰交換演算法]]來在客戶端和伺服器端同時生成的,生成密鑰需要一些參數,這些參數在 PHPRPC 中是由伺服器端提供的,密鑰的長度範圍也是由這些參數的長度來決定的,因此這裏的密鑰長度實際上是指伺服器端所提供的生成密鑰的參數的長度。
在 PHPRPC 中,伺服器端提供有下面幾個長度的密鑰參數:
>96 128 160 192 256 512 768 1024 1536 2048 3072 4096
密鑰長度是通過客戶端來指定,然後通過與伺服器協商後確定。因為太長的密鑰在生成時花費的時間也更長,所以伺服器不一定對所有這些長度的密鑰參數都支持。對於計算能力比較弱的語言來說,支援的長度可能就較短。
協商的規則很簡單,就是伺服器在它支持的長度中尋找最接近客戶端指定的長度的值作為最後的長度值。例如,客戶端指定長度為 1000,如果伺服器端最大支持的是 512,那麼最後的長度就是 512,如果伺服器端支持的最大長度是2048,並且其中包括 1024 的話,那麼最後的長度就是 1024。
!!物件引用和指標引用的形式
先介紹一下這兩種引用序列化的形式:
物件引用序列化形式為:
{{{
r:<number>;
}}}
指針引用序列化形式為:
{{{
R:<number>;
}}}
!!什麼是物件引用和指標引用呢?
這裏以 PHP 為例來用程式說明這一點:
<code php>
<?php
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
?>
</code>
該程式的命令行執行結果為:
{{{
O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}
}}}
這裏變數 $a 的 value 欄位的值被序列化成了 r:1,而 $b 的 value 欄位的值被序列化成了 R:1。
也就是說,$a->value 是 $a 的對象引用,而 $b->value 是 $b 的指針引用。
!!物件引用和指標引用有什麼區別呢?
那繼續看下面這個例子:
<code php>
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
</code>
該程式的命令行執行結果為:
{{{
object(SampleClass)#1 (1) {
["value"]=>
int(1)
}
int(1)
}}}
改變 $a->value 的值僅僅是改變了 $a->value 的值,而改變 $b->value 的值卻改變了 $b 本身,這就是物件引用和指標引用的區別。
但是在 PHP 中,陣列並不是物件,因此陣列不會作為物件引用來序列化,例如:
<code php>
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo $a["value"]["value"][1];
echo "\n";
$a = unserialize(serialize($a));
echo $a["value"]["value"][1];
</code>
該程式的命令行執行結果為:
{{{
1
}}}
大家會發現,將原陣列序列化再反序列化後,陣列結構變了。原本 $a["value"]["value"][1] 中的值 1,但在反序列化之後丟失了。
原因是什麼呢?讓我們輸出序列化之後的結果來看一看:
<code php>
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo serialize($a);
</code>
該程式的命令行執行結果為:
{{{
a:2:{i:1;i:1;s:5:"value";a:2:{i:1;i:1;s:5:"value";N;}}
}}}
原來,序列化之後,$a["value"]["value"] 變成了 NULL,而不是一個物件引用。
也就是說,PHP 只對物件在序列化時才會生成物件引用。對所有的標量類型和陣列(也包括 NULL)序列化時都不會生成物件引用。但是如果明確使用了 & 運算符,在序列化時,會被序列化為指針引用。
因此,我們在其他語言中,對帶有引用的容器類型採用指標引用方式序列化,對帶有引用的物件類型採用物件引用方式序列化,這樣就可以保證可以在所有語言中正確傳遞引用結構了。
!!引用序列化中的數字是什麼?
大家一定很奇怪引用序列化形式中,{{{r}}} 和 {{{R}}} 後面這個 <number> 是什麼吧?下面我們就來詳細介紹一下。
<number> 簡單的說,就是所引用的物件在序列化串中第一次出現的位置。但是這個位置不是指字元的位置,而是指物件(這裏的物件是泛指所有類型的量,而不僅限於物件類型)的位置。
下面來舉例說明一下:
<code php>
class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}
$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;
echo serialize($a);
</code>
該程式的命令行執行結果為:
{{{
O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}
}}}
在這個例子中,首先序列化的物件是 {{{ClassA}}} 的一個物件,那麼給它編號為 1,接下來要序列化的是這個物件的幾個成員,第一個被序列化的成員是 {{{int}}} 欄位,那它的編號就為 2,接下來被序列化的成員是 {{{str}}},那它的編號就是 3,依此類推,到了 {{{obj}}} 成員時,它發現該成員已經被序列化了,並且編號為 1,因此它被序列化時,就被序列化成了 {{{r:1;}}} ,在接下來被序列化的是 {{{pr}}} 成員,它發現該成員實際上是指向 {{{str}}} 成員的一個引用,而 {{{str}}} 成員的編號為 3,因此,{{{pr}}} 就被序列化為 {{{R:3;}}} 了。
PHP 序列化是如何來編號被序列化的資料的呢?在 PHP 序列化時,首先建立一個空表,然後每個被序列化的資料在被序列化之前,先判斷該資料是否在表中出現過,如果出現過則返回第一次出現的位置,否則添加該資料的引用到這個表的最後。如果返回了位置,則根據是否是指標引用類型來進行相應的引用序列化操作,而引用序列化中那個數字就是這個位置,最後如果不是指標引用,還需要將資料也添加到這個表的最後。
上面的過程只是來說明這個數字是如何生成的,實際在程式實現時,可以根據每種語言的特徵進行具體的優化。
!!物件引用的反序列化
雖然 PHP 只對物件類型的資料做物件引用的序列化,但是反序列化的字串如果不是 PHP 生成的,而是人為構造或者用其他語言生成的,即使物件引用指向的不是一個物件,它也能正確地按照物件引用所指向的資料進行反序列化。例如:
<code php>
class StrClass {
var $a;
var $b;
}
$a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}');
var_dump($a);
</code>
該程式的命令行執行結果為:
{{{
object(StrClass)#1 (2) {
["a"]=>
string(5) "Hello"
["b"]=>
string(5) "Hello"
}
}}}
大家會發現,上面的例子反序列化後,$a->b 的值與 $a->a 的值是一樣的,儘管 $a->a 不是一個物件,而是一個字串。因此如果用其他語言來實現 PHP 序列化的話,沒有必要把字串作為標量類型來處理,即使按照物件引用來序列化擁有相同字串內容的資料,用 PHP 同樣可以正確的反序列化。這樣可以更節省序列化後的內容所佔用的空間。
在本章裏,我們會首先介紹如何下載與安裝 PHPRPC,之後我們會以 PHP、Java 和 ~JavaScript 三種語言來給出幾個簡單的示例,讓你對 PHPRPC 有一個快速的認識。如果你所使用的語言並不是這三種語言,也沒有關係,在你需要的語言章節中同樣也會看到類似的簡單示例。
!下載
<<tiddler [[PHPRPC 的下載]]>>
!安裝
<<tiddler [[PHPRPC 的安裝]]>>
!示例
<<tiddler [[HelloWorld]]>>
PHP 序列化支援的數值類型有 32 位元有符號整型數和雙精度浮點數,另外,為了方便講解,布林類型在這裏我們也將它算為數值類型。
!! 布林類型
布林類型序列化形式為:
{{{
b:<digit>;
}}}
其中 <digit> 為 0 或 1,當布林值為 false 時,<digit> 為 0,否則為 1。
它與各語言之間的對應表如下:
| 語言 | 序列化 | 反序列化 |
| PHP | true/false | true/false |
| Java | true/false | true/false |
| C# | true/false | true/false |
| VB(.NET) | True/False | True/False |
| ~JavaScript | true/false | true/false |
| ~ActionScript 2 | true/false | true/false |
| ~ActionScript 3 | true/false | true/false |
| Delphi | True/False | True/False |
| Ruby | true/false | true/false |
| Python | True/False | True/False |
!!整數類型
整數類型被序列化為:
{{{
i:<number>;
}}}
其中 <number> 為一個整型數的十進位字串表示,範圍與 32 位元有符號整形數相同(-2147483648 到 2147483647)。數字前可以有正負號,超出這個範圍的整數,應以浮點數類型方式序列化。
| 語言 | 序列化 | 反序列化 |
| PHP | integer | integer |
| Java | Byte/Short/Integer | Integer |
| C# | byte/sbyte/short/ushort/int | int |
| VB(.NET) | Byte/~SByte/Short/~UInt16/Integer | Integer |
| ~JavaScript | -2147483648 到 2147483647 範圍的整數 | Number |
| ~ActionScript 2 | -2147483648 到 2147483647 範圍的整數 | Number |
| ~ActionScript 3 | -2147483648 到 2147483647 範圍的整數 | int |
| Delphi | Byte/Word/~SmallInt/~ShortInt/Integer 以及 -2147483648 到 2147483647 範圍的 ~LongWord/Int64 | Integer |
| Ruby | -2147483648 到 2147483647 範圍內的 Integer | Integer |
| Python | Int 以及 -2147483648 到 2147483647 範圍內 Long | Int |
!!浮點數類型
浮點數類型被序列化為:
{{{
d:<number>;
}}}
其中 <number> 為一個浮點數,其範圍與雙精度浮點數的範圍一樣。可以表示成整數形式、浮點數形式和科學技術法形式。另外 <number> 有三個特殊值,它們是無窮大、負無窮大和非數,它們分別對應的值為 INF、-INF 和 NAN。對於不支援非數的語言,NAN 反序列化時返回 0。如果 <number> 範圍超過雙精度浮點數的範圍,仍然可以用數位形式表示,對於不能表示這個範圍內數位的語言來說,可以視情況將其反序列化為無窮大、負無窮大或 0。
| 語言 | 序列化 | 反序列化 |
| PHP | double | double |
| Java | Long/Float/Double | Long/Double |
| C# | uint/long/ulong/decimal/float/double | int/uint/long/ulong/decimal/double |
| VB(.NET) | ~UInt32/Long/~UInt64/Decimal/Single/Double | Integer/~UInt32/Long/~UInt64/Decimal/Double |
| ~JavaScript | 實數和 -2147483648 到 2147483647 範圍之外的整數 | Number |
| ~ActionScript 2 | 實數和 -2147483648 到 2147483647 範圍之外的整數 | Number |
| ~ActionScript 3 | 實數和 -2147483648 到 2147483647 範圍之外的整數 | Number |
| Delphi | Single/Double/Currency 和 -2147483648 到 2147483647 範圍之外的 ~LongWord/Int64 | Integer/Int64/Currency/Double |
| Ruby | Float 和 -2147483648 到 2147483647 範圍之外的 Integer | Integer/Float |
| Python | Float 和 -2147483648 到 2147483647 範圍之外的 Long | Long/Float |
<<tiddler [[關於本指南]]>>
如果您已經對 PHPRPC 的用途和特點有所瞭解,那麼可以直接進入[[快速入門]],如果你需要針對某種語言的具體資料,那麼請直接進入相關<<tag 語言主題>>。
!為什麼要有 PHPRPC?
<<tiddler [[PHPRPC 的起源]]>>
那麼 PHPRPC 有哪些[[特點|PHPRPC 的特點]]呢?
物件序列化形式為:
{{{
O:<length>:"<class name>":<n>:{<name 1><value 1><name 2><value 2>...<name n><value n>}
}}}
其中 <length> 表示物件的類名 <class name> 的字串長度。<n> 表示物件中的欄位```在 PHP 手冊中,欄位被稱為屬性,而實際上,在 PHP 5 中引入的用 {{{__set}}}、{{{__get}}} 來定義的物件成員更適合叫做屬性。因為用 {{{__set}}}、{{{__get}}} 來定義的物件成員與其他語言中的屬性的行為是一致,而 PHP 手冊中所說的屬性實際上在其他語言中(例如:C#)中被稱為欄位,為了避免混淆,這裏也稱為欄位,而不是屬性。```個數。對於 PHP 來說,這些欄位包括物件所在類及其祖先類中用 var、public、protected 和 private 聲明的欄位,但是不包括 static 和 const 聲明的靜態欄位。也就是說只有實例(instance)欄位。
<name 1>、<name 2>……<name n>表示每個欄位的欄位名,而 <value 1>、<value 2>……<value n> 則表示與欄位名所對應的欄位值。
欄位名是字串型,序列化後格式與字串型資料序列化後的格式相同。
欄位值可以是任意類型,其序列化後的格式與其所對應的類型序列化後的格式相同。
但欄位名的序列化與它們聲明的可見性是有關的,下面重點介紹一下關於欄位名的序列化,這裏以 PHP 來說明:
var 和 public 聲明的欄位都是公共欄位,因此它們的欄位名的序列化格式是相同的。公共欄位的欄位名按照聲明時的欄位名進行序列化,但序列化後的欄位名中不包括聲明時的變數首碼符號 $。
protected 聲明的欄位為保護欄位,在所聲明的類和該類的子類中可見,但在該類的物件實例中不可見。因此保護欄位的欄位名在序列化時,欄位名前面會加上 {{{\0*\0}}} 的首碼。這裏的 \0 表示 ASCII 碼為 0 的字元,而不是 \0 組合。
private 聲明的欄位為私有欄位,只在所聲明的類中可見,在該類的子類和該類的物件實例中均不可見。因此私有欄位的欄位名在序列化時,欄位名前面會加上 {{{\0<declared class name>\0}}} 的首碼。這裏 <declared class name> 表示的是聲明該私有欄位的類的類名,而不是被序列化的對象的類名。因為聲明該私有欄位的類不一定是被序列化的物件的類,而有可能是它的祖先類。
欄位名被作為字串序列化時,字串值中包括根據其可見性所加的首碼。字串長度也包括所加首碼的長度。其中 \0 字元也是計算長度的。
在其他語言中,支援反射私有欄位和保護欄位的語言(如 Java、C# 等),同樣支援上面的欄位名的序列化規則。但有些語言不支援反射私有欄位,這種情況下都按照公共欄位名序列化規則來進行序列化。
它與各語言的對應關係如下表:
| 語言 | 序列化 | 反序列化 |
| PHP | Object/~PHPRPC_Date | Object/~PHPRPC_Date |
| Java | Object/Date | Object/Date |
| C# | Object/~DateTime | Object/~DateTime |
| VB(.NET) | Object/~DateTime | Object/~DateTime |
| ~JavaScript | Object/Date | Object/Date |
| ~ActionScript 2 | Date | Object/Date |
| ~ActionScript 3 | Object/Date | Object/Date |
| Delphi | ~TPHPObject/~TDateTime | ~TPHPObject/~TDateTime |
| Ruby | Struct/Object/Time | Struct/Object/Time |
| Python | Object/datetime.datetime | Object/datetime.datetime |
上表中的 Object 是指各語言中的可序列化物件,關於各語言中具體實現請參見 PHPRPC 3.0 用戶手冊的相關章節。
這裏所說的物件自定義序列化是指對序列化方式進行自定義,而不是指自定義物件的序列化。
PHP 5 提供了一個 Serializable 介面,如果用戶在自己定義的類中實現了這個介面,那麼在該類的物件序列化時,就會被按照用戶實現的方式去進行序列化。
物件自定義序列化形式如下:
{{{
C:<name length>:"<class name>":<data length>:{<data>}
}}}
其中 <name length> 表示類名 <class name> 的長度,<data length> 表示自定義序列化資料 <data> 的長度,而自定義的序列化資料 <data> 是完全的用戶自己定義的格式,與 PHP 序列化格式可以完全無關,這部分資料由用戶自己實現的序列化和反序列化介面方法來管理。
Serializable 介面中定義了 2 個方法,serialize 和 unserialize,這兩個方法不會被直接調用,而是在調用 PHP 序列化過程中被自動調用。其中 serialize 方法沒有參數,它的返回值就是 <data> 的內容。而 unserialize 有一個參數,這個參數的值就是 <data> 的內容,它沒有返回值。實際上介面中的 serialize 方法就是讓用戶來自己序列化物件中的內容,序列化後的內容格式直接充填到 <data> 中,等到反序列化時,取出這部分內容,然後傳給用戶實現的 unserialize 介面方法,讓用戶自己去反序列化這部分內容。
下面舉一個 PHP 的例子,來說明 Serializable 介面的使用:
<code php>
class MyClass implements Serializable {
public $member;
function MyClass() {
$this->member = 'member value';
}
public function serialize() {
return wddx_serialize_value($this->member);
}
public function unserialize($data) {
$this->member = wddx_deserialize($data);
}
}
$a = new MyClass();
echo serialize($a);
echo "\n";
print_r(unserialize(serialize($a)));
</code>
命令行下的輸出結果為:
{{{
C:7:"MyClass":90:{<wddxPacket version='1.0'><header/><data><string>member value</string></data></wddxPacket>}
MyClass Object
(
[member] => member value
)
}}}
目前在其他大部分語言中也提供了同樣的機制可以實現同樣的自定義序列化功能,並可以在不同語言之間通過這種格式進行交互。目前 PHPRPC 實現中,只有 ~ActionScript 2.0 不支援這種物件自定義序列化,但是反序列化這種資料時,它會返回一個包含 name 和 data 欄位的物件。
@@color(#ff6600):本指南使用 [[TiddlyWiki|http://www.tiddlywiki.com/]] 製作,在 [[Firefox|http://www.mozilla.com/firefox/]] 下可以得到最好的流覽效果。建議使用 [[Firefox|http://www.mozilla.com/firefox/]] 流覽。@@
你可以免費線上閱讀或者[[下載|http://www.phprpc.org/download/phprpc_3.0_docs.zip]]閱讀本指南。
未經作者明確授權,禁止發行本指南及其修改版本。
未經作者事先授權,禁止將本作品及其衍生物以標準(紙質)書籍形式發行。
未經作者允許,禁止在學術論文、教材或專著中引用。
如果有興趣再發行或再版本指南的全部或部分內容,不論修改與否,都請事先聯繫本指南作者(同時也是版權所有者) andotcn@msn.com。
對 PHPRPC 有任何問題或建議,請進入[[官方論壇|http://www.phprpc.org/forum]]發佈相關討論。
感謝所有對 PHPRPC 關注和支持的朋友!