Access flow
All file I/O processing in PHP user space is through php Processed by the PHP stream wrapper introduced in 4.3. Internally, the extension code can choose to use stdio or posix file processing to communicate with the local file system or Berkeley domain socket, or it can call the same API as user space stream I/O .
Overview of streams
Generally, direct file descriptors are less expensive than calling the stream wrapper layer CPU and memory; however, this puts all the work of implementing a specific protocol on you as the extension developer. By hooking into the stream wrapper layer, your extension code can transparently use the various built-in streams Wrappers, such as HTTP, FTP, and their SSL counterparts, as well as gzip and bzip2 compression wrappers. By including specific PEAR or PECL modules, your code can also access other protocols, such as SSH2, WebDav, and even Gopher!
This chapter will introduce the basic API that works internally based on streams. Later in Chapter 16 "Interesting Streams", we will see things like applying filters, using context options and parameters, etc. Advanced concepts.
Opening a stream
Although a unified API, it actually depends on the required Types of streams, there are four different paths to open a stream. From a user space perspective, these four different categories are as follows (the function list only represents examples, not a complete list):
<?php /* fopen包装 * 操作文件/URI方式指定远程文件类资源 */ $fp = fopen($url, $mode); $data = file_get_contents($url); file_put_contents($url, $data); $lines = file($url); /* 传输 * 基于套接字的顺序I/O */ $fp = fsockopen($host, $port); $fp = stream_socket_client($uri); $fp = stream_socket_server($uri, $options); /* 目录流 */ $dir = opendir($url); $files = scandir($url); $obj = dir($url); /* "特殊"的流 */ $fp = tmpfile(); $fp = popen($cmd); proc_open($cmd, $pipes); ?>
No matter what type of stream you open, they are all stored in a common structure php_stream.
fopen packaging
We first start by implementing the fopen() function. Now you should be familiar with creating extension skeletons. If not, please go back to Chapter 5 "Your First Extension" for review Take a look, here is the fopen() function we implemented:
PHP_FUNCTION(sample5_fopen) { php_stream *stream; char *path, *mode; int path_len, mode_len; int options = ENFORCE_SAFE_MODE | REPORT_ERRORS; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &path, &path_len, &mode, &mode_len) == FAILURE) { return; } stream = php_stream_open_wrapper(path, mode, options, NULL); if (!stream) { RETURN_FALSE; } php_stream_to_zval(stream, return_value); }
php_stream_open_wrapper()’s purpose should be to completely bypass the bottom layer. path specifies the file name or URL to be read and written, and the read and write behavior Depends on the value of mode.
options is a collection of flag values in the bit field, here it is set to a set of fixed values as introduced below:
USE_PATHApply the include_path in the php.ini file to the relative path
. The built-in function fopen() is specified in the third This option will be set when a parameter is TRUE
.
STREAM_USE_URLSet this After option, only the remote URL will be opened. For
php://, file://, zlib://, bzip2:// these The URL wrappers do not
consider them to be remote URLs.
ENFORCE_SAFE_MODEalthough This constant is named this, but actually sets this option
is just a mandatory check to enable safe mode (
safe_mode directive in the php.ini file) . Failure to set this
option will cause the safe_mode check to be skipped (regardless of the INI setting
How to set safe_mode in)
REPORT_ERRORSWhen an error is encountered during opening of the specified resource, if set
If this option is passed, an error report will be generated.
STREAM_MUST_SEEKFor some streams, such as sockets, this is not possible seek (random
access); This type of file handle can only be used under certain circumstances
seek. If the calling scope specifies this option, and the wrapper
will refuse to open it if it detects that it cannot guarantee that it can seek.
## streams.
STREAM_WILL_CASTThe stream can be converted to stdio if the calling scope requires it Or
posix file descriptor, you should pass this option to the open_wrapper function
## , to ensure that the I/O operation fails before it occurs
STREAM_ONLY_GET_HEADERSIdentifies that only metadata needs to be requested from the stream. In fact this is used for
http wrapper, obtains the http_response_headers global variable
without actually crawling the remote file Content.
STREAM_DISABLE_OPEN_BASEDIRSimilar to the safe_mode check, if this option is not set, it will check
INI sets open_basedir. If you specify this option, you can
Bypass this default check
STREAM_OPEN_PERSISTENT Inform the stream wrapping layer that all internally allocated space is allocated persistently
, and the associated resources are registered in the persistence list.
IGNORE_PATHIf not specified, the default include path is searched. Most URLs
wrappers All browsers ignore this option.
IGNORE_URL提供这个选项时, 流包装层只打开本地文件. 所
有的is_url包装器都将被忽略.
最后的NULL参数是char **类型, 它最初是用来设置匹配路径, 如果path指向普通文件URL, 则去掉file://部分, 保留直接的文件路径用于传统的文件名操作. 这个参数仅仅是以前引擎内部处理使用的.
此外, 还有php_stream_open_wrapper()的一个扩展版本:
php_stream *php_stream_open_wrapper_ex(char *path, char *mode, int options, char **opened_path, php_stream_context *context);
最后一个参数context允许附加的控制, 并可以得到包装器内的通知. 你将在第16章看到这个参数的细节.
传输层包装
尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子.
从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现:
PHP_FUNCTION(sample5_fsockopen) { php_stream *stream; char *host, *transport, *errstr = NULL; int host_len, transport_len, implicit_tcp = 1, errcode = 0; long port = 0; int options = ENFORCE_SAFE_MODE; int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &host, &host_len, &port) == FAILURE) { return; } if (port) { int implicit_tcp = 1; if (strstr(host, "://")) { /* A protocol was specified, * no need to fall back on tcp:// */ implicit_tcp = 0; } transport_len = spprintf(&transport, 0, "%s%s:%d", implicit_tcp ? "tcp://" : "", host, port); } else { /* When port isn't specified * we can safely assume that a protocol was * (e.g. unix:// or udg://) */ transport = host; transport_len = host_len; } stream = php_stream_xport_create(transport, transport_len, options, flags, NULL, NULL, NULL, &errstr, &errcode); if (transport != host) { efree(transport); } if (errstr) { php_error_docref(NULL TSRMLS_CC, E_WARNING, "[%d] %s", errcode, errstr); efree(errstr); } if (!stream) { RETURN_FALSE; } php_stream_to_zval(stream, return_value); }
这个函数的基础构造和前面的fopen示例是一样的. 不同在于host和端口号使用不同的参数指定, 接着为了给出一个传输流URL就必须将它们合并到一起. 在产生了一个有意义的"路径:后, 将它传递给php_stream_xport_create()函数, 方式和fopen()使用的php_stream_open_wrapper()API一样. php_stream_xport_create()的原型如下:
php_stream *php_stream_xport_create(char *xport, int xport_len, int options, int flags, const char *persistent_id, struct timeval *timeout, php_stream_context *context, char **errstr, int *errcode);
每个参数的含义如下:
xport基于URI的传输描述符. 对于基于inet的套接字流, 它可以是
tcp://127.0.0.1:80, udp://10.0.0.1:53, ssl://169.254.13.24:445等. 此
外, UNIX域传输协议unix:///path/to/socket,
udg:///path/to/dgramsocket等都是合法的. xport_len指定了xport的长
度, 因此xport是二进制安全的.
options这个值是由前面php_stream_open_wrapper()中介绍的选项通过按位
或组成的值.
flags由STREAM_XPORT_CLIENT或STREAM_XPORT_SERVER之一
与下面另外一张表中将列出的STREAM_XPORT_*常量通过按位或
组合得到的值.
persistent_idIf the requested transport stream needs to be persisted between requests, the calling scope can provide a
key Name describing the connection. Specifying this value as NULL creates a non-persistent connection; specifying a unique string value will attempt to first look up from the persistence pool. There is an existing transport stream, or if there is no
create a new persistence stream when found.
timeout How long to attempt a connection before a timeout returns and fails. Passing NULL for this value causes
to use the default specified in php.ini Timeout value. This parameter has no meaning for server-side transport streams.
errstrIf creating, connecting, binding or listening on the selected socket An error occurs, the char * reference value passed here
will be set to a string describing the cause of the error. errstr is initially
should initially point to NULL; if it is set when returning, the calling scope has
Responsibility to release the memory related to this string.
errcodeThe numerical error code corresponding to the error message returned through errstr.
The flags parameter of php_stream_xport_create() uses the STREAM_XPORT_* family of constants defined as follows:
STREAM_XPORT_CLIENTThe local end will establish a connection with the remote resource through the transport layer . This
## tag is usually used in conjunction with STREAM_XPORT_CONNECT or
#STREAM_XPORT_CONNECT_ASYNC
##Use.STREAM_XPORT_SERVER
The local end will accept the connection through the transport layer. This flag is usually
## Used with STREAM_XPORT_BIND and##STREAM_XPORT_LISTEN.
STREAM_XPORT_CONNECTUsed to illustrate that establishing a remote resource connection is a part of the creation of the transport stream
. When creating the client transport stream It is legal to omit this flag
, but doing so requires manual invocation
# #php_stream_xport_connect().
STREAM_XPORT_CONNECT_ASYNC尝试连接到远程资源, 但不阻塞.
STREAM_XPORT_BIND将传输流绑定到本地资源. 用在服务端传输流时,
这将使得accept连接的传输流准备端口, 路径或
特定的端点标识符等信息.
STREAM_XPORT_LISTEN在已绑定的传输流端点上监听到来的连接. 这通
常用于基于流的传输协议, 比如: tcp://, ssl://,
unix://.
目录访问
fopen包装器支持目录访问, 比如file://和ftp://, 还有第三种流打开函数也可以用于目录访问, 下面是对opendir()的实现:
PHP_FUNCTION(sample5_opendir) { php_stream *stream; char *path; int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &path, &path_len) == FAILURE) { return; } stream = php_stream_opendir(path, options, NULL); if (!stream) { RETURN_FALSE; } php_stream_to_zval(stream, return_value); }
同样的, 也可以为某个特定目录打开一个流, 比如本地文件系统的目录名或支持目录访问的URL格式资源. 这里我们又看到了options参数, 它和原来的含义一样, 第三个参数NULL原型是php_stream_context类型.
在目录流打开后, 和文件以及传输流一样, 返回给用户空间.
特殊流
还有一些特殊类型的流不能归类到fopen/transport/directory中. 它们中每一个都有自己独有的API:
php_stream *php_stream_fopen_tmpfile(void); php_stream *php_stream_fopen_temporary_file(const char *dir, const char *pfx, char **opened_path);
创建一个可seek的缓冲区流用于读写. 在关闭时, 这个流使用的所有临时资源, 包括所有的缓冲区(无论是在内存还是磁盘), 都将被释放. 使用这一组API中的后一个函数, 允许临时文件被以特定的格式命名放到指定路径. 这些内部API调用被用户空间的tmpfile()函数隐藏.
php_stream *php_stream_fopen_from_fd(int fd, const char *mode, const char *persistent_id); php_stream *php_stream_fopen_from_file(FILE *file, const char *mode); php_stream *php_stream_fopen_from_pipe(FILE *file, const char *mode);
这3个API方法接受已经打开的FILE *资源或文件描述符ID, 使用流API的某种操作包装. fd格式的接口不会搜索匹配你前面看到过的fopen函数打开的资源, 但是它会注册持久化的资源, 后续的fopen可以使用到这个持久化资源.
访问流
在你打开一个流之后, 就可以在它上面执行I/O操作了. 使用那种协议包装API创建了流并不重要, 它们都使用相同的访问API.
读
流的读写可以使用下面的API函数组合完成, 它们多数都是遵循POSIX I/O中对应的API规范的:
int php_stream_getc(php_stream *stream);
从数据流中接收一个字符. 如果流上再没有数据, 则返回EOF.
size_t php_stream_read(php_stream *stream, char *buf, size_t count);
从指定流中读取指定字节的数据. buf必须预分配至少count字节的内存空间. 这个函数将返回从数据流实际读到缓冲区中的数据字节数.
php_stream_read()不同于其他的流读取函数. 如果使用的流不是普通文件流, 哪怕数据流中有超过请求字节数的数据, 并且当前也可以返回, 它也只会调用过一次底层流实现的read函数. 这是为了兼容基于包(比如UDP)的协议的这种做法.
char *php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, size_t *returned_len); char *php_stream_gets(php_stream *stream, char *buf, size_t maxlen);
这两个函数从stream中读取最多maxlen个字符, 直到碰到换行符或流结束. buf可以是一个指向预分配的至少maxlen字节的内存空间的指针, 也可以是NULL, 当它是NULL时, 则会自动的创建一个动态大小的缓冲区, 用从流中实际读出的数据填充, 成功后函数返回指向缓冲区的指针, 失败则返回NULL. 如果returned_len传递了非NULL值, 则在返回时它将被设置为实际从流中读取的字节数.
char *php_stream_get_record(php_stream *stream, size_t maxlen, size_t *returned_len, char *delim, size_t delim_len TSRMLS_DC);
和php_stream_get_line()类似, 这个函数将读取最多maxlen, 或到达EOF/行结束第一次出现的位置. 但是它也有和php_stream_get_line()的不同指出, 这个函数允许指定任意的停止读取标记.
读取目录项
从php流中读取目录项和上面从普通文件中读取普通数据相同. 这些数据放到了固定大小的dirents块中. 内部的php_stream_dirent结构体如下, 它与POSIX定义的dirent结构体一致:
typedef struct _php_stream_dirent { char d_name[MAXPATHLEN]; } php_stream_dirent;
实际上你可以直接使用php_stream_read()函数读取数据到这个结构体中:
{ struct dirent entry; if (php_stream_read(stream, (char*)&entry, sizeof(entry)) == sizeof(entry)) { /* 成功从目录流中读取到一项 */ php_printf("File: %s\n", entry.d_name); } }
由于从目录流中读取是很常见的操作, php流包装层暴露了一个API, 它将记录大小的检查和类型转换处理封装到了一次调用中:
php_stream_dirent *php_stream_readdir(php_stream *dirstream, php_stream_dirent *entry);
如果成功读取到目录项, 则传入的entry指针将被返回, 否则返回NULL标识错误. 使用这个为目录流特殊构建的函数而不是直接从目录流读取非常重要, 这样做未来流API改变时就不至于和你的代码冲突.
写
和读类似, 向流中写数据只需要传递一个缓冲区和缓冲区长度给流.
size_t php_stream_write(php_stream *stream, char *buf, size_t count); size_t php_stream_write_string(php_stream *stream, char *stf);
write_string的版本实际上是一个提供便利的宏, 它允许写一个NULL终止的字符串,而不用显式的提供长度. 返回的是实际写到流中的字节数. 要特别小心的是尝试写大数据的时候可能导致流阻塞, 比如套接字流, 而如果流被标记为非阻塞, 则实际写入的数据量可能会小于传递给函数的期望大小.
int php_stream_putc(php_stream *stream, int c); int php_stream_puts(php_string *stream, char *buf);
还有一种选择是, 使用php_stream_putc()和php_stream_puts()写入一个字符或一个字符串到流中. 要注意, php_stream_puts()不同于php_stream_write_string(), 虽然它们的原型看起来是一样的, 但是php_stream_puts()会在写出buf中的数据后自动的追加一个换行符.
size_t php_stream_printf(php_stream *stream TSRMLS_DC, const char *format, ...);
功能和格式上都类似于fprintf(), 这个API调用允许在写的同时构造字符串而不用去创建临时缓冲区构造数据. 这里我们能够看到的一个明显的不同是它需要TSRMLS_CC宏来保证线程安全.
随机访问, 查看文件偏移量以及缓存的flush
基于文件的流, 以及另外几种流是可以随机访问的. 也就是说, 在流的一个位置读取了一些数据之后, 文件指针可以向前或向后移动, 以非线性顺序读取其他部分.
如果你的流应用代码预测到底层的流支持随机访问, 在打开的时候就应该传递STREAM_MUST_SEEK选项. 对于那些原本就可随机访问的流来说, 这通常不会有什么影响, 因为流本身就是可随机访问的. 而对于那些原本不可随机访问的流, 比如网络I/O或线性访问文件比如FIFO管道, 这个暗示可以让调用程序有机会在流的数据被消耗掉之前, 优雅的失败.
在可随机访问的流资源上工作时, 下面的函数可用来将文件指针移动到任意位置:
int php_stream_seek(php_stream *stream, off_t offset, int whence); int php_stream_rewind(php_stream *stream);
offset是相对于whence表示的流位置的偏移字节数, whence的可选值及含义如下:
SEEK_SEToffset相对于文件开始位置. php_stream_rewind()API调用实际上是一个宏,
展开后是php_stream_seek(stream, 0, SEEK_SET), 表示移动到文件开始
位置偏移0字节处. 当使用SEEK_SET时, 如果offset传递负值被认为是错误
的, 将会导致未定义行为. 指定的位置超过流的末尾也是未定义的, 不过结果
通常是一个错误或文件被扩大以满足指定的偏移量.
SEEK_CURoffset相对于文件的当前偏移量. 调用php_stream_seek(steram, offset,
SEEK_CUR)一般来说等价于php_stream_seek(stream, php_stream_tell()
+ offset, SEEK_SET);
SEEK_ENDoffset是相对于当前的EOF位置的. 负值的offset表示在EOF之前的位置, 正
值和SEEK_SET中描述的是相同的语义, 可能在某些流实现上可以工作.
int php_stream_rewinddir(php_stream *dirstream);
在目录流上随机访问时, 只有php_stream_rewinddir()函数可用. 使用php_stream_seek()函数将导致未定义行为. 所有的随机访问一族函数返回0标识成功或者-1标识失败.
off_t php_stream_tell(php_stream *stream);
如你之前所见, php_stream_tell()将返回当前的文件偏移量.
int php_stream_flush(php_stream *stream);
调用flush()函数将强制将流过滤器此类内部缓冲区中的数据输出到最终的资源中. 在流被关闭时, flush()函数将自动调用, 并且大多数无过滤流资源虽然不进行任何内部缓冲, 但也需要flush. 显式的调用这个函数很少见, 并且通常也是不需要的.
int php_stream_stat(php_stream *stream, php_stream_statbuf *ssb);
调用php_stream_stat()可以获取到流实例的其他信息, 它的行为类似于fstat()函数. 实际上, php_stream_statbuf结构体现在仅包含一个元素: struct statbuf sb; 因此, php_stream_stat()调用可以如下面例子一样, 直接用传统的fstat()操作替代, 它只是将posix的stat操作翻译成流兼容的:
int php_sample4_fd_is_fifo(int fd) { struct statbuf sb; fstat(fd, &sb); return S_ISFIFO(sb.st_mode); } int php_sample4_stream_is_fifo(php_stream *stream) { php_stream_statbuf ssb; php_stream_stat(stream, &ssb); return S_ISFIFO(ssb.sb.st_mode); }
关闭
所有流的关闭都是通过php_stream_free()函数处理的, 它的原型如下:
int php_stream_free(php_stream *stream, int options);
这个函数中的options参数允许的值是PHP_STREAM_FREE_xxx一族常量的按位或的结果, 这一族常量定义如下(下面省略PHP_STREAM_FREE_前缀):
CALL_DTOR流实现的析构器应该被调用. 这里提供了一个时机对特定的流
进行显式释放.
RELEASE_STREAM释放为php_stream结构体分配的内存
PRESERVE_HANDLE指示流的析构器不要关闭它的底层描述符句柄
RSRC_DTOR流包装层内部管理资源列表的垃圾回收
PERSISTENT作用在持久化流上时, 它的行为将是永久的而不局限于当前请
求.
CLOSECALL_DTOR和RELEASE_STREAM的联合. 这是关闭
非持久化流的一般选项.
CLOSE_CASTEDCLOSE和PRESERVE_HANDLE的联合.
CLOSE_PERSISTENTCLOSE和PERSISTENT的联合. 这是永久关闭持久化流的一
般选项.
实际上, 你并不需要直接调用php_stream_free()函数. 而是在关闭流时使用下面两个宏的某个替代:
#define php_stream_close(stream) \ php_stream_free((stream), PHP_STREAM_FREE_CLOSE) #define php_stream_pclose(stream) \ php_stream_free((stream), PHP_STREAM_FREE_CLOSE_PERSISTENT)
通过zval交换流
因为流通常映射到zval上, 反之亦然, 因此提供了一组宏用来简化操作, 并统一编码(格式):
#define php_stream_to_zval(stream, pzval) \ ZVAL_RESOURCE((pzval), (stream)->rsrc_id);
要注意, 这里并没有调用ZEND_REGISTER_RESOURCE(). 这是因为当流打开的时候, 已经自动的注册为资源了, 这样就可以利用到引擎内建的垃圾回收和shutdown系统的优点. 使用这个宏而不是尝试手动的将流注册为新的资源ID是非常重要的; 这样做的最终结果是导致流被关闭两次以及引擎崩溃.
#define php_stream_from_zval(stream, ppzval) \ ZEND_FETCH_RESOURCE2((stream), php_stream*, (ppzval), \ -1, "stream", php_file_le_stream(), php_file_le_pstream()) #define php_stream_from_zval_no_verify(stream, ppzval) \ (stream) = (php_stream*)zend_fetch_resource((ppzval) \ TSRMLS_CC, -1, "stream", NULL, 2, \ php_file_le_stream(), php_file_le_pstream())
从传入的zval *中取回php_stream *有一个类似的宏. 可以看出, 这个宏只是对资源获取函数(第9章"资源数据类型")的一个简单封装. 请回顾ZEND_FETCH_RESOURCE2()宏, 第一个宏php_stream_from_zval()就是对它的包装, 如果资源类型不匹配, 它将抛出一个警告并尝试从函数实现中返回. 如果你只是想从传入的zval *中获取一个php_stream *, 而不希望有自动的错误处理, 就需要使用php_stream_from_zval_no_verify()并且需要手动的检查结果值.
静态资源操作
一个基于流的原子操作并不需要实际的实例. 下面这些API仅仅使用URL执行这样的操作:
int php_stream_stat_path(char *path, php_stream_statbuf *ssb);
和前面的php_stream_stat()类似, 这个函数提供了一个对POSIX的stat()函数协议依赖的包装. 要注意, 并不是所有的协议都支持URL记法, 并且即便支持也可能不能报告出statbuf结构体中的所有成员值. 一定要检查php_stream_stat_path()失败时的返回值, 0标识成功, 要知道, 不支持的元素返回时其值将是默认的0.
int php_stream_stat_path_ex(char *path, int flags, php_stream_statbuf *ssb, php_stream_context *context);
这个php_stream_url_stat()的扩展版本允许传递另外两个参数. 第一个是flags, 它的值可以是下面的PHP_STERAM_URL_STAT_*(下面省略PHP_STREAM_URL_STAT_前缀)一族常量的按位或的结果. 还有一个是context参数, 它在其他的一些流函数中也有出现, 我们将在第16章去详细学习.
LINK原始的php_stream_stat_path()对于符号链接或目录将会进行解析直到碰到
协议定义的结束资源. 传递PHP_STREAM_URL_STAT_LINK标记将导致
php_stream_stat_path()返回请求资源的信息而不会进行符号链接的解析.
(译注: 我们可以这样理解, 没有这个标记, 底层使用stat(), 如果有这个标记,
底层使用lstat(), 关于stat()和lstat()的区别, 请查看*nix手册)
QUIET默认情况下, 如果在执行URL的stat操作过程中碰到错误, 包括文件未找到错
误, 都将通过php的错误处理机制触发. 传递QUIET标记可以使得
php_stream_stat_path()返回而不报告错误.
int php_stream_mkdir(char *path, int mode, int options, php_stream_context *context); int php_stream_rmdir(char *path, int options, php_stream_context *context);
创建和删除目录也会如你期望的工作. 这里的options参数和前面的php_stream_open_wrapper()函数的同名参数含义一致. 对于php_stream_mkdir(), 还有一个参数mode用于指定一个八进制的值表明读写执行权限.
小结
本章中你接触了一些基于流的I/O的内部表象. 下一章将演示做呢样实现自己的协议包装, 甚至是定义自己的流类型.
以上就是[翻译][php扩展开发和嵌入式]第14章-php中流的访问的内容,更多相关内容请关注PHP中文网(www.php.cn)!