流的概念
概述
PHP 流(Streams)是 PHP 中处理数据的统一抽象层。无论是文件、网络连接、压缩数据还是进程管道,都可以通过流的接口来读写。流由三层组成:流本身、流包装器(Wrappers)和流上下文(Context)。理解流模型有助于掌握 PHP 的高级 I/O 操作。
适用场景
- 统一的文件/网络 I/O 操作
- 自定义协议处理
- 流过滤器实现数据转换
- 高效处理大文件和网络数据
基础概念
流的三层架构
┌─────────────────────────┐
│ 流过滤器 (Filters) │ 数据转换层(如 base64 编码)
├─────────────────────────┤
│ 流传输 (Transport) │ 实际的 I/O 操作
├─────────────────────────┤
│ 流包装器 (Wrappers) │ 协议/路径解析(如 file://, http://)
├─────────────────────────┤
│ 流上下文 (Context) │ 配置选项(超时、头部等)
└─────────────────────────┘核心流函数
| 函数 | 功能 |
|---|---|
fopen() | 打开流 |
fread() / fgets() | 从流读取 |
fwrite() / fputs() | 写入流 |
fclose() | 关闭流 |
stream_get_contents() | 读取剩余内容 |
stream_get_line() | 按行读取 |
stream_select() | 多路复用 I/O |
stream_filter_append() | 添加过滤器 |
stream_context_create() | 创建上下文 |
统一接口
所有流使用相同的函数接口(fopen/fread/fwrite),无需关心底层是文件、HTTP 还是其他协议。
语法与代码示例
基本流操作
php
<?php
// 文件流
$handle = fopen('/tmp/data.txt', 'r');
$content = stream_get_contents($handle);
fclose($handle);
// HTTP 流
$handle = fopen('https://example.com/api/data', 'r');
$response = stream_get_contents($handle);
fclose($handle);
// php:// 流
$stdin = fopen('php://stdin', 'r');
$input = stream_get_line($stdin, 1024, "\n");
fclose($stdin);
// 内存流
$memory = fopen('php://memory', 'r+');
fwrite($memory, 'Hello, Stream!');
rewind($memory);
echo stream_get_contents($memory); // Hello, Stream!
fclose($memory);stream_get_contents
php
<?php
// 读取指定长度
$handle = fopen('/tmp/data.txt', 'r');
$content = stream_get_contents($handle, 1024); // 最多 1024 字节
fclose($handle);
// 从偏移位置读取
$handle = fopen('/tmp/data.txt', 'r');
$content = stream_get_contents($handle, -1, 100); // 从第 100 字节开始读到末尾
fclose($handle);
// 读取到末尾
$handle = fopen('/tmp/data.txt', 'r');
// 先读了一部分
$partial = fread($handle, 100);
// 读取剩余
$rest = stream_get_contents($handle);
fclose($handle);stream_select 多路复用
php
<?php
// stream_select 同时监听多个流
$read = [];
$write = [];
$except = [];
// 添加要监听的流
$stream1 = fopen('http://example.com/api/1', 'r');
$stream2 = fopen('http://example.com/api/2', 'r');
$stream3 = fopen('php://stdin', 'r');
$read[] = $stream1;
$read[] = $stream2;
$read[] = $stream3;
// 等待最多 5 秒
$changed = stream_select($read, $write, $except, 5);
if ($changed === false) {
echo "select 失败\n";
} elseif ($changed === 0) {
echo "超时\n";
} else {
foreach ($read as $ready) {
$data = fread($ready, 4096);
if (strlen($data) === 0) {
fclose($ready);
} else {
echo "收到数据: " . substr($data, 0, 50) . "...\n";
}
}
}流的元信息
php
<?php
// stream_get_meta_data 获取流的元信息
$handle = fopen('https://example.com', 'r');
$meta = stream_get_meta_data($handle);
print_r($meta);
/*
[
'wrapper_data' => [...], // 包装器特定的数据/头部
'wrapper_type' => 'http', // 包装器类型
'stream_type' => 'tcp_socket/ssl', // 流类型
'mode' => 'r', // 访问模式
'unread_bytes' => 0, // 未读字节数
'seekable' => false, // 是否可定位
'eof' => false, // 是否到达末尾
'blocked' => true, // 是否阻塞
'uri' => 'https://example.com', // URI
]
*/
// 判断是否到达末尾
while (!feof($handle)) {
$data = fread($handle, 4096);
// 处理数据
}
fclose($handle);实战示例
简单 HTTP 客户端
php
<?php
declare(strict_types=1);
class SimpleHttpClient
{
public function get(string $url, array $headers = [], int $timeout = 30): string
{
$context = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => $timeout,
'header' => $this->buildHeaders($headers),
'ignore_errors' => true, // 不抛出 HTTP 错误
],
]);
$handle = fopen($url, 'r', false, $context);
if ($handle === false) {
throw new RuntimeException("无法打开 URL: {$url}");
}
// 获取响应头和状态码
$meta = stream_get_meta_data($handle);
$statusCode = $this->parseStatusCode($meta['wrapper_data'] ?? []);
$body = stream_get_contents($handle);
fclose($handle);
if ($statusCode >= 400) {
throw new RuntimeException("HTTP 错误: {$statusCode}");
}
return $body;
}
public function post(string $url, array $data, array $headers = [], int $timeout = 30): string
{
$body = http_build_query($data);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'timeout' => $timeout,
'header' => array_merge(
$this->buildHeaders($headers),
['Content-Type: application/x-www-form-urlencoded', 'Content-Length: ' . strlen($body)]
),
'content' => $body,
'ignore_errors' => true,
],
]);
$response = file_get_contents($url, false, $context);
if ($response === false) {
throw new RuntimeException("POST 请求失败: {$url}");
}
return $response;
}
private function buildHeaders(array $headers): array
{
return array_map(
fn(string $key, string $value) => "{$key}: {$value}",
array_keys($headers),
$headers
);
}
private function parseStatusCode(array $wrapperData): int
{
foreach ($wrapperData as $header) {
if (preg_match('/^HTTP\/\d\.\d\s+(\d+)/', $header, $matches)) {
return (int)$matches[1];
}
}
return 200;
}
}
// 使用
$client = new SimpleHttpClient();
$data = $client->get('https://api.example.com/users');
$result = $client->post('https://api.example.com/login', ['username' => 'test', 'password' => 'secret']);流式大文件下载
php
<?php
declare(strict_types=1);
function streamDownload(string $url, string $destPath, int $timeout = 60): void
{
$context = stream_context_create([
'http' => ['timeout' => $timeout, 'follow_location' => true],
]);
$source = fopen($url, 'rb', false, $context);
if ($source === false) {
throw new RuntimeException("无法打开远程文件: {$url}");
}
$dest = fopen($destPath, 'wb');
if ($dest === false) {
fclose($source);
throw new RuntimeException("无法创建本地文件: {$destPath}");
}
// 流式复制,不占用大内存
$totalBytes = stream_copy_to_stream($source, $dest);
fclose($source);
fclose($dest);
echo "下载完成: {$totalBytes} 字节\n";
}
// 使用
streamDownload('https://example.com/large-file.zip', '/tmp/download.zip');注意事项
流的超时处理
php
<?php
// 设置流的超时
$context = stream_context_create([
'http' => [
'timeout' => 10, // 连接 + 传输总超时
],
'socket' => [
'bindto' => '192.168.1.1:0', // 绑定本地 IP
],
]);
// 默认超时(php.ini default_socket_timeout)
echo ini_get('default_socket_timeout') . PHP_EOL;
// 运行时修改超时
$handle = fopen('https://slow-api.example.com', 'r');
stream_set_timeout($handle, 5); // 5 秒超时
$info = stream_get_meta_data($handle);
echo "Timed out: " . ($info['timed_out'] ? 'yes' : 'no') . PHP_EOL;流的阻塞与非阻塞
php
<?php
// 阻塞模式(默认)
$handle = fopen('https://example.com', 'r');
// fread 会等到数据到达才返回
// 非阻塞模式
stream_set_blocking($handle, false);
// fread 立即返回,可能返回空字符串最佳实践
1. 使用 stream_copy_to_stream 代替 file_get_contents
php
<?php
// 不好:大文件占用内存
$data = file_get_contents('http://example.com/large-file.zip');
file_put_contents('/tmp/file.zip', $data); // 内存翻倍
// 好:流式传输,内存友好
$source = fopen('http://example.com/large-file.zip', 'rb');
$dest = fopen('/tmp/file.zip', 'wb');
stream_copy_to_stream($source, $dest);
fclose($source);
fclose($dest);2. 总是关闭流
php
<?php
$handle = fopen('http://example.com', 'r');
try {
$data = stream_get_contents($handle);
} finally {
fclose($handle); // 确保关闭
}3. 使用 try-finally 管理流资源
php
<?php
function processStream(string $url): string
{
$handle = fopen($url, 'r');
if ($handle === false) {
throw new RuntimeException("无法打开: {$url}");
}
try {
return stream_get_contents($handle);
} finally {
fclose($handle);
}
}