2008年10月27日星期一

关于Hessian的PHP支持问题

关于Hessian的PHP支持问题
原创作者:韩国恺 ,时间:2008-10-19
Hessian主页:http://hessian.caucho.com/
Hessian PHP实现主页:http://hessianphp.sourceforge.net/
Hessian PHP开源项目主页:http://sourceforge.net/projects/hessianphp/



1.HessianPHP的DateTime问题
我在家使用时发现了问题,php上显示错误“Cannot redeclare class DateTime”。经调查发现,从PHP5.2开始php有了叫DateTime的类型,见文章http://laughingmeme.org/2007/02/27/looking-at-php5s-datetime-and-datetimezone/,这就与HessianPHP包定义的DateTime类冲突,所以导致了上面的错误。我在公司用的是php 5.0.x的版本没问题,在家用了php5.2.x的版本就有问题。
这就有问题,现在很多客户已使用了PHP的新版本,直接用就不行了,而HessianPHP也没有更新。所以目前只能自己解决。解决方法是:将HessianPHP包中使用的DateTime的地方改成别的名字,如HessianDateTime,经我测试没问题了。
这个问题在其开源项目已有人提到,见http://sourceforge.net/tracker/index.php?func=detail&aid=1804163&group_id=126933&atid=707220,问题解决了,基本做法也是将两个文件里的名字改了。

2. HessianPHP的中文字符编码问题
Hessian调用的中文问题,Java之间调用没有乱码问题,与其它主要语言(C#,Python)应该也是没问题的(未测试),但与PHP直接传输中文字符串就有问题,双向都有问题。经研究,HessianPHP的实现有问题。另外,PHP本身对处理中文字符串有问题,PHP没有很好的支持Unicode,PHP中的字符串是按字节处理的 。
目前解决方法,需要在php客户端和java服务端都做urlencode和urldecode编解码。因为编码后的传输只有ascii字符了,而ascii字符传输没问题。这样就解决了中文问题。
但这种方法有几个缺点:
  1. 需要客户端和服务端双方都要对中文做编解码。
  2. 服务端是向所有客户提供服务的,不只是php客户端。采用此方法后将使以后其它客户端访问此服务也需要编解码。


我对HessianPHP处理中文字符串错误,研究结果如下:
在HessianPHP包中,负责发送String是要用到HessianWriter类writeStringData方法,此类定义在Protocal.php文件中:
function writeStringData($value){
$this->stream .= pack('n',strlen($value));
$this->stream .= utf8_encode($value);
}
在Hessian协议中字符串是要以utf8编码方式传输的,所以这里原作者用了utf8_encode函数。但问题是utf8_encode是将 ISO-8859-1 编码的字符串转换为 UTF-8 编码,问题就是这里。如果我们用的php源文件本身就是utf8编码的,那么其中的中文字符串已经是utf8编码了(php只把字符串看作字节),再用utf8_encode函数就等于把已经是utf8的内容又做了一次编码,所以收到的内容当然是乱码了。我在这里把utf8_encode函数去掉,直接用$value,收到的就不是乱码。如我发送“韩国恺”,收到的内容是“韩国恺z”,可以看出问题就在这里。但又有问题了,“韩国恺”的后面多个一个“z”,说明别的地方自然就少了一个“z”,“z”其实表示一个终结符,服务器端报错“com.caucho.hessian.io.HessianProtocolException: hello: expected end of call ('z') at end of stream.”。推测可知,这个问题是前面计算字符串长度strlen($value)有误导致的问题,根据Hessian字符串映射的定义, 长度是按照16bit字符长度计算的,并不一定等于字节的数量strlen($value)。也就是说,长度应该是Unicode的字符数。而PHP中的strlen函数其实只是得到了字符串的字节数,不是真正的字符数,中文是多字节的,当然有问题。清楚原因了,我们可以解决这个问题。很简单,将strlen函数替换为我后面定义的utf8_strlen函数即可,utf8_strlen函数返回的是这个utf8编码的字符串的真正字符数,作用如同Java中String的length方法一样。最后,经我修改后,终于解决了php发送中文的乱码问题。修改后的代码如下:
function writeStringData($value){
$this->stream .= pack('n',utf8_strlen($value));
$this->stream .= $value;
}
另外,HessianPHP中,负责解析字符串的HessianParser类的readString方法也有问题,导致php收到的中文字符串也有问题,变成了问号。有经验的人很清楚,一般将UTF-8/GBK等多字节编码转换为ASCII/ISO-8859-1的单字节编码时,不兼容的部分就会变成问号了。通过查看Protocal.php文件HessianParser类的readString方法,可以看出作者已经考虑根据UTF-8读取字节的问题,但是该方法的最后一句话“return utf8_decode($string);”就出了问题。utf8_decode函数是把字符串转成ISO-8859-1的编码,那当然中文就成了问号!最后,只要改为“return $string;”就可以了。
终于,修改后的Hessian的PHP实现可以按照Hessian协议标准正常的收发中文字符串了!这是完全透明的。
这种方法才是从根本上解决了Hessian PHP的问题,完全没有前面解决方法的所有缺点。有点麻烦的是,要自己修改源文件的几个地方。我会向作者反映这个问题,希望以后版本能够改进,继续支持开源。


以上可以看出,HessianPHP的实现者在国际化(I18N)方面的理解上不够,没有正确理解php中utf8_encode和utf8_decode函数。还可以看出PHP不支持Unicode真的很烂,比起其它Java等支持Unicode的语言要多走一些弯路。
实际上,PHP与Java的xml-rpc调用上也存在类似的问题,这可不是Java的问题。

我认为Hessian的PHP支持不好。下载的最新HessianPHP包中,显示有“HessianPHP 1.0.5 RC2 (PHP4 & 5) Release Date: 2005-12-29”,从06年开始就没有更新过了,只有少数人在维护,且支持处于不活跃状态。另外,文档不够详细,细节需要看源文件。如果遇到什么问题,只能我们自己解决了,这是一种额外的负担。

提示:
除了使用Hessian作为rpc协议外,使用基本的RESTful的方式也可以很轻松的实现服务,比如通过http get或post方法把参数传过来,返回特定的xml或json格式的结果。这种方式非常简单、灵活,且语言、平台无关,不需要额外对中文编解码。



function utf8_strlen($str)
{
$count = 0;

for($i = 0; $i
< strlen($str); $i++)
{
$value = ord($str[$i]);

if($value > 127)
{
if($value >= 192 &&
$value <= 223)
$i++;
elseif($value >= 224
&& $value <= 239)
$i = $i + 2;

elseif($value >= 240 && $value <= 247)
$i = $i +
3;
else
die('Not a UTF-8 compatible
string');
}

$count++;
}


return $count;
}