草庐IT

php: 无法对 php://input 执行多个 fread() 调用

coder 2024-04-05 原文

我正在通过 POST 使用 content-encoding: chunked 将数据来回发送到 PHP 应用程序。我需要我的 PHP 应用程序读取一些数据、对其进行处理、发回响应、读取更多数据等等。我无法一次读取所有数据,因为它不可用。想象一下,定期发送带有校验和作为响应的大型文件上传。

问题是,虽然我可以从 php://input 读取少量字节,但随后对 fread 的调用不会返回新内容。

目前我正在使用 PHP's Docker container .我尝试了 php:7.0-apachephp:5-apache,结果相同。

下面的 PoC 客户端生成随机字符串,并以 3 秒的间隔将它们作为 block 发送到服务器。服务器以 1 秒的间隔从 php://input 读取并打印内容。服务器输出显示只读取了前三个字符串;在读取前三个之前,服务器似乎也会“阻塞”。

我已经尝试过,但无济于事:

  • 使用fseek
  • 使用 stream_select 似乎不适用于,呃,php://input stream。我不知道为什么这对我来说是理想的,但考虑到 PHP 的设计和实现有多么糟糕,我并不感到惊讶。
  • 关闭并重新打开 php://input
  • 使用fgetc

客户端输出:

    $ python poc.py
Sending:
---
POST /poc.php HTTP/1.1
Host: localhost
accept-encoding: *;q=0
Transfer-Encoding: chunked
Content-Type: application/octet-stream


---

After sending headers, response:
 HTTP/1.1 200 OK
Date: Mon, 29 May 2017 14:25:52 GMT
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/5.6.30
transfer-encoding: chunked
Content-Type: application/octet-stream

4
OK


Waiting 3 seconds
Sending string: AuVuvsyGJc

Waiting 3 seconds
Sending string: LfKouYzccV

Waiting 3 seconds
Sending string: WmpPspYqiR

Waiting 3 seconds
Sending string: IApMOjoaIv

Waiting 3 seconds
Sending string: tuGrVklcVy

Waiting 3 seconds
Sending string: btUVIezCow

Waiting 3 seconds
Sending string: XUPOrEidyd

Traceback (most recent call last):
  File "poc.py", line 33, in <module>
    websock.send(to_chunk(rnd))
socket.error: [Errno 32] Broken pipe

服务器输出:

Connected
Read: AuVuvsyGJc
LfKouYzccV
WmpPspYqiR

Read:
Read:
Read:
Read:
172.17.0.1 - - [29/May/2017:14:25:52 +0000] "POST /poc.php HTTP/1.1" 200 191 "-" "-"

PHP 服务器:

<?php
header("transfer-encoding: chunked");
header("content-type: application/octet-stream");
flush(); 
/**
 * Useful to print debug messages in the Apache logs
 */
function _log($what) {
    file_put_contents("php://stderr", print_r($what, true) . "\n");
}
_log("Connected");

/**
 * To send data as chunks
 */
function _ch($chunk) {
    echo sprintf("%x\r\n", strlen($chunk));
    echo $chunk;
    echo "\r\n";
    flush();
}
// Test chunks
_ch("OK\r\n");

$web_php_input = fopen("php://input", 'r');
$continue = 5;
while ($continue-- > 0) {
    $contents = fread($web_php_input, 1024);
    _log("Read: " . $contents);
    sleep(1);
}
fclose($web_php_input);
?>

Python 客户端:

from __future__ import print_function
import random
import socket
import string
import time

def to_chunk(what):
    return format(len(what), 'X') + "\r\n" + what + "\r\n"

websock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
websock.connect(("localhost", 8080))

# Send the initial chunked POST header
connect_string = ''.join((
    "POST /poc.php HTTP/1.1\r\n",
    "Host: localhost\r\n",
    "accept-encoding: *;q=0\r\n",  # ,gzip;q=0,deflate;q=0\r\n",
    "Transfer-Encoding: chunked\r\n",
    "Content-Type: application/octet-stream\r\n",
    # "Connection: keep-alive\r\n",
    "\r\n",
))
print("Sending:\n---\n{}\n---\n".format(connect_string))
websock.sendall(connect_string)
print("After sending headers, response:\n {}".format(websock.recv(1024)))
c = True
while c:
    print("Waiting 3 seconds")
    time.sleep(3)
    rnd = ''.join(random.choice(string.ascii_letters) for _ in range(10))
    rnd += '\r\n'
    print("Sending string: {}".format(rnd))
    websock.send(to_chunk(rnd))
print("done")

码头文件:

FROM php:5-apache
COPY custom.ini /usr/local/etc/php/conf.d

Docker 命令行:

docker build -t listener .
docker run -i --rm -p 8080:80 -v $(pwd):/var/www/html --name listener listener

custom.ini 文件让 PHP 知道 POST 主体不应该被缓冲:

enable_post_data_reading=false

在其他人建议使用另一种语言、框架或以不同方式做事之前:它必须是 PHP;它不能依赖任何第三方库或 PECL;而这正是我所需要的。

作为旁注,此行为符合 the HTTP spec ;服务器在向客户端返回部分响应之前不必读取所有入站数据。另见 RFC6202 .

最佳答案

为了理解为什么会发生这种情况,您需要了解 HTTP 的工作原理,不幸的是,这并不是您所想的那样。 分块传输编码和 PHP 也不会像您认为的那样工作。 我将尝试以一种与我认为您正在尝试做的事情相关的方式进行解释。

如果我理解正确,您正在尝试以交错方式发送请求和响应 block ,或者按照您的描述来回发送数据。 这违反了 HTTP 规范。 因此,您将无法这样做,因为请求是由 HTTP 服务器而不是 PHP 直接处理的。

HTTP

HTTP 是一种请求/响应协议(protocol)(RFC2616 第 1.4 节),操作简单:

  1. 客户端向服务器发送 HTTP 请求消息。
  2. 接收并解释请求消息后,服务器以 HTTP 响应消息进行响应。 (RFC2616 第 6 节)

请注意第 2 步说的是“之后”,而不是“同时”,这意味着服务器必须等待请求完成才能发送响应。 这就是“服务器似乎阻塞”的原因。

RFC6202 中描述的 HTTP 长轮询和 HTTP 流的生命周期实际上以相同的方式工作,没有违反 HTTP 规范。 它们不会来回发送数据(无交错)。

分块传输编码

如果请求有 Transfer-Encoding: chunked header ,服务器必须等待最后一个 block 。 至少在两个地方对此进行了描述:

  1. Section 3.6.1 的 BNF 中. 观察 Chunked-Body 必须有 last-chunk
  2. Section 19.4.6的伪代码中. 观察循环内没有“向客户端发送响应”或类似内容(在整个伪代码中,真的)。

简而言之,不允许交错。 分块传输编码不会引入交错,因此不会改变 HTTP 的工作方式。

PHP

因为服务器必须等待请求,所以直到请求完成后才会调用 PHP。 因此,当您发送具有 3 秒延迟的数据 block 时,您的 PHP 脚本甚至还没有运行。

至于PHP配置项enable_post_data_rendering,是不存在的。 最接近的是enable_post_data_reading , 这仅仅意味着请求正文将不会被解析,因此 $_FILES 和 $_POST 将为空。 这是出于效率原因:没有时间花在解析请求体上,也没有内存用于保存 $_FILES 和 $_POST 的值。 它与 POST 正文缓冲无关。

如果您还有什么不明白的地方,请告诉我。

更新

这是我自己实验的输出,事件之间的间隔为 3 秒,套接字超时为 15 秒。 时间戳可用于确定哪些事件链接在一起。

观察到从服务器读取总是在发送最后一个 block 之前超时。 还要观察发送最后一个 block 时的时间戳 13:43:03,这也是调用 PHP 的时间。 它表明服务器在调用 PHP 之前等待最后一个 block 。

client 13:40:54 opening socket... opened
client 13:40:57 sending request... 130 bytes sent
client 13:41:00 reading from server...
client 13:41:15 timed out
client 13:41:18 sending chunk 0... 14 bytes sent
client 13:41:21 reading from server...
client 13:41:36 timed out
client 13:41:39 sending chunk 1... 14 bytes sent
client 13:41:42 reading from server...
client 13:41:57 timed out
client 13:42:00 sending chunk 2... 14 bytes sent
client 13:42:03 reading from server...
client 13:42:18 timed out
client 13:42:21 sending chunk 3... 14 bytes sent
client 13:42:24 reading from server...
client 13:42:39 timed out
client 13:42:42 sending chunk 4... 14 bytes sent
client 13:42:45 reading from server...
client 13:43:00 timed out
client 13:43:03 sending last chunk... 5 bytes sent
client 13:43:06 reading from server...
client 13:43:06 279 bytes read
client 13:43:06 ---------- start of response
HTTP/1.1 200 OK
Host: localhost
Connection: close
X-Powered-By: PHP/7.0.12
Transfer-Encoding: chunked
Content-Type: application/octet-stream

20
server 2017-06-16 13:43:03 start
2d
13:41:18
13:41:39
13:42:00
13:42:21
13:42:42

1e
server 2017-06-16 13:43:03 end
0

client 13:43:06 ---------- end of response
client 13:43:06 done

This is the server.php:

<?php
while(@ob_end_flush());
header("Transfer-Encoding: chunked");
header("Content-Type: application/octet-stream");

echo chunk("server ".gmdate("Y-m-d H:i:s ")."start");

if($f = fopen("php://input", "r")){
    while($s = fread($f, 1024)){
        echo chunk($s);
    }
    fclose($f);
}

echo chunk("server ".gmdate("Y-m-d H:i:s ")."end");
echo chunk("");

function chunk($s){
    return dechex(strlen($s))."\r\n".$s."\r\n";
}

这是client.php:

<?php
out("opening socket... ");
if($socket = fsockopen("localhost", 80, $errno, $error)){
    echo "opened\n";
    
    //set socket timeout to 15 seconds
    stream_set_timeout($socket, 15);
    sleep(3);
    
    out("sending request... ");
    $n = fwrite($socket, "POST http://localhost/server.php HTTP/1.1\r\n"
        ."Host: localhost\r\n"
        ."Transfer-Encoding: chunked\r\n"
        ."Content-Type: application/octet-stream\r\n"
        ."\r\n"
    );
    echo "$n bytes sent\n";
    sleep(3);

    readFromServer($socket);
    sleep(3);
    
    for($i=0; $i<5; $i++){
        out("sending chunk {$i}... ");
        $n = fwrite($socket, chunk(gmdate("H:i:s\n")));
        echo "$n bytes sent\n";
        sleep(3);
        readFromServer($socket);
        sleep(3);
    }
    out("sending last chunk... ");
    $n = fwrite($socket, chunk(""));
    echo "$n bytes sent\n";
    sleep(3);

    readFromServer($socket);
    fclose($socket);
}else{
    echo "error\n";
}
out("done\n");

function out($s){
    echo "client ".gmdate("H:i:s ").$s;
}

function chunk($s){
    return dechex(strlen($s))."\r\n".$s."\r\n";
}

function readFromServer($socket){
    out("reading from server... \n");
    $response = fread($socket, 1024);
    $info = stream_get_meta_data($socket);
    if($info['timed_out']){
        out("timed out\n");
    }else{
        out(strlen($response)." bytes read\n");
        if($response){
            out("---------- start of response\n");
            echo $response;
            out("---------- end of response\n");
        }
    }
}

关于php: 无法对 php://input 执行多个 fread() 调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44245156/

有关php: 无法对 php://input 执行多个 fread() 调用的更多相关文章

  1. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  2. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  3. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  4. ruby-on-rails - 在 Ruby 中循环遍历多个数组 - 2

    我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代

  5. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  6. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  7. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  8. ruby-on-rails - 无法在centos上安装therubyracer(V8和GCC出错) - 2

    我正在尝试在我的centos服务器上安装therubyracer,但遇到了麻烦。$geminstalltherubyracerBuildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingtherubyracer:ERROR:Failedtobuildgemnativeextension./usr/local/rvm/rubies/ruby-1.9.3-p125/bin/rubyextconf.rbcheckingformain()in-lpthread...yescheckingforv8.h...no***e

  9. ruby - 无法让 RSpec 工作—— 'require' : cannot load such file - 2

    我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳

  10. ruby - 多个属性的 update_column 方法 - 2

    我有一个具有一些属性的模型:attr1、attr2和attr3。我需要在不执行回调和验证的情况下更新此属性。我找到了update_column方法,但我想同时更新三个属性。我需要这样的东西:update_columns({attr1:val1,attr2:val2,attr3:val3})代替update_column(attr1,val1)update_column(attr2,val2)update_column(attr3,val3) 最佳答案 您可以使用update_columns(attr1:val1,attr2:val2

随机推荐