☰
AJ-Captcha PHP 依赖中的 Guzzle PSR-7 消息实现:完整解读 Stream、Message、Query 与 Uri 工具集
2026/10/5 2:23:51 网站建设 项目流程
  • 后端
  • 应用安全
  • 图像处理

【免费下载链接】captcha

行为验证码(滑动拼图、点选文字),前后端(java)交互,包含h5/Android/IOS/flutter/uni-app的源码和实现

项目地址:https://gitcode.com/gh_mirrors/captc/captcha
点击查看免费下载

导读

本文围绕当前仓库service/php/vendor/guzzlehttp/psr7/README.md这一文档,系统讲解 Guzzle PSR-7 消息实现的核心能力:从十余种流实现与装饰器、Message 静态 API、Query 查询串解析,到 Uri 的规范化与引用解析。该包以 MIT 协议提供完整的 PSR-7 HTTP 消息实现(版本 2.1.0),并作为 AJ-Captcha PHP 行为验证码后端fastknife/ajcaptcha(service/php/composer.json)的间接依赖随composer install一同安装。读完本文,你将掌握每个流类的适用场景、静态工具方法的使用范式、2.0 从函数 API 到静态 API 的迁移方法,以及 URI 规范化与相对引用解析的 RFC 3986 实践。

一、背景:为什么 AJ-Captcha 会携带 PSR-7 实现

AJ-Captcha PHP 包本身不直接调用 GuzzleHttp\Psr7 命名空间下的类,它主要通过 GD 扩展与 Intervention Image 完成滑动拼图与点选文字的图像生成。但 Intervention Image 2.x 在其依赖中声明了guzzlehttp/psr7(见 service/php/vendor/intervention/image/composer.json 的"guzzlehttp/psr7": "~1.1 || ^2.0"),因此composer install后会在service/php/vendor/guzzlehttp/psr7下落地该实现(本仓库锁定版本为 2.1.0,可从 service/php/vendor/composer/installed.json 确认)。

了解这一点有助于你在排查验证码服务端问题时快速定位:凡是涉及 HTTP 消息对象化、流式读取与 URI 规范化操作的报错栈,都可能来自这个底层库。下面按文档原骨架(Stream 实现 → 静态 API → URI 方法)逐一深入。

二、Stream 实现与装饰器:十一类流的定位与用法

GuzzleHttp\Psr7命名空间下提供了一批实现了Psr\Http\Message\StreamInterface的流类,源码均位于 service/php/vendor/guzzlehttp/psr7/src。以下代码示例均出自原文档,可直接复制运行。

2.1 AppendStream:顺序拼接多段流

GuzzleHttp\Psr7\AppendStream依次读取多个流,把多段内容拼接成一个可整体读出的流:

use GuzzleHttp\Psr7; $a = Psr7\Utils::streamFor('abc, '); $b = Psr7\Utils::streamFor('123.'); $composed = new Psr7\AppendStream([$a, $b]); $composed->addStream(Psr7\Utils::streamFor(' Above all listen to me')); echo $composed; // abc, 123. Above all listen to me.

2.2 BufferStream:带高水位标记(hwm)的内存缓冲

GuzzleHttp\Psr7\BufferStream提供可写可读的缓冲流,并对外暴露hwm元数据:当缓冲内容超过配置的高水位时,write()开始返回false,提示上游写入方应放慢速度:

use GuzzleHttp\Psr7; // 缓冲超过 1024 字节后,写入开始返回 false $buffer = new Psr7\BufferStream(1024);

2.3 CachingStream:为不可 seek 的流提供回退能力

GuzzleHttp\Psr7\CachingStream把已读过的字节缓存到 PHP 临时流(先内存后落盘),从而允许对不可 seek 的实体流执行seek()——典型场景是重定向后需要回绕请求体:

use GuzzleHttp\Psr7; $original = Psr7\Utils::streamFor(fopen('http://www.google.com', 'r')); $stream = new Psr7\CachingStream($original); $stream->read(1024); echo $stream->tell(); // 1024 $stream->seek(0); echo $stream->tell(); // 0

2.4 DroppingStream:超出容量即丢弃数据

GuzzleHttp\Psr7\DroppingStream是一个装饰器:当底层流已满到阈值时,后续写入的数据被直接丢弃:

use GuzzleHttp\Psr7; $stream = Psr7\Utils::streamFor(); // 空流 // 流超过 10 字节后开始丢弃写入 $dropping = new Psr7\DroppingStream($stream, 10); $dropping->write('01234567890123456789'); echo $stream; // 0123456789

2.5 FnStream:以函数表组合流行为

GuzzleHttp\Psr7\FnStream允许用一组回调函数组合出一个流,便于测试和轻量扩展:

use GuzzleHttp\Psr7; $stream = Psr7\Utils::streamFor('hi'); $fnStream = Psr7\FnStream::decorate($stream, [ 'rewind' => function () use ($stream) { echo 'About to rewind - '; $stream->rewind(); echo 'rewound!'; } ]); $fnStream->rewind(); // Outputs: About to rewind - rewound!

2.6 InflateStream:透明解压 zlib / gzip

GuzzleHttp\Psr7\InflateStream借助 PHP 的zlib.inflate过滤器,解压 RFC 1950(HTTP deflate)或 RFC 1952(gzip)内容。实现上先把输入流转换为 PHP 流资源、追加过滤器,再包装回 Guzzle 流,对调用方完全透明。

2.7 LazyOpenStream:惰性打开文件

GuzzleHttp\Psr7\LazyOpenStream在构造时不打开文件,只有真正发生 IO 时才打开,适合延迟加载大文件:

use GuzzleHttp\Psr7; $stream = new Psr7\LazyOpenStream('/path/to/file', 'r'); // 此时文件尚未打开…… echo $stream->read(10); // 仅在真正读取时才打开并读取文件

2.8 LimitStream:读取流的子区间

GuzzleHttp\Psr7\LimitStream从既有流中切出指定长度、指定起始偏移的子流,可用于将大文件分片传输(如 S3 分片上传):

use GuzzleHttp\Psr7; $original = Psr7\Utils::streamFor(fopen('/tmp/test.txt', 'r+')); echo $original->getSize(); // >>> 1048576 // 从字节 2048 开始,仅读取 1024 字节 $stream = new Psr7\LimitStream($original, 1024, 2048); echo $stream->getSize(); // >>> 1024 echo $stream->tell(); // >>> 0

2.9 MultipartStream / NoSeekStream / PumpStream

  • GuzzleHttp\Psr7\MultipartStream:读取时产出multipart/form-data格式字节流;
  • GuzzleHttp\Psr7\NoSeekStream:包装流并禁止 seek——isSeekable()返回false,调用seek()后继续read()得到NULL:
    $original = Psr7\Utils::streamFor('foo'); $noSeek = new Psr7\NoSeekStream($original); var_export($noSeek->isSeekable()); // false
  • GuzzleHttp\Psr7\PumpStream:只读流,由 PHP callable 供数;每次读取时传入建议字节数,callable 可返回更多或更少字节(多余部分内部缓冲),数据耗尽时必须返回false。

2.10 自定义装饰器:StreamDecoratorTrait

文档专门演示了基于GuzzleHttp\Psr7\StreamDecoratorTrait快速实现装饰器:trait 已把Psr\Http\Message\StreamInterface的所有方法代理到底层流,你只需实现自定义方法。例如在读到 EOF 时触发回调:

use Psr\Http\Message\StreamInterface; use GuzzleHttp\Psr7\StreamDecoratorTrait; class EofCallbackStream implements StreamInterface { use StreamDecoratorTrait; private $callback; public function __construct(StreamInterface $stream, callable $cb) { $this->stream = $stream; $this->callback = $cb; } public function read($length) { $result = $this->stream->read($length); // Invoke the callback when EOF is hit. if ($this->eof()) { call_user_func($this->callback); } return $result; } }

使用方式:

use GuzzleHttp\Psr7; $original = Psr7\Utils::streamFor('foo'); $eofStream = new EofCallbackStream($original, function () { echo 'EOF!'; }); $eofStream->read(2); $eofStream->read(1); // echoes "EOF!"

StreamDecoratorTrait的代理实现可参见 service/php/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php(含__get惰性创建底层流、__toString异常处理等细节)。

2.11 StreamWrapper:把 PSR-7 流当作 PHP 流资源

若需要把 PSR-7 流交给只接受 PHP 流资源的函数,可用GuzzleHttp\Psr7\StreamWrapper::getResource():

use GuzzleHttp\Psr7\StreamWrapper; $stream = GuzzleHttp\Psr7\Utils::streamFor('hello!'); $resource = StreamWrapper::getResource($stream); echo fread($resource, 6); // outputs hello!

三、静态 API:从函数式到面向对象

从 1.7.0 起包提供静态 API,用于规避全局函数在包的多副本间冲突的问题;2.0.0 彻底移除了函数式 API。核心入口类为GuzzleHttp\Psr7\Message、Header、Query、Utils与MimeType。

3.1 Message:消息序列化与解析

方法签名作用
Message::toString(MessageInterface): string返回 HTTP 消息的字符串表示
Message::bodySummary(MessageInterface, int $truncateAt = 120): string\|null返回消息体摘要,不可打印时返回null
Message::rewindBody(MessageInterface): void回绕消息体,失败抛异常;仅当tell()非 0 时才真正回绕
Message::parseMessage(string): array把 HTTP 消息解析为含start-line、headers、body三键的数组
Message::parseRequestUri(string $path, array $headers): string为请求消息构造 URI
Message::parseRequest(string): Request请求字符串 → Request 对象
Message::parseResponse(string): Response响应字符串 → Response 对象

序列化示例:

$request = new GuzzleHttp\Psr7\Request('GET', 'http://example.com'); echo GuzzleHttp\Psr7\Message::toString($request);

parseRequest/parseResponse的实现位于 service/php/vendor/guzzlehttp/psr7/src/Message.php,它们借助 Rfc7230 解析起行与头字段,再按“请求/响应”语义构造对应对象。

3.2 Header:解析与归一化

  • Header::parse(string|array $header): array:把;分隔的头部参数解析为键值对数组,无值的参数注入空字符串键。
  • Header::normalize(string|array $header): array:把可能含逗号合并值的头字段拆分为无逗号合并的数组。

3.3 Query:查询串解析与构建

  • Query::parse(string $str, int|bool $urlEncoding = true): array:查询串 → 关联数组。同键多值时值为数组;不支持PHP 嵌套数组风格(foo[a]=1&foo[b]=2解析为['foo[a]' => '1', 'foo[b]' => '2'])。
  • Query::build(array $params, int|false $encoding = PHP_QUERY_RFC3986): string:数组 → 查询串,可直接用parse()的返回值回建;与http_build_query()不同,遇到数组键时不改写键名。

实现见 service/php/vendor/guzzlehttp/psr7/src/Query.php。

3.4 Utils:通用工具方法

  • Utils::caselessRemove(iterable $keys, array $data): array:从数据中按键名大小写不敏感地移除项。
  • Utils::copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void:把源流复制到目标流,最多$maxLen字节。
  • Utils::copyToString(StreamInterface $stream, int $maxLen = -1): string:把流内容读入字符串。
  • Utils::hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string:基于 PHPhash_init对整流计算滚动哈希。
  • Utils::modifyRequest(RequestInterface $request, array $changes): RequestInterface:克隆并修改请求,减少多次克隆成本。$changes支持method、set_headers、remove_headers、body、uri、query、version等键。
  • Utils::readLine(StreamInterface $stream, int $maxLength = null): string:按最大缓冲读取一行。
  • Utils::tryFopen(string $filename, string $mode): resource:安全打开 PHP 流资源,把 fopen 失败时的告警转为异常。
  • Utils::uriFor(string|UriInterface $uri): UriInterface:字符串或 UriInterface → UriInterface。
streamFor:最常用的工厂方法

Utils::streamFor(mixed $resource = '', array $options = []): StreamInterface按输入类型创建流。$options可含metadata(自定义元数据)与size(流大小)。支持的$resource类型:

  • StreamInterface:原样返回;
  • string:以字符串为内容创建流;
  • resource:包装 PHP 流资源;
  • Iterator:创建只读流,读取时迭代器数据持续填充缓冲;
  • 含__toString()的对象:先转字符串再建流;
  • NULL:返回空流;
  • callable:创建只读流,按建议字节数调用该 callable,耗尽时必须返回false。

示例:

$stream = GuzzleHttp\Psr7\Utils::streamFor('foo'); $stream = GuzzleHttp\Psr7\Utils::streamFor(fopen('/path/to/file', 'r')); $generator = function ($bytes) { for ($i = 0; $i < $bytes; $i++) { yield ' '; } } $stream = GuzzleHttp\Psr7\Utils::streamFor($generator(100));

3.5 MimeType:文件名与扩展名 → MIME 类型

  • MimeType::fromFilename(string $filename): string|null:按扩展名推断 MIME 类型。
  • MimeType::fromExtension(string $extension): string|null:扩展名 → MIME 类型映射。

3.6 2.0 迁移对照表(原文档全文继承)

函数式 API 已在 2.0.0 移除,迁移对照如下:

原函数替代方法
strMessage::toString
uri_forUtils::uriFor
stream_forUtils::streamFor
parse_headerHeader::parse
normalize_headerHeader::normalize
modify_requestUtils::modifyRequest
rewind_bodyMessage::rewindBody
try_fopenUtils::tryFopen
copy_to_stringUtils::copyToString
copy_to_streamUtils::copyToStream
hashUtils::hash
readlineUtils::readLine
parse_requestMessage::parseRequest
parse_responseMessage::parseResponse
parse_queryQuery::parse
build_queryQuery::build
mimetype_from_filenameMimeType::fromFilename
mimetype_from_extensionMimeType::fromExtension
_parse_messageMessage::parseMessage
_parse_request_uriMessage::parseRequestUri
get_message_body_summaryMessage::bodySummary
_caseless_removeUtils::caselessRemove

四、URI 附加方法:类型判定、组件操作与引用解析

除标准GuzzleHttp\Psr7\Uri类外,文档还讲解了UriResolver、UriNormalizer两组按 RFC 3986 实现的静态工具。

4.1 URI 类型判定(RFC 3986 4.2)

UriInterface实例可能是绝对 URI 或相对引用。相对引用分为三类:

  • network-path 引用://example.com/path
  • absolute-path 引用:/path
  • relative-path 引用:subpath

对应判定方法:

方法判定内容
Uri::isAbsolute(UriInterface $uri): bool是否绝对 URI(含 scheme)
Uri::isNetworkPathReference(UriInterface $uri): bool是否以双斜杠开头
Uri::isAbsolutePathReference(UriInterface $uri): bool是否以单斜杠开头
Uri::isRelativePathReference(UriInterface $uri): bool是否不以斜杠开头
Uri::isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool除 fragment 外与 base 是否完全一致;无 base 时仅空引用成立

4.2 URI 组件操作

  • Uri::isDefaultPort(UriInterface $uri): bool:判断是否使用当前 scheme 的默认端口(独立于具体实现判断getPort()为 null 或标准端口)。
  • Uri::composeComponents($scheme, $authority, $path, $query, $fragment): string:按 RFC 3986 5.3 组装 URI 字符串(通常经__toString间接调用,无需手动调用)。
  • Uri::fromParts(array $parts): UriInterface:由parse_url结果哈希创建 URI。
  • Uri::withQueryValue(UriInterface $uri, $key, $value): UriInterface:设置单个查询值(完全匹配的旧键被替换;值为 null 时输出无值的键,如key)。
  • Uri::withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface:批量设置查询值,行为与withQueryValue()一致。
  • Uri::withoutQueryValue(UriInterface $uri, $key): UriInterface:移除指定查询键。

4.3 UriResolver:引用解析与相对化(RFC 3986 5)

  • UriResolver::resolve(UriInterface $base, UriInterface $rel): UriInterface:把相对 URI 解析为基于 base 的新 URI,等价于浏览器根据当前请求 URI 解析页面链接的行为。
  • UriResolver::removeDotSegments(string $path): string:按 RFC 3986 5.2.4 移除路径中的./..段。
  • UriResolver::relativize(UriInterface $base, UriInterface $target): UriInterface:resolve()的逆操作,返回 target 相对 base 的引用,满足恒等式(string)$target === (string)UriResolver::resolve($base, UriResolver::relativize($base, $target))。典型用途:以当前请求 URI 为 base,为文档生成相对链接以减小体积或制作自包含归档:
$base = new Uri('http://example.com/a/b/'); echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'.

4.4 UriNormalizer:规范化与等价比较(RFC 3986 6)

UriNormalizer::normalize(UriInterface $uri, $flags = self::PRESERVING_NORMALIZATIONS): UriInterface返回规范化 URI。scheme 与 host 已按 PSR-7 要求小写化,其余规范化由$flags位掩码控制:

常量语义示例
PRESERVING_NORMALIZATIONS默认规范化,仅保留语义—
CAPITALIZE_PERCENT_ENCODING百分号编码三元组字母大写http://example.org/a%c2%b1b→.../a%C2%B1b
DECODE_UNRESERVED_CHARACTERS解码非保留字符的百分号编码.../%7Eusern%61me/→.../~username/
CONVERT_EMPTY_PATHhttp/https 空路径转为/http://example.org→http://example.org/
REMOVE_DEFAULT_HOST移除默认 host(仅filescheme 默认 host 为localhost)file://localhost/myfile→file:///myfile
REMOVE_DEFAULT_PORT移除默认端口http://example.org:80/→http://example.org/
REMOVE_DOT_SEGMENTS移除多余点段(相对引用中的点段不删除以免改变语义)http://example.org/../a/b/../c/./d.html→http://example.org/a/c/d.html
REMOVE_DUPLICATE_SLASHES连续斜杠合并(%2F编码斜杠不处理;可能改变语义)http://example.org//foo///bar.html→http://example.org/foo/bar.html
SORT_QUERY_PARAMETERS查询参数按键字母排序(参数顺序可能具有语义,不安全)?lang=en&article=fred→?article=fred&lang=en

UriNormalizer::isEquivalent(UriInterface $uri1, UriInterface $uri2, $normalizations = self::PRESERVING_NORMALIZATIONS): bool先按给定位掩码规范化再比较;也接受相对引用——此时默认它们会基于同一 base 解析,否则等价性判断无意义。

五、安全、许可与依赖边界

  • 安全:包内安全漏洞通过 security@tidelift.com 私下上报,修复公告前请勿公开披露(见 service/php/vendor/guzzlehttp/psr7/README.md 的 Security 一节)。
  • 许可:Guzzle PSR-7 采用 MIT 协议,许可文本见 service/php/vendor/guzzlehttp/psr7/LICENSE;注意它与其所属的 AJ-Captcha PHP 主包(GPL-3.0-only,见 service/php/composer.json)协议不同,作为依赖使用时按各自协议约束执行。
  • 依赖边界:该包要求 PHP^7.2.5 || ^8.0,并依赖psr/http-factory、psr/http-message与ralouphie/getallheaders(见 service/php/vendor/guzzlehttp/psr7/composer.json);而 AJ-Captcha 主包要求 PHP>=7.1与 GD、OpenSSL 等扩展。在 ThinkPHP、Laravel 等框架项目中通过composer require fastknife/ajcaptcha安装后,这些依赖会随composer install一并落地(示例工程见 service/php/test/thinkphp 与 service/php/test/laravel)。

结语

Guzzle PSR-7 文档所覆盖的十一类流、Message/Header/Query/Utils/MimeType 静态 API 与 Uri 三件套,构成了 PHP HTTP 客户端生态中最常用的一层消息抽象。对 AJ-Captcha 使用者而言,理解这些底层组件既有助于在验证码服务端集成中排查 HTTP 相关故障,也能在需要流式处理图像响应或构造 HTTP 请求时直接复用这套成熟实现。建议结合 service/php/vendor/guzzlehttp/psr7/src 下的源码(如Utils.php、Message.php、UriResolver.php、UriNormalizer.php)做一次通读,能更快掌握其设计取舍。

  • 后端
  • 应用安全
  • 图像处理

【免费下载链接】captcha

行为验证码(滑动拼图、点选文字),前后端(java)交互,包含h5/Android/IOS/flutter/uni-app的源码和实现

项目地址:https://gitcode.com/gh_mirrors/captc/captcha
点击查看免费下载
上一篇:Daft 文件与 URL 处理全指南:从分布式下载、daft.File 懒加载到字节范围读取
下一篇:什么是联邦学习?Flower 联邦学习入门指南:从集中式机器学习到联邦训练五步流程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询