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/plain

This 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/plain

Hello, this is the content of test.txt.
------WebKitFormBoundaryABC123--

三、服务器处理流程

1. 解析步骤
  1. 读取 Content-Type 中的 boundary
  2. 按分隔符分割请求体。
  3. 解析每个字段的头部和内容。
2. Node.js 示例(原生实现.学习使用 该案例未做安全防护,未做文件分割,大文件会导致内存溢出.)
javascript 复制代码
const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer((req, res) => {
  if (req.method === 'POST') {
    // 获取 boundary
    const 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
相关推荐
Python私教2 小时前
把开源 Agent 打包成"解压双击即用"的 Windows 便携包:一条命令的完整实现
node.js
没事别瞎琢磨4 小时前
十一、审计与 Run Session——每一步操作都被记录
人工智能·node.js
没事别瞎琢磨4 小时前
十六、AgentSandbox——把所有模块串起来的编排类
人工智能·node.js
没事别瞎琢磨4 小时前
十二、网络代理与白名单规则引擎
人工智能·node.js
没事别瞎琢磨5 小时前
十四、Git Worktree 隔离执行
人工智能·node.js
没事别瞎琢磨6 小时前
十、统一 Runner 入口——能力检测与模式回退
人工智能·node.js
没事别瞎琢磨6 小时前
八、环境隔离——构建安全的子进程环境
人工智能·node.js
没事别瞎琢磨7 小时前
六、输出捕获与截断
人工智能·node.js
没事别瞎琢磨7 小时前
七、敏感路径预检——Protected Paths
人工智能·node.js
没事别瞎琢磨7 小时前
五、进程执行——spawn、超时与进程树清理
人工智能·node.js