我正在通过 POST 使用 content-encoding: chunked
将数据来回发送到 PHP 应用程序。我需要我的 PHP 应用程序读取一些数据、对其进行处理、发回响应、读取更多数据等等。我无法一次读取所有数据,因为它不可用。想象一下,定期发送带有校验和作为响应的大型文件上传。
问题是,虽然我可以从 php://input
读取少量字节,但随后对 fread
的调用不会返回新内容。
目前我正在使用 PHP's Docker container .我尝试了 php:7.0-apache
和 php: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 节),操作简单:
- 客户端向服务器发送 HTTP 请求消息。
- 接收并解释请求消息后,服务器以 HTTP 响应消息进行响应。 (RFC2616 第 6 节)
请注意第 2 步说的是“之后”,而不是“同时”,这意味着服务器必须等待请求完成才能发送响应。 这就是“服务器似乎阻塞”的原因。
RFC6202 中描述的 HTTP 长轮询和 HTTP 流的生命周期实际上以相同的方式工作,没有违反 HTTP 规范。 它们不会来回发送数据(无交错)。
分块传输编码
如果请求有 Transfer-Encoding: chunked
header ,服务器必须等待最后一个 block 。
至少在两个地方对此进行了描述:
- 在 Section 3.6.1 的 BNF 中.
观察
Chunked-Body
必须有last-chunk
。 - 在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/