当前位置: 首页 > 面试题库 >

如何实时读取和回显正在服务器上写入的上传文件的文件大小,而又不会在服务器和客户端上造成阻塞?

白昊东
2023-03-14
问题内容

题:

如何实时读取和回显正在服务器上写入的上传文件的文件大小,而又不会在服务器和客户端上造成阻塞?

内容:

文件上传进度被从写入到服务器POST请求所作fetch(),其中body被设置为BlobFileTypedArray,或ArrayBuffer对象。

当前实现将File对象设置为body传递给的第二个参数的对象fetch()

需求:

读取echo并向客户端发送写入服务器上文件系统的文件的文件大小text/event- stream。当所有字节(作为变量提供给脚本,作为GET请求中的查询字符串参数)已写入时,停止。当前,文件的读取是在单独的脚本环境中进行的,在该脚本环境中,GET应在POST将文件写入服务器的脚本之后调用应读取文件的脚本。

尚未达到将文件写入服务器或读取文件以获取当前文件大小的潜在问题的错误处理,尽管一旦echo文件大小部分完成,这将是下一步。

目前正尝试使用来满足要求php。虽然也有兴趣在cbashnodejspython,或其他可以用来执行相同任务的语言或方法。

客户端javascript部分不是问题。根本不熟悉php,它是万维网上最常用的服务器端语言之一,用于在不包含不需要的部分的情况下实现该模式。

问题:

得到

PHP Notice:  Undefined index: HTTP_LAST_EVENT_ID in stream.php on line 7

terminal

另外,如果替代

while(file_exists($_GET["filename"]) 
  && filesize($_GET["filename"]) < intval($_GET["filesize"]))

对于

while(true)

在产生错误EventSource

如果没有sleep()电话,正确的文件大小被派往message事件的3.3MB文件3321824,被印在console
619212621438093次,分别在上传同一文件三次。预期结果是文件在写入时的文件大小

stream_copy_to_stream($input, $file);

而不是上传文件对象的文件大小。是fopen()stream_copy_to_stream()堵不如到其他不同php的过程stream.php

php

// can we merge `data.php`, `stream.php` to same file?
// can we use `STREAM_NOTIFY_PROGRESS` 
// "Indicates current progress of the stream transfer 
// in bytes_transferred and possibly bytes_max as well" to read bytes?
// do we need to call `stream_set_blocking` to `false`
// data.php
<?php

  $filename = $_SERVER["HTTP_X_FILENAME"];
  $input = fopen("php://input", "rb");
  $file = fopen($filename, "wb"); 
  stream_copy_to_stream($input, $file);
  fclose($input);
  fclose($file);
  echo "upload of " . $filename . " successful";

?>



// stream.php
<?php

  header("Content-Type: text/event-stream");
  header("Cache-Control: no-cache");
  header("Connection: keep-alive");
  // `PHP Notice:  Undefined index: HTTP_LAST_EVENT_ID in stream.php on line 7` ?
  $lastId = $_SERVER["HTTP_LAST_EVENT_ID"] || 0;
  if (isset($lastId) && !empty($lastId) && is_numeric($lastId)) {
      $lastId = intval($lastId);
      $lastId++;
  }
  // else {
  //  $lastId = 0;
  // }

  // while current file size read is less than or equal to 
  // `$_GET["filesize"]` of `$_GET["filename"]`
  // how to loop only when above is `true`
  while (true) {
    $upload = $_GET["filename"];
    // is this the correct function and variable to use
    // to get written bytes of `stream_copy_to_stream($input, $file);`?
    $data = filesize($upload);
    // $data = $_GET["filename"] . " " . $_GET["filesize"];
    if ($data) {
      sendMessage($lastId, $data);
      $lastId++;
    } 
    // else {
    //   close stream 
    // }
    // not necessary here, though without thousands of `message` events
    // will be dispatched
    // sleep(1);
    }

    function sendMessage($id, $data) {
      echo "id: $id\n";
      echo "data: $data\n\n";
      ob_flush();
      flush();
    }
?>

javascript

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<input type="file">
<progress value="0" max="0" step="1"></progress>
<script>

const [url, stream, header] = ["data.php", "stream.php", "x-filename"];

const [input, progress, handleFile] = [
        document.querySelector("input[type=file]")
      , document.querySelector("progress")
      , (event) => {
          const [file] = input.files;
          const [{size:filesize, name:filename}, headers, params] = [
                  file, new Headers(), new URLSearchParams()
                ];
          // set `filename`, `filesize` as search parameters for `stream` URL
          Object.entries({filename, filesize})
          .forEach(([...props]) => params.append.apply(params, props));
          // set header for `POST`
          headers.append(header, filename);
          // reset `progress.value` set `progress.max` to `filesize`
          [progress.value, progress.max] = [0, filesize];
          const [request, source] = [
            new Request(url, {
                  method:"POST", headers:headers, body:file
                })
            // https://stackoverflow.com/a/42330433/
          , new EventSource(`${stream}?${params.toString()}`)
          ];
          source.addEventListener("message", (e) => {
            // update `progress` here,
            // call `.close()` when `e.data === filesize` 
            // `progress.value = e.data`, should be this simple
            console.log(e.data, e.lastEventId);
          }, true);

          source.addEventListener("open", (e) => {
            console.log("fetch upload progress open");
          }, true);

          source.addEventListener("error", (e) => {
            console.error("fetch upload progress error");
          }, true);
          // sanity check for tests, 
          // we don't need `source` when `e.data === filesize`;
          // we could call `.close()` within `message` event handler
          setTimeout(() => source.close(), 30000);
          // we don't need `source' to be in `Promise` chain, 
          // though we could resolve if `e.data === filesize`
          // before `response`, then wait for `.text()`; etc.
          // TODO: if and where to merge or branch `EventSource`,
          // `fetch` to single or two `Promise` chains
          const upload = fetch(request);
          upload
          .then(response => response.text())
          .then(res => console.log(res))
          .catch(err => console.error(err));
        }
];

input.addEventListener("change", handleFile, true);
</script>
</body>
</html>

问题答案:

您需要清除statcache以获取实际文件大小。固定了一些其他位之后,您的stream.php可能如下所示:

<?php

header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");
header("Connection: keep-alive");
// Check if the header's been sent to avoid `PHP Notice:  Undefined index: HTTP_LAST_EVENT_ID in stream.php on line `
// php 7+
//$lastId = $_SERVER["HTTP_LAST_EVENT_ID"] ?? 0;
// php < 7
$lastId = isset($_SERVER["HTTP_LAST_EVENT_ID"]) ? intval($_SERVER["HTTP_LAST_EVENT_ID"]) : 0;

$upload = $_GET["filename"];
$data = 0;
// if file already exists, its initial size can be bigger than the new one, so we need to ignore it
$wasLess = $lastId != 0;
while ($data < $_GET["filesize"] || !$wasLess) {
    // system calls are expensive and are being cached with assumption that in most cases file stats do not change often
    // so we clear cache to get most up to date data
    clearstatcache(true, $upload);
    $data = filesize($upload);
    $wasLess |= $data <  $_GET["filesize"];
    // don't send stale filesize
    if ($wasLess) {
        sendMessage($lastId, $data);
        $lastId++;
    }
    // not necessary here, though without thousands of `message` events will be dispatched
    //sleep(1);
    // millions on poor connection and large files. 1 second might be too much, but 50 messages a second must be okay
    usleep(20000);
}

function sendMessage($id, $data)
{
    echo "id: $id\n";
    echo "data: $data\n\n";
    ob_flush();
    // no need to flush(). It adds content length of the chunk to the stream
    // flush();
}

注意事项:

安全。我是说运气据我了解,这是概念的证明,而安全是最不用担心的,但免责声明应该存在。这种方法从根本上来说是有缺陷的,仅当您不关心DOS攻击或有关文件的信息消失时才应使用此方法。

中央处理器。没有usleep该脚本将消耗100%的单个内核。长时间睡眠会使您有在单个迭代中上载整个文件的风险,并且永远不会满足退出条件。如果您在本地进行测试,usleep则应将其完全删除,因为在本地上传MB的时间约为毫秒。

打开连接。apache和nginx /
fpm都具有可以满足请求的php进程数量有限。上载单个文件所需的时间为2。在带宽缓慢或伪造请求的情况下,此时间可能会很长,并且Web服务器可能开始拒绝请求。

客户端部分。文件完全上载后,您需要分析响应并最终停止侦听事件。

编辑:

为了使它或多或少对生产友好,您将需要内存中的存储(例如redis或memcache)来存储文件元数据。

发出发布请求,添加一个唯一的令牌,该令牌标识文件和文件大小。

在您的JavaScript中:

const fileId = Math.random().toString(36).substr(2); // or anything more unique
...

const [request, source] = [
    new Request(`${url}?fileId=${fileId}&size=${filesize}`, {
        method:"POST", headers:headers, body:file
    })
    , new EventSource(`${stream}?fileId=${fileId}`)
];
....

在data.php中注册令牌并按块报告进度:

....

$fileId = $_GET['fileId'];
$fileSize = $_GET['size'];

setUnique($fileId, 0, $fileSize);

while ($uploaded = stream_copy_to_stream($input, $file, 1024)) {
    updateProgress($id, $uploaded);
}
....


/**
 * Check if Id is unique, and store processed as 0, and full_size as $size 
 * Set reasonable TTL for the key, e.g. 1hr 
 *
 * @param string $id
 * @param int $size
 * @throws Exception if id is not unique
 */
function setUnique($id, $size) {
    // implement with your storage of choice
}

/**
 * Updates uploaded size for the given file
 *
 * @param string $id
 * @param int $processed
 */
function updateProgress($id, $processed) {
    // implement with your storage of choice
}

因此,您的stream.php根本不需要打入磁盘,并且只要UX可以接受就可以hibernate:

....
list($progress, $size) = getProgress('non_existing_key_to_init_default_values');
$lastId = 0;

while ($progress < $size) {
    list($progress, $size) = getProgress($_GET["fileId"]);
    sendMessage($lastId, $progress);
    $lastId++;
    sleep(1);
}
.....


/**
 * Get progress of the file upload.
 * If id is not there yet, returns [0, PHP_INT_MAX]
 *
 * @param $id
 * @return array $bytesUploaded, $fileSize
 */
function getProgress($id) {
    // implement with your storage of choice
}

除非您放弃EventSource进行旧的良好拉拔,否则无法解决2个开放连接的问题。没有循环的stream.php的响应时间只有几毫秒,并且保持连接一直处于打开状态非常浪费,除非您每秒需要数百次更新。



 类似资料:
  • 问题内容: 题: 如何实时读取和回显正在服务器上写入的上传文件的文件大小,而又不会阻塞服务器和客户端? 内容: 文件上传进度被从写入到服务器请求所作,其中被设置为,,,或对象。 当前实现将对象设置为传递给的第二个参数的对象。 需求: 读取并向客户端发送写入服务器上文件系统的文件的文件大小。当所有字节(作为变量提供给脚本)作为请求中的查询字符串参数写入时,停止。当前,文件的读取是在单独的脚本环境中进

  • 本文向大家介绍Java实现文件上传服务器和客户端,包括了Java实现文件上传服务器和客户端的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了Java实现文件上传服务器和客户端的具体代码,供大家参考,具体内容如下 文件上传服务器端: 文件上传客户端: 本文已被整理到了《Java上传操作技巧汇总》,欢迎大家学习阅读。  以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持呐

  • 问题内容: 我在DWR中使用Spring。我想返回一个文件对象作为响应,但是我将文件(要发送)保存在服务器的临时位置,然后将其位置作为href发送给客户端的锚标记,但是我想知道是否有办法抛出该文件文件直接发送到响应对象上的浏览器,而无需将其临时保存在服务器上。 我希望是否有办法通过DWR发送文件作为响应。 问题答案:

  • 本文向大家介绍java模拟客户端向服务器上传文件,包括了java模拟客户端向服务器上传文件的使用技巧和注意事项,需要的朋友参考一下 本文实例为大家分享了java客户端向服务器上传文件的具体代码,供大家参考,具体内容如下 先来了解一下客户端与服务器Tcp通信的基本步骤: 服务器端先启动,然后启动客户端向服务器端发送数据。 服务器端收到客户端发送的数据,服务器端会响应应客户端,向客户端发送响应结果。

  • 我创建了一个示例客户机/服务器应用程序来熟悉Spring WebFlux/Reactor Netty。现在,当响应包含Flux并且媒体类型是“text/event-stream”时,我对客户端的行为有点困惑。我看到的是,服务器上产生的每个元素都被立即发送到客户机,但还没有交付给订户。在服务器端的生产者完成流量之后,第一次交付给订阅者。对我来说,这意味着所有元素首先在客户端的reactor-nett

  • 问题内容: 我想将一些文件上传到HTTP服务器。基本上,我需要的是对服务器的某种POST请求,其中包含一些参数和文件。我看到了仅上传文件的示例,但没有找到如何也传递其他参数的示例。 什么是最简单,免费的解决方案?有人有我可以学习的文件上传示例吗?我已经搜寻了几个小时,但是(也许只是那几天)找不到我真正需要的东西。最好的解决方案是不涉及任何第三方类或库的东西。 问题答案: 通常,你会用来触发HTTP