PHP 提供了一套非常灵活的自动构建系统(automatic build system),它把所有的模块均放在 Ext 子目录下。每个模块除自身的源代码外,还都有一个用来配置该扩展的 config.m4 文件(详情请参见http://www.gnu.org/software/m4/manual/m4.html )。

包括 .cvsignore 在内的所有文件都是由位于 Ext 目录下的 ext_skel 脚本自动生成的,它的参数就是你想创建模块的名称。这个脚本会创建一个与模块名相同的目录,里面包含了与该模块对应的一些的文件。

下面是操作步骤:

:~/cvs/php4/ext:> ./ext_skel --extname=my_module
Creating directory my_module
Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].

To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/my_module/config.m4
3. $ ./buildconf
4. $ ./configure –[with|enable]-my_module
5. $ make
6. $ ./php -f ext/my_module/my_module.php
7. $ vi ext/my_module/my_module.c
8. $ make

Repeat steps 3-6 until you are satisfied with ext/my_module/config.m4 and step 6 confirms that your module is compiled into PHP. Then, start writing code and repeat the last two steps as often as necessary.

这些指令就会生成前面所说的那些文件。为了能够在自动配置文件和构建程序中包含新增加的模块,你还需要再运行一次 buildconf 命令。这个命令会通过搜索 Ext 目录和查找所有 config.m4 文件来重新生成 configure 脚本。默认情况下的的 config.m4 文件如例 3-1 所示,看起来可能会稍嫌复杂:

例3.1 默认的 config.m4 文件


dnl $Id: build.xml,v 1.1 2005/08/21 16:27:06 goba Exp $
dnl config.m4 for extension my_module
dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

dnl PHP_ARG_WITH(my_module, for my_module support, dnl Make sure that the comment is aligned:
dnl [ --with-my_module Include my_module support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(my_module, whether to enable my_module support,
dnl Make sure that the comment is aligned:
dnl [ --enable-my_module Enable my_module support])

if test $PHP_MY_MODULE != “no”; then
dnl Write more examples of tests here…

dnl # –with-my_module -> check with-path
dnl SEARCH_PATH = /usr/local /usr # you might want to change this
dnl SEARCH_FOR=/include/my_module.h you most likely want to change this
dnl if test -r $PHP_MY_MODULE/; then # path given as parameter
dnl MY_MODULE_DIR=$PHP_MY_MODULE
dnl else # search default path list
dnl AC_MSG_CHECKING([for my_module files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl MY_MODULE_DIR=$i
AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$MY_MODULE_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the my_module distribution])
dnl fi

dnl # –with-my_module -> add include path
dnl PHP_ADD_INCLUDE($MY_MODULE_DIR/include)

dnl # –with-my_module -> chech for lib and symbol presence
dnl LIBNAME=my_module # you may want to change this
dnl LIBSYMBOL=my_module # you most likely want to change this
dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [ dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MY_MODULE_DIR/lib, MY_MODULE_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_MY_MODULELIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong my_module lib version or lib not found])
dnl ],[
dnl -L$MY_MODULE_DIR/lib -lm -ldl
dnl ])
dnl
dnl PHP_SUBST(MY_MODULE_SHARED_LIBADD)

PHP_NEW_EXTENSION(my_module, my_module.c, $ext_shared)
fi

如果你不太熟悉 M4 文件(现在毫无疑问是熟悉 M4 文件的大好时机),那么就可能会有点糊涂。但是别担心,其实非常简单。

注意:凡是带有 dnl 前缀的都是注释,注释是不被解析的。

config.m4 文件负责在配置时解析 configure 的命令行选项。这就是说它将检查所需的外部文件并且要做一些类似配置与安装的任务。

默认的配置文件将会在 configure 脚本中产生两个配置指令:–with-my_module 和 –enable-my_module。当需要引用外部文件时使用第一个选项(就像用 –with-apache 指令来引用 Apache 的目录一样)。第二个选项可以让用户简单的决定是否要启用该扩展。不管你使用哪一个指令,你都应该注释掉另外一个。也就是说,如果你使用了–enable-my_module,那就应该去掉–with-my_module。反之亦然。

默认情况下,通过 ext_skel 创建的 config.m4 都能接受指令,并且会自动启用该扩展。启用该扩展是通过 PHP_EXTENSION 这个宏进行的。如果你要改变一下默认的情况,想让用户明确的使用 –enable-my_module 或 –with-my_module 指令来把扩展包含在 PHP 二进制文件当中,那么将 “if test "$PHP_MY_MODULE" != “no””改为“if test "$PHP_MY_MODULE" == "yes"”即可。

if test "$PHP_MY_MODULE" == "yes"; then dnl
Action.. PHP_EXTENSION(my_module, $ext_shared)
fi

这样就会导致在每次重新配置和编译 PHP 时都要求用户使用 –enable-my_module 指令。

另外请注意在修改 config.m4 文件后需要重新运行 buildconf 命令。

在我们开始讨论具体编码这个话题前,你应该让自己熟悉一下 PHP 的源代码树以便可以迅速地对各个源文件进行定位。这也是编写和调试 PHP 扩展所必须具备的一种能力。

下表列出了一些主要目录的内容:

目录 内容
php-src 包含了PHP主源文件和主头文件;在这里你可以找到所有的 PHP API 定义、宏等内容。(重要). 其他的一些东西你也可以在这里找到。
php-src/ext 这里是存放动态和内建模块的仓库;默认情况下,这些就是被集成于主源码树中的“官方” PHP 模块。自 PHP 4.0开始,这些PHP标准扩展都可以编译为动态可载入的模块。(至少这些是可以的)。
php-src/main 这个目录包含主要的 PHP 宏和定义。 (重要)
php-src/pear 这个目录就是“PHP 扩展与应用仓库”的目录。包含了PEAR 的核心文件。
php-src/sapi 包含了不同服务器抽象层的代码。
TSRM Zend 和 PHP的 “线程安全资源管理器” (TSRM) 目录。
ZendEngine2 包含了Zend 引擎文件;在这里你可以找到所有的 Zend API 定义与宏等。(重要)

当然,讨论 PHP 包里面全部每一个文件无疑是超出了本章的范围,但你还是应该仔细看一下下面的几个文件

  • php-src/main/php.h, 位于PHP 主目录。这个文件包含了绝大部分 PHP 宏及 API 定义。
  • php-src/Zend/zend.h, 位于 Zend 主目录。这个文件包含了绝大部分 Zend 宏及 API 定义。
  • php-src/Zend/zend_API.h, 也位于 Zend 主目录,包含了Zend API 的定义。

除此之外,你也应该注意一下这些文件所包含的一些文件。举例来说,哪些文件与 Zend 执行器有关,哪些文件又为 PHP 初始化工作提供了支持等等。在阅读完这些文件之后,你还可以花点时间再围绕PHP包来看一些文件,了解一下这些文件和模块之间的依赖性――它们之间是如何依赖于别的文件又是如何为其他文件提供支持的。同时这也可以帮助你适应一下 PHP 创作者们代码的风格。要想扩展 PHP,你应该尽快适应这种风格。

扩展规范

Zend 是用一些特定的规范构建的。为了避免破坏这些规范,你应该遵循以下的几个规则:

几乎对于每一项重要的任务,Zend 都预先提供了极为方便的宏。在下面章节的图表里将会描述到大部分基本函数、结构和宏。这些宏定义大多可以在 Zend.h 和 Zend_API.h 中找到。我们建议您在学习完本节之后仔细看一下这些文件。(当然你也可以现在就阅读这些文件,但你可能不会留下太多的印象。)

内存管理

资源管理仍然是一个极为关键的问题,尤其是对服务器软件而言。资源里最具宝贵的则非内存莫属了,内存管理也必须极端小心。内存管理在 Zend 中已经被部分抽象,而且你也应该坚持使用这些抽象,原因显而易见:由于得以抽象,Zend 就可以完全控制内存的分配。Zend 可以确定一块内存是否在使用,也可以自动释放未使用和失去引用的内存块,因此就可以避免内存泄漏。下表列出了一些常用函数:

函数 描述
emalloc() 用于替代 malloc()
efree() 用于替代 free()
estrdup() 用于替代 strdup()
estrndup() 用于替代strndup()。速度要快于 estrdup() 而且是二进制安全的。如果你在复制之前预先知道这个字符串的长度那就推荐你使用这个函数。
ecalloc() 用于替代 calloc()
erealloc() 用于替代 realloc()

emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用于申请内部的内存,efree() 则用来释放这些前面这些函数申请的内存。e*() 函数所用到的内存仅对当前本地的处理请求有效,并且会在脚本执行完毕,处理请求终止时被释放。

Zend 还有一个线程安全资源管理器,这可以为多线程WEB 服务器提供更好的本地支持。不过这需要你为所有的全局变量申请一个局部结构来支持并发线程。但是因为在写本章内容时Zend 的线程安全模式仍未完成,因此我们无法过多地涉及这个话题。

目录与文件函数

下列目录与文件函数应该在 Zend 模块内使用。它们的表现和对应的 C 语言版本完全一致,只是在线程级提供了虚拟目录的支持。

Zend 函数 对应的 C 函数
V_GETCWD() getcwd()
V_FOPEN() fopen()
V_OPEN() open()
V_CHDIR() chdir()
V_GETWD() getwd()
V_CHDIR_FILE() 将当前的工作目录切换到一个以文件名为参数的该文件所在的目录。
V_STAT() stat()
V_LSTAT() lstat()

字符串处理

在 Zend 引擎中,与处理诸如整数、布尔值等这些无需为其保存的值而额外申请内存的简单类型不同,如果你想从一个函数返回一个字符串,或往符号表新建一个字符串变量,或做其他类似的事情,那你就必须确认是否已经使用上面的 e*() 等函数为这些字符串申请内存。(你可能对此没有多大的感觉。无所谓,现在你只需在脑子里有点印象即可,我们稍后就会再次回到这个话题)

复杂类型

像数组和对象等这些复杂类型需要另外不同的处理。它们被出存在哈希表中,Zend 提供了一些简单的 API 来操作这些类型。

正如上图(图3-1 PHP 内部结构图)所示,PHP 主要以三种方式来进行扩展:外部模块,内建模块和 Zend 引擎。下面我们将分别讨论这些方式:

外部模块

外部模块可以在脚本运行时使用 dl() 函数载入。这个函数从磁盘载入一个共享对象并将它的功能与调用该函数的脚本进行绑定并使之生效。脚本终止后,这个外部模块将在内存中被丢弃。这种方式有利有弊,如下表所示:

优点 缺点
外部模块不需要重新对 PHP 进行编译。 共享对象在每次脚本调用时都需要对其进行加载,速度较慢。
PHP通过“外包”方式来让自身的体积保持很小。 附加的外部模块文件会让磁盘变得比较散乱。
  每个想使用该模块功能的脚本都必须使用 dl() 函数手动加载,或者在 php.ini 文件当中添加一些扩展标签(这并不总是一个恰当的解决方案)。

综上所述,外部模块非常适合开发第三方产品,较少使用的附加的小功能或者仅仅是调试等这些用途。为了迅速开发一些附加功能,外部模块是最佳方式。但对于一些经常使用的、实现较大的,代码较为复杂的应用,那就有些得不偿失了。

第三方可能会考虑在 php.ini 文件中使用扩展标签来创建一个新的外部模块。这些外部模块完全同主PHP 包分离,这一点非常适合应用于一些商业环境。商业性的发行商可以仅发送这些外部模块而不必再额外创建那些并不允许绑定这些商业模块的PHP 二进制代码。

内建模块

内建模块被直接编译进 PHP 并存在于每一个 PHP 处理请求当中。它们的功能在脚本开始运行时立即生效。和外部模块一样,内建模块也有一下利弊:

优点 缺点
无需专门手动载入,功能即时生效。 修改内建模块时需要重新编译PHP。
无需额外的磁盘文件,所有功能均内置在 PHP 二进制代码当中。 PHP 二进制文件会变大并且会消耗更多的内存。

Zend 引擎

当然,你也能直接在 Zend 引擎里面进行扩展。如果你需要在语言特性方面做些改动或者是需要在语言核心内置一些特别的功能,那么这就是一种很好的方式。但一般情况下应该尽力避免对 Zend 引擎的修改。这里面的改动会导致和其他代码的不兼容,而且几乎没有人会适应打过特殊补丁的 Zend 引擎。况且这些改动与主 PHP 源代码是不可分割的,因此就有可能在下一次的官方的源代码更新中被覆盖掉。因此,这种方式通常被认为是“不良的习惯”。由于使用极其稀少,本章将不再对此进行赘述。

“扩展 PHP”说起来容易做起来难。PHP 现在已经发展成了一个具有数兆字节源代码的非常成熟的系统。要想深入这样的一个系统,有很多东西需要学习和考虑。在写这一章节的时候,我们最终决定采用“边学边做”的方式。这也许并不是最科学和专业的方式,但却应该是最有趣和最有效的一种方式。在下面的小节里,你首先会非常快速的学习到如何写一个虽然很基础但却能立即运行的扩展,然后将会学习到有关 Zend API 的高级功能。另外一个选择就是将其作为一个整体,一次性的讲述所有的这些操作、设计、技巧和诀窍等,并且可以让我们在实际动手前就可以得到一副完整的愿景。这看起来似乎是一个更好的方法,也没有死角,但它却枯燥无味、费时费力,很容易让人感到气馁。这就是我们为什么要采用非常直接的讲法的原因。

注意,尽管这一章会尽可能多讲述一些关于 PHP 内部工作机制的知识,但要想真的给出一份在任何时间任何情况下的PHP 扩展指南,那简直是不可能的。PHP 是如此庞大和复杂,以致于只有你亲自动手实践一下才有可能真正理解它的内部工作机制,因此我们强烈推荐你随时参考它的源代码来进行工作。

Zend 是什么? PHP 又是什么?

Zend 指的是语言引擎,PHP 指的是我们从外面看到的一套完整的系统。这听起来有点糊涂,但其实并不复杂(见图3-1 PHP 内部结构图)。为了实现一个 WEB 脚本的解释器,你需要完成以下三个部分的工作:

  1. 解释器部分:负责对输入代码的分析、翻译和执行;
  2. 功能性部分:负责具体实现语言的各种功能(比如它的函数等等);
  3. 接口部分:负责同 WEB 服务器的会话等功能。

Zend 包括了第一部分的全部和第二部分的局部,PHP 包括了第二部分的局部和第三部分的全部。他们合起来称之为 PHP 包。Zend 构成了语言的核心,同时也包含了一些最基本的 PHP 预定义函数的实现。PHP 则包含了所有创造出语言本身各种显著特性的模块。

PHP 内部结构图

图3-1   PHP 内部结构图

下面将要讨论PHP 允许在哪里扩展以及如何扩展。

摘要

知者不言,言者不知。

――老子《道德经》五十六章

有时候,单纯依靠 PHP “本身”是不行的。尽管普通用户很少遇到这种情况,但一些专业性的应用则经常需要将 PHP 的性能发挥到极致(这里的性能是指速度或功能)。由于受到 PHP 语言本身的限制,同时还可能不得不把庞大的库文件包含到每个脚本当中。因此,某些新功能并不是总能被顺利实现,所以我们必须另外寻找一些方法来克服 PHP 的这些缺点。

了解到了这一点,我们就应该接触一下 PHP 的心脏并探究一下它的内核-可以编译成 PHP 并让之工作的 C 代码-的时候了。

译序:
网上关于 PHP 的资料多如牛毛,关于其核心 Zend Engine 的却少之又少。PHP 中文手册出现已 N 年,但 Zend API 的翻译却仍然不见动静,小弟自觉对 Zend Engine 略有小窥,并且翻译也有助于强迫自己对文章的进一步理解,于是尝试翻译此章,英文不好,恭请方家指点校核。转载请注明来自抚琴居(译者主页):http://www.yAnbiN.org/

PHP 中文手册《Zend API:深入 PHP 内核》一章当前翻译进度(已翻译完毕):

  1. 摘要
  2. 概述
  3. 可扩展性
  4. 源码布局
  5. 自动构建系统
  6. 开始创建扩展
  7. 使用扩展
  8. 故障处理
  9. 关于模块代码的讨论
  10. 接收参数
  11. 创建变量
  12. 使用拷贝构造函数复制变量内容
  13. 返回函数值
  14. 信息输出
  15. 启动函数与关闭函数
  16. 调用用户函数
  17. 支持初始化文件(php.ini)
  18. 何去何从
  19. 参考:关于配置文件的一些宏
  20. API 宏

作为目前最优秀 Delphi 反编译器(其实称之为针对 Delphi 的反汇编器会更为恰当些),DeDe 已经有三年没有更新了。随着越来越多的 Delphi 程序员开始转到 BDS2006,它也似乎有点力不从心了。除了不能正确识别高于 Delphi 7 编译的程序的编译器版本外,还有以下几个方面有待改进:

  1. 缺乏对应的 VCL 符号识别文件。这本应该在设计时考虑到的,但制作相应版本 VCL 符号文件却异常困难。似乎除了作者自己外,他人很难操刀;
  2. Dump DCU 引擎比较古老,现在已经有新版本出来了;
  3. 对 Delphi 窗体可视化编辑也不太好,对中文字符串也不能正确显示;
  4. 整个反汇编项目不能保存。下次只能重新“处理”。

所幸网上流传有 DeDe 较早版本(v3.10,DeDe 最新版本为 v3.50)的源代码,可供我们瞻仰学习一番。希望我能彻底读懂 DeDe 源代码,因为它的源代码实在太难读了~ :(

根据鄙人的使用经验,感觉相对于普通的 CHM 手册,Extended CHM 版则额外提供了以下几种主要特性:

  1. 附带非常实用用户注释,其价值不亚于用户手册,这是最大的优点!
  2. 可以使用自定义的 CSS 文件来切换外观,可自定义右键菜单;
  3. PHP 代码块以语法高亮显示;
  4. PHP 代码块中的函数为超链接形式,点击自动跳转至相应的函数;
  5. 可以很方便地集成于大多数 IDE 和编辑器。

尽管 Extended CHM 格式的 PHP 手册提供了这么多优秀特性,但 PHP 官方网站却只提供了英文版,始终没有提供简体中文版。因此我就趁空闲之机编译了这套手册。虽然网上也有其他热心 PHPER 编译提供的中文版,但大多只是简单的从 CVS 上 Checkout 编译了一下,并没有针对中文版的特殊情况做些处理,因此就出现了诸如“不能搜索中文”、“搜索到的条目中文标题显示为乱码”或“页面中某些中文显示为数字编码”等 BUG。而本版本几乎完美的解决了这些 BUG ,欢迎大家测试使用,反馈请到PHP 官方站点抚琴居,我只负责编译问题。:-)

压缩包内的文件说明:

  • php_manual_zh.chm 是手册的主文件,如果你只需要纯文本的手册,可以只保留这个文件。
  • php_manual_notes.chm 是手册对应章节的用户注释部分,不能单独打开,只能配合 php_manual_zh.chm 使用。这是一个很有用的东东,补充了一些手册没有的或不宜加入的部分,并且是随时更新的。
  • php_manual_prefs.js 负责为上面两个 chm 文件载入相应的 skin ,并且提供了对自定义右键菜单功能的支持。
  • context.ini 即是右键菜单的定义部分。你可以使用 php_manual_prefs.exe 这个 GUI 程序来配置,也可以直接手动编辑 context.ini 文件,但是编辑前注意和 php_manual_prefs.js 相关部分对应。
  • /skins/ 这个目录保存的即是一些另外的 skin(High 和 Low 两种Skin已经内置在两个 chm 文件中),可以根据自己的喜好来定。
  • mirrors.ini 里面保存所有 php.net 的镜像网址,在手册主文件当中,有个在线版本的连接,可以在 php_manual_prefs.exe 中定义采用哪个镜像来访问。

再附上一条使用技巧--在 EditPlus 中集成本手册的方法:

【工具(Tools)】–>【用户工具(User Tools)】–>【添加工具(Add Tool)】:
菜单文本(Menu Text):PHP 手册
命令(Command): HH X:\XXXXXX\php_manual_zh.chm (此处X:\XXXX替换为您的 chm 帮助文件的位置)
参数(Argument): ::/_function.html#$(CurWord)
初始目录(Initial): $(FileDir) (可不填)

使用方法:把点击某一关键词,然后按快捷键(如:Ctrl+1,这个快捷键可以在【工具(Tools)】菜单下看到)即可。

点此下载PHP 手册中文版(Extended CHM 格式)

在 PHP 开发领域,不断在讨论讨论 OO ,讨论框架、讨论设计模式、讨论 MVC 模型,讨论这些所带来的种种好处。我不对这些好处进行否认,我只是认为不能盲目跟随某种开发方式,一切方法都是有适用范围的, PHP 开发也不例外。PHP 开发根据受众、服务目标等可以大致可以分为三种不同的开发领域:行业商业软件通用共享软件私有专用软件。在这些不同的领域,所主要采用的开发手段也是有所区别的。明确自己产品所在领域并确定下来一种开发方法也是很有必要的。需要说明的是这个三个分类严格说来并不是完全并列,泾渭分明,希望这不会给大家带来困扰。领会精神~^_^

另限于个人的水平及观点的狭隘,有些看法难免有失偏颇甚至偏激,还望方家不吝赐教。

首先来说一下行业商用软件

这类软件主要面向特定行业或企业的某种应用,项目设计较为复杂。一般为某个开发公司独立承接,几乎没有竞争对手。目前主要以 CRM、CMS、OA 等为代表。这类软件的客户并不关心系统的运行速度有多快,而是关心这个系统能否协调一致完成所需要的功能。由于是面向特定的客户,所以该类软件使用面较为狭窄,若换了另外一家客户通常就不能很好的运行(这里的运行并非指代码的执行,而是指功能的实现),就必须推倒重来。为了减少在开发不同系统当中所作无谓的基础性的重复劳动,我们就必须把这些不同的系统应用中相同的部分给提取出来。这些相同的部分既含有代码技术上的相似性,也包含设计流程上相似性。这是一种将问题进行抽象的过程。我们现有的这些框架、模型就是前人在这些抽象过程的劳动成果。由于几乎每个 Java 项目通常都是较为大型的复杂的应用,所以我们在这些项目中处处可见框架,处处可见模式。你不采用这种开发方式,那就几乎无法前行。PHP 在开发这类应用时是跟 Java 很相似的,唯一不同的就是各自运行环境(主要是指各自的语言解释器,下同)不同。PHP 是一种脚本语言,其支持各种 OO 语言特性的代价很沉重。无论是在空间还是在时间上。所幸对于这类行业商用软件性能是次要的,并且可以自己决定运行环境,因此采用对 OO 特性支持良好的 PHP5 是必然的选择。而且采用一些框架也是必须的。

再来说说通用共享软件

这个概念从传统桌面型共享软件的概念而来,它的主要特点就是客户(包括潜在的客户)众多,同一类型的软件用户的选择也较多,竞争较为激烈。这类软件目前以论坛社区程序为代表。为了赢得客户,那你必须要做得比一般竞争对手更好。对这类软件来说,竞争主要在一下几个方面:

  1. 界面
  2. 界面是你的客户(包括客户的客户)对你产品的第一印象。因此界面必须要友好。界面不单指外观,还包括可操作性。界面必须要考虑到大多数人的习惯,操作必须要简单、顺手。外观虽然是萝卜白菜,但你也必须留一个选择权(接口)给客户,让客户能非常方便地修改使用。

  3. 性能
  4. 良好的界面当然会给你的产品加分。但在这可以 Ctrl+C 和 Ctrl+V 的世界,再优秀的界面都会被竞争对手瞬间所“学习”。如果说界面是第一印象,那么性能将是致命的考察。因为界面可以更换,但你不能指望客户自己去完善代码。在 PHP 开发中,性能很大程度上是指代码的运行速度,另外一个重要的表现就是对系统资源的损耗程度。每个处理进程的资源占有率越低,系统就越有时间来同时处理更多的请求。这些都是一个细微之处见真章的功夫。希望有机会再和大家详细探讨。但其中我个人有个大致的原则就是避免使用类。PHP 中的类真是性能杀手(注:在 PHP 5.2.x 以后情况有了极大的改善)。避免使用类的直接后果就是避免使用框架。有人说这样做会影响开发效率。我承认,是可能会造成一些这样的效果。但我认为,效率分两种:开发效率和运行效率。在行业商用软件中我们这样做是不合适的,但在通用共享软件里面,我们的竞争对手很多。况且客户才不会管你使用什么框架、采用什么模式,客户只关心他们自己的体验。雨和熊掌不可兼得,我们必须要舍弃一点开发效率来保证运行效率。这也是不得已而为之。

  5. 兼容性
  6. 这里的兼容性主要是指代码在不同 PHP 版本之间的兼容性。我们注意到,通常情况下,PHP 版本越新就意味着性能和稳定性就越强。因此我们应尽可能采用新版本所具有函数和语法。但另一方面,由于用户众多,我们无法对每一个用户的运行环境做出假设。并不是每个客户都拥有独立的服务器,很多使用通用共享软件客户都是采用虚拟主机作为运行平台。况且也不是每个虚拟主机提供商都能支持最新版本的 PHP 解释器。兼容性还有一个副作用就是限制了一些开发手段。毫无疑问,在 PHP4 平台想得心应手地使用各种 OO 特性与技巧是很困难的。这就有一个平衡问题。如何处理这种平衡,我想一些关于 PHP 版本运行情况分布的调查或许可以作为一个有力的参考。

最后是私有专用软件

私有专用软件是指具有一定研发实力的公司根据自身的业务特点而独立开发的应用系统。专供自己使用,很少作为产品出售。这类系统复杂性并不亚于企业商用型软件,但其对性能的要求更高,几近苛刻。这些系统以 sina 的新闻发布系统、淘宝的物品买卖管理这些为代表(虽然所举的这些并不全都是采用 PHP 开发)。这类系统的特点是通常企业自身也拥有独立的服务器,可以对服务器自身有针对性的配置和优化。若想对付这些应用,必须从企业业务自身特点出发,并根据实际的服务器情况专门进行对 PHP 模块进行优化编译。然后还采用 PHP 扩展甚至是 Zend 扩展来代替脚本中实现一些功能。这就要求 PHP 程序员同时要具备一定的 C 语言知识(虽然理论上其它的语言也可以,但无疑 C 是最安全和方便的)。

对于开发一个不考虑跨平台,只在 Windows Server 环境下运行的高性能服务器来说,IOCP 无疑是一个最优的解决方案。最近一个项目要用到 IOCP ,特地找了些资料。网上的资料很多,但很多都是以基础性的介绍为主,代码也是些经典书籍上的标准代码。这些代码对理解 IOCP 无疑是很重要的,但对于高性能服务器开发来说,细节的实现则似乎更加重要。根据自己最近做的一个项目,有几点体会,特记录下来,以备后查。

  1. 是在写服务端而不是在写客户端
  2. 服务端与客户端绝对是两码事。在客户端我们提倡 Create/New 和 Free/Dispose,随用随申请,不用即释放。但在服务端要尽量避免这样做。在客户端可以随时使用 string 类型,但在服务端也必须尽量避免使用 string 。string使用起来异常方便,但我们看看编译后的代码恐怕就会只冒冷汗:原来编译器为string的方便做了那么多额外的工作。客户端要为客户解决内存,但服务端能“浪费”则“浪费”。

  3. 内存管理
  4. 不得不再次佩服一下某大牛说的话:“玩服务器就是玩内存”。
    内存管理不当就会造成内存泄漏和内存碎片。对于客户端而言,内存碎片几乎不算是问题。内存泄漏那么一点点也可以接受。但对于 24 * 7 的服务器而言,这却绝对致命,其重要性甚至超过了 IOCP 本身。

    关于内存泄漏,只要记得保证申请和释放动作的对称性即可,外加一系列的测试工具,基本就可以把这个问题解决。
    其次就是内存碎片。内存碎片问题的重要性绝不亚于内存泄漏。造成碎片的原因也是防不胜防。简单的如每次的 New 和 Dispose ,Create 和 Free ,隐晦一点的如 string 类型的操作。

    解决办法:

    首先对于Create和Free,尽量少用。换句话说,尽量少用封装。适当的封装是可以的,只要封装的层次不是太深。Delphi 提供了 VCL 源码,我们可以看看即使是直接继承 TObject 那也会多做多少工作!对于频繁调用的函数,不要采用虚拟函数。这些晚绑定的函数,想调用就得查找 VMT,很费时间。对于类的普通函数,由于进行了早绑定,这个和其他非类的常规一样,不会降低效率。

    其次,相应的,尽量使用结构和函数来代替类。对于结构,New 和 Dispose 也要尽量少用。要集中的使用来避免内存碎片。我们应该一次性把所预料的内存都申请完,服务器就得有服务器的样,放着那么多内存干什么。早晚都得申请,为什么在服务端启动的时候不一次性申请完,在服务端关闭的时候一次性释放掉?既避免了内存碎片又避免了以后的再申请操作,一举两得,何乐而不为?要知道内存分配和释放是非常昂贵的操作。不论是从时间上还是从稳定性上而言。再具体些,怎么保存这些申请到的内存?怎么保证在必要的时候可以很方便的再申请或及时的释放一些内存?我采用的是链表。在每次为一个数据结构申请内存的时候,先查看这个链表是否为空,如果不为空,就从这个链表中取出一个内存块,不需要真正调用函数申请。如果为空,再动态分配。使用完成后,把这个数据结构不释放,而是再把它插入到链表中去,以便下一次使用。

    再次,不用 string 用什么?用数组!用字符数组!就像C中的字符数组一样。就是这么简单~

  5. 使用使用 AcceptEx 代替 accept
  6. AcceptEx 函数是微软的 Winsosk 扩展函数,这个函数和 accept 或 WSAAccept 是阻塞的,一直要到有客户端连接上来后 accept 才返回,而且,accept 本质上是在接受一个连接的同时再创建一个套接字。而创建一个套接字,对于 Windows 的网络模型而言,代价是非常大的。而 AcceptEx 则避免了这两个问题。首先它是异步的,直接就返回了。其次可以也是必须事先要和某一套接字绑定在一起。这样在接受一个连接时就不必再创建套接字了,而这个套接字我们可以事先使用 WSASocket 函数申请好,就像上面的预申请内存一样。总而言之一句话,“准备工作”一定要做好,到时需要拿来就是了。

    这里面还有一个问题,刚开始创建和投递多少 AcceptEx 调用?万一不够用怎么办?这个问题我们可以把 FD_ACCEPT 事件和一个 Event 对象关联起来,然后用 WaitForSingleObject() 等待这个 Event ,若预投递的套接字不够用的话就会触发 FD_ACCEPT 事件, Event 受信,WaitForSingleObject() 返回,我们就重新再发出一些 AcceptEx 调用。

  7. 要利用好 GetQueuedCompletionStatus() 函数中的 lpCompletionKey 参数
  8. 这个东西传递的是“单句柄数据”,换句话说,是和每个连接/套接字本身而不是在某个连接的 I/O 操作绑定的。在服务端设计当中,不可避免的,我们都会有一些只和该连接本身关联的一些数据(比如这个连接的套接字、客户端的IP,连接的会话密钥等等),如果采用传统的操作手法,将会不可避免采用一些查询机制在每次收发数据时来获取这些信息(比如数据的解密密钥),但现在我们只需要再创建完成端口时把包含这些信息结构体的指针传入就行了,下次直接使用 GetQueuedCompletionStatus() 取得结构体指针就行了,无需再次查询。方便和高效之极。

  9. 关于 Delphi 下 WinSock 函数库的封装
  10. 这是 Delphi 相对于 C/C++ 特有的问题。这些库 M$ 都是以 C 头文件(.h文件)形式给出的。因此若想在 Delphi 上调用就需要把其中的 C 表达形式转换为 Delphi 表达形式。问题就出在转换这里。抛开在转换期间可能会转换错误以外,由于没有一个强制性的转换标准,所以就会造成好几个转换版本,既便他们都是正确的。这就造成调用时所采用的代码不同。就拿最常用的 GetQueuedCompletionStatus() 函数来说,M$定义如下:

    BOOL GetQueuedCompletionStatus(
      HANDLE CompletionPort,
      LPDWORD lpNumberOfBytes,
      PULONG_PTR lpCompletionKey,
      LPOVERLAPPED* lpOverlapped,
      DWORD dwMilliseconds
    );

    其中第二、三、四个参数都是需要传入指针形式的。某个 Delphi 转换版本如下:
    function GetQueuedCompletionStatus(CompletionPort: THandle;
      var lpNumberOfBytesTransferred, lpCompletionKey: DWORD;
      var lpOverlapped: POverlapped; dwMilliseconds: DWORD): BOOL; stdcall;

    当然这个转换是没问题的,但关键是对于第二、三、四个参数它采用 var 而使得参数进行了引用传递。在调用时只需要填入参数,而不必再使用取运算符 @ 来填入参数地址。另外一个转换版本如下:
    function GetQueuedCompletionStatus(CompletionPort: THandle;
      lpNumberOfBytesTransferred, lpCompletionKey: PDWORD;
      lpOverlapped: PPOverlapped; dwMilliseconds: DWORD): BOOL; stdcall;

    这个版本没有采用 var 引用传递,而是采用的指针的值传递。调用时必须先使用运算符 @ 来取得参数地址。那么这种差异就可能导致我们更换一个 WinSock 声明文件就不能正常编译的问题。更可怕的是编译通过,但是误把指针当引用而引起的运行时错误,更是防不胜防。所以我觉得在一个项目当中有必要统一一下。

    那么两种声明哪个更好些?虽然第一种在调用时代码可能书写较为美观,但我还是推荐第二种,不采用 var 引用传递的那种。原因有两点:一是这样最接近原 .h 头文件的表达形式,二是调用时也会明显看到是需要传入一个类型还是需要该指向该类型的一个指针。

  11. 其他的一些小问题:
    1. 既然决定采用 IOCP 了,那就不要考虑跨平台了。尽量采用 M$ 提供的扩展版本的 Winsock 函数。这通常会给程序带来一些性能方面的优化。
    2. 同样的代码,在不同版本的 Windows Server 上表现也是不一样的。通常版本越高级,负载能力越强。
    3. 虽然 IOCP 是不分 TCP 和 UDP 的,但 IOCP 通常不用在 UDP 服务端上。原因也很简单,UDP 服务端总共就需要一个套接字,但 TCP 每个连接都需要一个套接字。“绑定”在 UDP 上 IOCP 根本没有 IOCP 的感觉。:)

« Previous PageNext Page »