当前位置: 首页 > news >正文

1.11 HTTP 文件上传的核心协议

 HTTP 文件上传是 Web 开发中的常见需求,涉及到特殊的请求格式和处理机制。

一、HTTP 文件上传的核心协议

1. 两种主要方式
  • multipart/form-data(主流)
    支持二进制文件和表单字段混合传输,由 Content-Type 头部标识。
  • application/x-www-form-urlencoded(传统表单)
    仅支持文本数据,文件会被编码为 Base64(体积增大 33%),已逐渐淘汰。
2. 关键请求头
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 12345
  • boundary:分隔符,用于标记不同表单字段的边界。
  • Content-Length:请求体总长度(字节)。

3.请求体

请求体包含了实际要上传的数据。对于文件上传,数据被分割成多个部分,每部分由两部分组成:一部分是头部,描述了该部分的内容(如字段名和文件名),另一部分是实际的文件内容。每个部分都以--boundary开始,并以--boundary--结束

关键规则

  1. 字段头部与内容之间必须有空行
    空行由 \r\n\r\n(CRLF CRLF)组成,是协议的硬性规定。

  2. 分隔符与字段头部之间
    分隔符后可以紧跟字段头部(无需空行),但实际请求中可能存在一个换行符(取决于客户端实现)。

  3. 结束标记
    最后一个分隔符必须以 -- 结尾(如 -----------------------------1234567890--)。

二、请求报文结构详解

1. 基础格式
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=---------------------------1234567890-----------------------------1234567890
Content-Disposition: form-data; name="textField"Hello, World!
-----------------------------1234567890
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plainThis is the file content.
-----------------------------1234567890--
2. 核心组成部分
  • 分隔符(Boundary)
    由 Content-Type 中的 boundary 指定,用于分隔不同字段。
  • 字段头部(Headers)
    Content-Disposition: form-data; name="file"; filename="example.txt"
    Content-Type: text/plain
    
     
    • name:字段名(对应表单中的 name 属性)。
    • filename:文件名(可选,仅文件字段需要)。
    • Content-Type:文件 MIME 类型(默认 application/octet-stream)。
  • 字段内容(Body)
    文件的二进制数据或文本值。

 完整示例

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 12345------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"JohnDoe
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plainHello, this is the content of test.txt.
------WebKitFormBoundaryABC123--

三、服务器处理流程

1. 解析步骤
  1. 读取 Content-Type 中的 boundary
  2. 按分隔符分割请求体。
  3. 解析每个字段的头部和内容。
2. Node.js 示例(原生实现.学习使用 该案例未做安全防护,未做文件分割,大文件会导致内存溢出.)
const http = require('http');
const fs = require('fs');
const path = require('path');const server = http.createServer((req, res) => {if (req.method === 'POST') {// 获取 boundaryconst contentType = req.headers['content-type'];const boundary = `--${contentType.split('boundary=')[1]}`;const boundaryBuffer = Buffer.from(boundary);// 存储完整请求体(二进制形式)let requestBuffer = Buffer.from('');// 收集所有数据块(二进制形式)req.on('data', (chunk) => {requestBuffer = Buffer.concat([requestBuffer, chunk]);});req.on('end', () => {// 按 boundary 分割(使用二进制操作)const parts = splitBuffer(requestBuffer, boundaryBuffer);parts.forEach(part => {// 分离头部和内容(二进制形式)const headerEnd = part.indexOf('\r\n\r\n');if (headerEnd === -1) return;const headersBuffer = part.slice(0, headerEnd);const contentBuffer = part.slice(headerEnd + 4); // +4 跳过 \r\n\r\n// 解析头部(转换为字符串)const headers = parseHeaders(headersBuffer.toString());// 如果是文件,保存到磁盘if (headers.filename) {// 移除内容末尾的 \r\n--const endIndex = contentBuffer.indexOf('\r\n--');const fileContent = endIndex !== -1 ? contentBuffer.slice(0, endIndex) : contentBuffer;// 生成安全的文件名(添加时间戳)const ext = path.extname(headers.filename);const safeFilename = `${Date.now()}_${Math.random().toString(36).substring(2, 10)}${ext}`;const savePath = path.join(__dirname, 'uploads', safeFilename);// 直接写入二进制数据fs.writeFile(savePath, fileContent, (err) => {if (err) {console.error('保存文件失败:', err);res.statusCode = 500;res.end('服务器错误');}});console.log(`文件已保存: ${savePath}`);}});res.end('上传完成');});} else {//设置为utf-8编码res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });res.end(`<form method="post" enctype="multipart/form-data"><input type="file" name="file"><button type="submit">上传</button></form>`);}
});// 按 boundary 分割 Buffer
function splitBuffer(buffer, boundary) {const parts = [];let startIndex = 0;while (true) {const index = buffer.indexOf(boundary, startIndex);if (index === -1) break;if (startIndex > 0) {parts.push(buffer.slice(startIndex, index));}startIndex = index + boundary.length;// 检查是否到达末尾if (buffer.slice(index + boundary.length, index + boundary.length + 2).toString() === '--') {break;}}return parts;
}// 解析头部信息
function parseHeaders(headerText) {const headers = {};const lines = headerText.split('\r\n');lines.forEach(line => {if (!line) return;const [key, value] = line.split(': ');if (key === 'Content-Disposition') {const params = value.split('; ');params.forEach(param => {const [name, val] = param.split('=');if (val) headers[name] = val.replace(/"/g, '');});} else {headers[key] = value;}});return headers;
}// 创建上传目录
fs.mkdirSync(path.join(__dirname, 'uploads'), { recursive: true });server.listen(3000, () => {console.log('服务器运行在 http://localhost:3000');
});

四、 前端实现

  • 原生表单
    <form action="/upload" method="post" enctype="multipart/form-data"><input type="file" name="file"><input type="submit">
    </form>
    
  • AJAX 上传(使用 FormData
    const formData = new FormData();
    formData.append('file', fileInput.files[0]);fetch('/upload', {method: 'POST',body: formData
    });
    

五 总结

  • 协议核心multipart/form-data 格式通过分隔符实现多字段传输。
  • 安全要点:该案例不适合生产使用. 生产使用建议使用第三方库formidable 
http://www.xdnf.cn/news/999937.html

相关文章:

  • 小米CR660X/TR60X系列,获取SSH权限后刷openwrt系统
  • Linux中source和bash的区别
  • 树莓派5-ubuntu 24.04 安装 ros环境
  • linux 配置mvn
  • 创始人 IP 打造:心理学与家庭教育赛道知识变现新路径
  • LeetCode 热题 100 链表篇|Java 通关全攻略:从基础到进阶的 20 道核心题解(附完整思路与代码)
  • ARM SMMUv3命令和事件队列分析(四)
  • LeetCode 3423. Maximum Difference Between Adjacent Elements in a Circular Array
  • Haption遥操作机械臂解决方案通过高精度力反馈技术实现人机协同操作
  • elastalert实现飞书机器人告警-docker
  • Python爬虫实战:研究Crossbar相关技术
  • C/C++ 面试复习笔记(6)
  • 【测试开发】函数进阶-纯函数
  • 关于transceiver复位测试
  • 亚马逊关闭Posts:站内社交梦碎,卖家流量策略急待重构
  • Babylon.js场景加载器(Scene Loader)使用指南
  • 怎么把Dify部署在Windows系统上?
  • git merge合并分支push报错:Your branch is ahead of ‘xxx‘ by xx commits.
  • AI换衣技术实现原理浅析:基于图像合成的虚拟试衣实践
  • Python 爬虫入门 Day 1 - 网络请求与网页结构基础
  • WSGI(自用)
  • 解决npm install 一直卡着不动,npm install --verbose
  • TickIt:基于 LLM 的自动化 Oncall 升级
  • 相机Camera日志实例分析之三:相机Camx【视频光斑人像录制】单帧流程日志详解
  • 如何快速删除谷歌浏览器在mac启动台生成的网页图标
  • 42 C 语言随机数生成:rand() 与 srand() 深度解析、生成指定范围随机数、应用实战
  • xilinx的gtx使用qpll,是否可以实现4lane运行不同的线速率
  • 【力扣 简单 C】21. 合并两个有序链表
  • Python训练打卡Day48
  • Jenkins 配置gitlab的 pipeline流水线任务