通过JavaScript/Ajax上传文件至PHP服务器的UTF-8编码问题
核心问题在于你在前端到后端的传输过程中,不小心把二进制数据转换成了UTF-8字符串,导致原始字节被破坏。要实现1:1的原始数据传输与写入,需要从前端处理和后端接收两个环节同时入手,完全避开字符串编码转换的坑。
一、前端:直接传输二进制数据,避免字符串转换
不要再尝试把ArrayBuffer转成逗号分隔的数字字符串或者其他文本格式——这必然会引入编码问题。推荐两种可靠的方式:
方式1:用FormData传递Blob(最直观,支持文件上传的标准方式)
把文件分片读取成ArrayBuffer后,直接包装成Blob,通过FormData发送,浏览器会自动以二进制格式传输,不会做UTF-8转换:
// 读取文件分片为ArrayBuffer function readFileSlice(file, start, end) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => resolve(e.target.result); reader.onerror = reject; reader.readAsArrayBuffer(file.slice(start, end)); }); } // 上传分片 async function uploadSlice(file, sliceIndex, chunkSize = 1024*1024) { const start = sliceIndex * chunkSize; const end = Math.min(start + chunkSize, file.size); const arrayBuffer = await readFileSlice(file, start, end); const blob = new Blob([arrayBuffer], { type: 'application/octet-stream' }); const formData = new FormData(); formData.append('file_slice', blob); formData.append('slice_index', sliceIndex); formData.append('total_slices', Math.ceil(file.size / chunkSize)); formData.append('file_name', file.name); const res = await fetch('/upload_chunk.php', { method: 'POST', body: formData }); return await res.json(); }
方式2:直接发送ArrayBuffer(更轻量化)
如果不想用FormData,也可以直接把ArrayBuffer作为请求体发送,设置正确的Content-Type:
async function uploadRawSlice(arrayBuffer) { const res = await fetch('/upload_raw.php', { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: arrayBuffer }); return await res.json(); }
二、后端PHP:以二进制模式读写,拒绝字符串处理
你的PHP代码之前的问题在于,接收的是经过UTF-8编码的数字字符串,再通过pack还原时已经丢失了原始字节信息。正确的做法是直接接收原始二进制数据,并以二进制模式写入文件:
对应FormData方式的PHP处理(upload_chunk.php)
<?php // 确保PHP不会自动解析为UTF-8字符串 mb_internal_encoding('UTF-8'); // 不影响二进制数据处理,只是确保系统默认编码不干扰 // 获取分片文件的临时路径 $sliceTmpPath = $_FILES['file_slice']['tmp_name']; $sliceIndex = intval($_POST['slice_index']); $totalSlices = intval($_POST['total_slices']); $fileName = $_POST['file_name']; // 临时分片存储路径 $tempDir = './temp_uploads/'; if (!is_dir($tempDir)) mkdir($tempDir, 0755, true); $targetSlicePath = $tempDir . "{$fileName}_slice_{$sliceIndex}"; // 以二进制模式写入分片(关键:用wb模式) $fp = fopen($targetSlicePath, 'wb'); // 直接读取临时文件的原始二进制内容,无需任何编码转换 $rawContent = file_get_contents($sliceTmpPath); fwrite($fp, $rawContent); fclose($fp); // 如果是最后一个分片,合并所有分片 if ($sliceIndex === $totalSlices - 1) { $finalFilePath = "./uploads/{$fileName}"; $finalFp = fopen($finalFilePath, 'wb'); for ($i = 0; $i < $totalSlices; $i++) { $slicePath = $tempDir . "{$fileName}_slice_{$i}"; fwrite($finalFp, file_get_contents($slicePath)); unlink($slicePath); // 删除临时分片 } fclose($finalFp); rmdir($tempDir); } echo json_encode([ 'success' => true, 'message' => "分片 {$sliceIndex} 上传成功" ]); ?>
对应直接发送ArrayBuffer的PHP处理(upload_raw.php)
如果前端直接发二进制数据,PHP需要从php://input读取原始请求体:
<?php // 读取原始二进制请求体 $rawData = file_get_contents('php://input'); // 以二进制模式写入文件 file_put_contents('./temp_slice.bin', $rawData, FILE_BINARY); echo json_encode(['success' => true]); ?>
三、为什么你之前的方法会失败?
你把Int8Array转成逗号分隔的字符串(比如"80,75,3,4..."),这个过程本身没问题,但当这个字符串通过HTTP传输时,浏览器会把它当作UTF-8文本发送。PHP接收时,会把这个字符串按UTF-8解析,而某些负数对应的字符在UTF-8中是无效的,会被替换成乱码字符,导致explode后的数字序列已经不是原始的Int8Array值了,最终pack出来的二进制数据自然错误。
四、关于断点续传的补充
实现断点续传只需要在上传前,让前端请求后端已上传的分片索引,然后跳过这些分片即可。可以用文件的MD5作为唯一标识(前端可以通过SparkMD5等库计算文件MD5),后端记录每个文件已上传的分片列表。
内容的提问来源于stack exchange,提问作者Richard




