14.1 流的概览

    本章将介绍内部基于流工作的基础API. 后面到第16章”有趣的流”中, 我们将看到诸如应用过滤器, 使用上下文选项和参数等高级概念.

    尽管是一个统一的API, 但实际上依赖于所需的流的类型, 有四种不同的路径去打开一个流. 从用户空间角度来看, 这四种不同的类别如下(函数列表只代表示例, 不是完整列表):

    无论你打开的是什么类型的流, 它们都存储在一个公共的结构体php_stream中.

    fopen包装

    我们首先从实现fopen()函数开始. 现在你应该已经对创建扩展骨架很熟悉了, 如果还不熟悉, 请回到第5章”你的第一个扩展”复习一下, 下面是我们实现的fopen()函数:

    1. {
    2. php_stream *stream;
    3. char *path, *mode;
    4. int path_len, mode_len;
    5. int options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
    6. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",
    7. &path, &path_len, &mode, &mode_len) == FAILURE) {
    8. return;
    9. }
    10. stream = php_stream_open_wrapper(path, mode, options, NULL);
    11. if (!stream) {
    12. RETURN_FALSE;
    13. }
    14. php_stream_to_zval(stream, return_value);
    15. }

    php_stream_open_wrapper()的目的应该是完全绕过底层. path指定要读写文件名或URL, 读写行为依赖于mode的值.

    最后的NULL参数是char **类型, 它最初是用来设置匹配路径, 如果path指向普通文件URL, 则去掉file://部分, 保留直接的文件路径用于传统的文件名操作. 这个参数仅仅是以前引擎内部处理使用的.

    此外, 还有php_stream_open_wrapper()的一个扩展版本:

    最后一个参数context允许附加的控制, 并可以得到包装器内的通知. 你将在第16章看到这个参数的细节.

    尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子.

    从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现:

    1. PHP_FUNCTION(sample5_fsockopen)
    2. php_stream *stream;
    3. char *host, *transport, *errstr = NULL;
    4. int host_len, transport_len, implicit_tcp = 1, errcode = 0;
    5. long port =
    6. int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
    7. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l",
    8. &host, &host_len, &port) == FAILURE) {
    9. }
    10. if (port) {
    11. int implicit_tcp = 1;
    12. if (strstr(host, "://")) {
    13. /* A protocol was specified,
    14. * no need to fall back on tcp:// */
    15. implicit_tcp = 0;
    16. }
    17. transport_len = spprintf(&transport, 0, "%s%s:%d",
    18. implicit_tcp ? "tcp://" : "", host, port);
    19. } else {
    20. /* When port isn't specified
    21. * we can safely assume that a protocol was
    22. * (e.g. unix:// or udg://) */
    23. transport = host;
    24. transport_len = host_len;
    25. }
    26. stream = php_stream_xport_create(transport, transport_len,
    27. options, flags,
    28. NULL, NULL, NULL, &errstr, &errcode);
    29. if (transport != host) {
    30. efree(transport);
    31. }
    32. if (errstr) {
    33. efree(errstr);
    34. }
    35. if (!stream) {
    36. RETURN_FALSE;
    37. }
    38. php_stream_to_zval(stream, return_value);
    39. }

    每个参数的含义如下:

    目录访问

    fopen包装器支持目录访问, 比如file://和ftp://, 还有第三种流打开函数也可以用于目录访问, 下面是对opendir()的实现:

    1. PHP_FUNCTION(sample5_opendir)
    2. {
    3. php_stream *stream;
    4. char *path;
    5. int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
    6. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",
    7. &path, &path_len) == FAILURE) {
    8. return;
    9. }
    10. stream = php_stream_opendir(path, options, NULL);
    11. if (!stream) {
    12. RETURN_FALSE;
    13. }
    14. php_stream_to_zval(stream, return_value);
    15. }

    同样的, 也可以为某个特定目录打开一个流, 比如本地文件系统的目录名或支持目录访问的URL格式资源. 这里我们又看到了options参数, 它和原来的含义一样, 第三个参数NULL原型是php_stream_context类型.

    在目录流打开后, 和文件以及传输流一样, 返回给用户空间.

    还有一些特殊类型的流不能归类到fopen/transport/directory中. 它们中每一个都有自己独有的API:

    1. php_stream *php_stream_fopen_from_fd(int fd, const char *mode, const char *persistent_id);
    2. php_stream *php_stream_fopen_from_file(FILE *file, const char *mode);
    3. php_stream *php_stream_fopen_from_pipe(FILE *file, const char *mode);

    这3个API方法接受已经打开的FILE *资源或文件描述符ID, 使用流API的某种操作包装. fd格式的接口不会搜索匹配你前面看到过的fopen函数打开的资源, 但是它会注册持久化的资源, 后续的fopen可以使用到这个持久化资源.