PHP回顧之流

轉載請註明文章出處: https://tlanyan.me/php-review...

PHP回顧系列目錄

上篇 「PHP回顧之IO」 提到讀取文件、網絡通訊等操做,本質上是與 「流(stream)」 打交道。流機制是許多編程語言的重要機制,程序經過流可自由操做文件、內存、網絡等設備的數據。php

本文先簡要跟蹤PHP底層流的原理,再回到用戶態中流的使用。html

底層流

咱們知道PHP中的fopen函數能夠打開本地文件、URL等並返回一個句柄,freadfwrite函數能對資源句柄進行讀寫,fclose用於關閉資源。PHP如何作到使用一致API對不一樣數據源進行操做?答案是PHP引入了「流」的概念,在底層對操做進行抽象,帶來的好處是上層可用同一套API。web

爲了理解PHP中的流,咱們先追蹤PHP中fopen函數調用過程。PHP的底層用C實現,閱讀文中的代碼須要必定的C語言基礎。若是不熟悉C語言,關注其思路便可。數據庫

用戶態的fopen函數定義在ext/standard/file.c文件中,函數體以下:編程

PHP_NAMED_FUNCTION(php_if_fopen)
{
    // 一些初始化代碼
    context = php_stream_context_from_zval(zcontext, 0);
    stream = php_stream_open_wrapper_ex(filename, mode, (use_include_path ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);

    if (stream == NULL) {
        RETURN_FALSE;
    }
    php_stream_to_zval(stream, return_value);
}

PHP_NAMED_FUNCTION(php_if_fopen)定義PHP中的fopen函數(區別C中的fopen),有拓展開發基礎的應當對這種寫法熟悉。略過初始化等可有可無的代碼,fopen主要工做是獲取流對象(stream)並轉成PHP值類型(zval)返回。安全

流對象由php_stream_open_wrapper_ex函數返回,該函數位於main/php_streams.h中,是定義在main/streams/streams.c_php_stream_open_wrapper_ex的別名:cookie

PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mode, int options,
        zend_string **opened_path, php_stream_context *context STREAMS_DC)
{
    // 初始化代碼
    wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options);
    if (options & STREAM_USE_URL && (!wrapper || !wrapper->is_url)) {
        php_error_docref(NULL, E_WARNING, "This function may only be used against URLs");
        if (resolved_path) {
            zend_string_release(resolved_path);
        }
        return NULL;
    }

    if (wrapper) {
        if (!wrapper->wops->stream_opener) {
            php_stream_wrapper_log_error(wrapper, options ^ REPORT_ERRORS,
                    "wrapper does not support stream open");
        } else {
            stream = wrapper->wops->stream_opener(wrapper,
                path_to_open, mode, options ^ REPORT_ERRORS,
                opened_path, context STREAMS_REL_CC);
        }

    }
    // stream檢測等代碼
}

_php_stream_open_wrapper_ex函數的工做主要有兩點:1. 調用php_stream_locate_url_wrapper函數獲取協議包裝器(wrapper);2. 調用包裝器打開資源並返回流對象。網絡

接着看同一文件內獲取包裝器的函數php_stream_locate_url_wrappersession

PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const char **path_for_open, int options)
{
    // 一些初始化代碼
    for (p = path; isalnum((int)*p) || *p == '+' || *p == '-' || *p == '.'; p++) {
        n++;
    }

    if ((*p == ':') && (n > 1) && (!strncmp("//", p+1, 2) || (n == 4 && !memcmp("data:", path, 5)))) {
        protocol = path;
    }

    if (protocol) {
        if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, protocol, n))) {
            char *tmp = estrndup(protocol, n);

            php_strtolower(tmp, n);
            if (NULL == (wrapper = zend_hash_str_find_ptr(wrapper_hash, tmp, n))) {
                char wrapper_name[32];

                if (n >= sizeof(wrapper_name)) {
                    n = sizeof(wrapper_name) - 1;
                }
                PHP_STRLCPY(wrapper_name, protocol, sizeof(wrapper_name), n);

                php_error_docref(NULL, E_WARNING, "Unable to find the wrapper \"%s\" - did you forget to enable it when you configured PHP?", wrapper_name);

                wrapper = NULL;
                protocol = NULL;
            }
            efree(tmp);
        }
    }
    /* TODO: curl based streams probably support file:// properly */
    if (!protocol || !strncasecmp(protocol, "file", n)) {
        /* fall back on regular file access */
        php_stream_wrapper *plain_files_wrapper = (php_stream_wrapper*)&php_plain_files_wrapper;

        // 檢測代碼

        return plain_files_wrapper;
    }
    // 檢測遠程文件等
    return wrapper;
}

php_stream_locate_url_wrapper中,咱們終於知道fopen支持本地文件、HTTP/FTP、php://等多種數據源的奧祕:函數先查找路徑是否以「http://」、"ftp://"相似協議開頭,有則從註冊的包裝器列表中查找對應包裝器;不以協議開頭則回退到本地文件模式(php_plain_files_wrapper);fopen返回的流對象由包裝器打開。app

追蹤以上代碼,fopen的奧祕已經暴露無遺,但有兩個關鍵點:1. 流對象(php_stream)是什麼?2. 包裝器(php_stream_wrapper)是什麼?

內核開發者在源碼的README.STREAMS文件中解釋使用流的緣由:讓拓展開發者能像普通文件同樣操做數據。爲達到這個目的,流操做的資源都是php_stream對象。統一好資源接口後,PHP還定義了與文件操做對應的一套流函數:

<img src="https://tlanyan.me/wp-content...; alt="" width="665" height="373" class="aligncenter size-large wp-image-2675" />

流函數的第一個參數老是php_stream對象,例如與fread對應的php_stream_read函數定義爲:PHPAPI size_t php_stream_read(php_stream * stream, char * buf, size_t count)

流操做的支持和具體操做由包裝器決定(流包裝器實際會調用php_stream中ops成員的具體函數,這些函數在包裝器打開流時被正確的賦值)。一樣是讀取數據(fread),從文件中讀和從內存中讀作法不一樣。另外有些操做對某些流不適用。例如http協議支持fread,但不支持fwrite;普通文件能夠其大小,但ssh2://協議的數據大小不可知(stat函數不可用)。內置的協議包裝器列表和可用操做可參考官方文檔中的「支持的協議和包裝器」。

更多關於底層流的操做可參考官方文檔中開發者的「」章節,本文再也不深刻。

用戶態流

讓咱們回到PHP應用層面,即用戶態中的流。PHP的官方手冊有專門講解用戶態流的章節,並提供一系列以stream開頭的函數。因爲fread/fputs等函數已經包含常見的流操做,stream開頭的函數主要分爲三類:輔助的過濾器filter和上下文context,包裝器以及socket編程。網絡編程將在後續的文章中講解,咱們先關注包裝器。

開發者能夠註冊流包裝器實現自定義協議,經過協議才能正常解析流的數據。好比咱們爲下面的小姐姐實現一個專屬的協議secret://

圖片描述

class SecretStream {
    private $position;
    private $file;
    private $cipher = "aes-256-cbc";
    private $key = "little-sister";

    function stream_open($path, $mode, $options, &$opened_path)
    {
        $info = parse_url($path);
        $this->file = fopen($info["host"], $mode);
        $this->position = 0;
        return true;
    }

    function stream_read($count)
    {
        $line = fgets($this->file);
        $text = openssl_decrypt(base64_decode($line), $this->cipher, $this->key);
        $this->position += strlen($text);
        return $text;
    }

    function stream_write($data)
    {
        $raw = @openssl_encrypt($data, $this->cipher, $this->key);
        $base64 = base64_encode($raw);
        fwrite($this->file, $base64 . PHP_EOL);
        $this->position += strlen($data);
        return strlen($data);
    }

    function stream_tell()
    {
        return $this->position;
    }

    function stream_eof()
    {
        return feof($this->file);
    }

    function stream_close()
    {
        fclose($this->file);
    }
}

使用自定義協議先要註冊,而後就能夠正常使用了:

// 先註冊自定義協議
stream_wrapper_register("secret", "SecretStream")
    or die("Failed to register protocol");

// 寫數據
$fp = fopen("secret://Akari", "w+");
fwrite($fp, "IPZ-985\n");
fwrite($fp, "IPX-021\n");
fwrite($fp, "IPZ-933\n");
fclose($fp);

// 因爲協議未實現seek功能,不能經過rewind讓文件指針到頭部,須要從新打開
$fp = fopen("secret://Akari", "r");
while (!feof($fp)) {
    echo fgets($fp);
}
fclose($fp);

經過簡單的代碼,咱們安全的存儲了小姐姐的數據,也守護了小姐姐的祕密。其餘人即便獲取到文件內容,不經過咱們的協議打開也很難知道具體內容。有沒有感受很不錯?小姐姐和你比心哦~

圖片描述

總結

本文先回顧了PHP流底層的細節,再回到應用層中流的使用,並給出了一個簡單的流包裝器示例(例子簡單,可用流章節中的php_user_filter來實現)。有興趣的讀者能夠爲下面的小姐姐建立自定義的協議,示例內容能夠是:SSNI-05六、SSNI-01四、SNIS-662等。

圖片描述

本文感謝「微通廣州」的贊助。

圖片描述

感謝閱讀,歡迎指正!

參考

  1. http://php.net/manual/en/book...
  2. http://php.net/manual/en/inte...
  3. https://blog.csdn.net/lgg201/...
  4. https://post.zz173.com/course...

感謝閱讀,敬請指正!

相關文章
相關標籤/搜索