【问题标题】:How to send binary file to the server using JavaScript如何使用 JavaScript 将二进制文件发送到服务器
【发布时间】:2021-01-20 20:48:59
【问题描述】:

我正在为我的 Messenger 进行文件加密,但在加密完成后我正在努力上传文件。

加密在性能方面看起来不错,但是当我尝试上传时,浏览器完全挂起。 Profiler 无限写入“small GC”事件,每 10 秒出现一次挂起脚本的黄条。

我已经尝试过的:

  1. 使用 FileReader 将文件读取到 ArrayBuffer,然后将其转换为基本 Array,对其进行加密,然后创建一个 FormData 对象,从数据中创建一个 File,将其附加到 FormData 并发送。当我没有进行加密时,它可以快速处理大小约为 1.3 Mb 的原始、未触及的文件,但在上传后加密的“假”文件对象上,我得到了 4.7 Mb 的文件并且它不可用。

  2. 作为普通 POST 字段发送(多部分表单数据编码)。以这种方式保存在PHP上后数据已损坏。

  3. 作为 Base64 编码的 POST 字段发送。最后,在我找到从二进制数组到 Base64 字符串的快速转换函数后,它开始以这种方式工作。 btoa() 在编码/解码后给出了错误的结果。但是在我尝试了一个 8.5 Mb 大小的文件后,它又挂了。

  4. 我尝试将额外数据移动到 URL 字符串并将文件作为 Blob 发送,如 here 所述。没有成功,浏览器仍然挂起。

  5. 我尝试向 Blob 构造函数传递一个基本数组,一个由它组成的 Uint8Array,最后我尝试按照文档中的建议发送文件,但结果仍然相同,即使是小文件。

代码有什么问题?发生此挂起时,HDD 负载为 0%。有问题的文件也非常小

当我按下按钮紧急终止 JS 脚本时,服务器脚本的输出如下所示:

Warning: Unknown: POST Content-Length of 22146226 bytes exceeds the limit of 8388608 bytes in Unknown on line 0

Warning: Cannot modify header information - headers already sent in Unknown on line 0

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent in D:\xmessenger\upload.php on line 2
Array ( ) 

这是我的 JavaScript:

function uploadEncryptedFile(nonce) {
    if (typeof window['FormData'] !== 'function' || typeof window['File'] !== 'function') return
    
    var file_input = document.getElementById('attachment')
    
    if (!file_input.files.length) return
    var file = file_input.files[0]
    
    var reader = new FileReader();
    
    reader.addEventListener('load', function() {
        var data = Array.from(new Uint8Array(reader.result))
        var encrypted = encryptFile(data, nonce)
        //return  //Here it never hangs
        var form_data = new FormData()
        form_data.append('name', file.name)
        form_data.append('type', file.type)
        form_data.append('attachment', arrayBufferToBase64(encrypted))
        /* form_data.append('attachment', btoa(encrypted)) // Does not help */
        form_data.append('nonce', nonce)
        var req = getXmlHttp()
        req.open('POST', 'upload.php?attachencryptedfile', true)
        req.onload = function() {
            var data = req.responseText.split(':')
            document.getElementById('filelist').lastChild.realName = data[2]
            document.getElementById('progress2').style.display = 'none'
            document.getElementById('attachment').onclick = null
            encryptFilename(data[0], data[1], data[2])
        }
        req.send(form_data)
        /* These lines also fail when the file is larger */
        /* req.send(new Blob(encrypted)) */
        /* req.send(new Blob(new Uint8Array(encrypted))) */
    })
    reader.readAsArrayBuffer(file)
}

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
}

这是我的 PHP 处理程序代码:

if (isset($_GET['attachencryptedfile'])) {
    $entityBody = file_get_contents('php://input');
    if ($entityBody == '') exit(print_r($_POST, true)); 
    else exit($entityBody);
    
    if (!isset($_POST["name"])) exit("Error");
    
    $name = @preg_replace("/[^0-9A-Za-z._-]/", "", $_POST["name"]);
    $nonce = @preg_replace("/[^0-9A-Za-z+\\/]/", "", $_POST["nonce"]);
    
    if ($name == ".htaccess") exit();

    $data = base64_decode($_POST["attachment"]);
    //print_r($_POST);
    //exit();
    if (strlen($data)>1024*15*1024) exit('<script type="text/javascript">parent.showInfo("Файл слишком большой"); parent.document.getElementById(\'filelist\').removeChild(parent.document.getElementById(\'filelist\').lastChild); parent.document.getElementById(\'progress2\').style.display = \'none\'; parent.document.getElementById(\'attachment\').onclick = null</script>');
    $uname = uniqid()."_".str_pad($_SESSION['xm_user_id'], 6, "0", STR_PAD_LEFT).substr($name, strrpos($name, "."));
    file_put_contents("upload/".$uname, $data);
    mysql_query("ALTER TABLE `attachments` AUTO_INCREMENT=0");
    mysql_query("INSERT INTO `attachments` VALUES('0', '".$uname."', '".$name."', '0', '".$nonce."')");
    exit(mysql_insert_id().":".$uname.":".$name);
}

HTML 表单:

<form name="fileForm" id="fileForm" method="post" enctype="multipart/form-data" action="upload.php?attachfile" target="ifr">
    <div id="fileButton" title="Прикрепить файл" onclick="document.getElementById('attachment').click()"></div>
    <input type="file" name="attachment" id="attachment" title="Прикрепить файл" onchange="addFile()" />
</form>

【问题讨论】:

  • 也许你应该显示一些代码......
  • 在后端加密然后保存呢?
  • 不幸的是我不能。信使的整个想法是E2E。我需要像 Telegram 这样的东西(他们用文件正确实现了它)

标签: javascript ajax file xmlhttprequest


【解决方案1】:

UPD:很遗憾,问题没有解决。我的回答只是部分正确。现在我在代码中犯了一个愚蠢的错误(忘记更新服务器端),我发现了另一个可能挂起的原因。如果我提交了一个基本的 POST 表单(x-www-urlencoded)并且 PHP 脚本中的代码尝试执行这一行(定义了 $uname,$_FILES 是一个空数组)

if (!copy($_FILES['attachment']['tmp_name'], "upload/".$uname)) exit("Error");

然后整个事情又挂了。如果我终止脚本,服务器响应是代码 200,并且正文内容很好(我的开发机器上有错误输出)。我知道这是一件坏事 - 使用第一个未定义的参数调用 copy,但即使服务器错误 500 也不能以这种方式挂起浏览器(顺便说一句,最新版本的 Firefox 也会受到影响)。

我在 Windows 7 x64 和 PHP 5.3 上安装了 Apache 2.4。有人可以验证这件事吗?也许应该向 Apache/Firefox 团队提交错误?


天哪。这种可怕的行为是由... post_max_size = 8M 在 php.ini 中设置的。并且小于 8 Mb 的文件实际上并没有挂起浏览器,正如我所想的那样。

最后一个问题是 - 为什么?为什么 Apache/PHP(我有 Apache 2.4 顺便说一句,它并不旧)不能以某种方式优雅地中止连接,告诉浏览器超出了限制?或者它可能是 XHR 实现中的一个错误,不适用于基本表单提交。无论如何,对偶然发现它的人很有用。

顺便说一句,我在 Chrome 中尝试了相同的 POST 大小限制,但它并没有像在 Firefox 中那样完全挂在那里(请求仍然处于“无响应可用”的挂起状态,但是 JS 引擎并且用户界面没有被阻止)。

【讨论】:

  • 在设置为发送100 Continue 的服务器中,将验证 POST 请求标头,并在开始时接受或拒绝请求。我不知道内置的 PHP 文件上传处理是否有能力,但这是理想的,这样当服务器无论如何都会拒绝它时,客户端不会花费几分钟上传数据。我怀疑在这种情况下,post max size 配置只是对传入的缓冲数据设置了一个限制,并且该模块可能不知道它将获得多少数据,直到它得到它。也就是说,它不检查标头。
  • 您始终可以直接在 PHP 代码中处理输入流,并绕过所有内置的 PHP 对请求数据的处理。如果您愿意,这将允许您关闭与响应的连接。
  • 我尝试使用 php://input(测试打印它),但问题仍然存在。此外,问题不在于浏览器发送所有 8.5 Mb 或 40 Mb 或其他任何内容(我正在 localhost 上测试应用程序,而且速度非常快)。是 Firefox JS 引擎中断并且表现得就像它进入了一个无限循环(感谢上帝,他们构建了每 10 秒弹出一次的黄色弹出窗口并建议停止正在运行的脚本)。如果我按下第二个“等待”按钮,我什至无法使用工具栏上的重新加载按钮,或 F5,或者在地址栏中按 Enter - 我可以编辑 URL,但重新加载永远不会发生
  • 我可以在 Firefox 54 之前的版本中期待这样的废话,当时它都是单进程,但我的版本是 62,所以 wtf 是错误的 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-12-23
  • 2011-02-10
  • 2012-09-10
  • 2013-02-12
  • 2013-10-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多