Web服务器开发

一、认识 Stream 流


1. 什么是流

流(Stream)是连续字节数据的抽象 ,数据会像水流一样分段传输。

传统 readFile/writeFile 会一次性将整个文件加载到内存,大文件(视频、图片)极易造成内存溢出 。而流支持分段读写、暂停、恢复、精准控制读取位置,是处理大文件的最优方案。

所有流都是 EventEmitter 实例,Node.js 内置 4 种基础流:

  1. Readable:可读流(读取数据,如读取文件)
  2. Writable:可写流(写入数据,如写入文件)
  3. Duplex:双向流(同时可读可写,如网络套接字)
  4. Transform:转换流(读写时修改数据,如压缩)

下面重点讲解最常用的 Readable、Writable

2. Readable 可读流(读取文件)

使用 fs.createReadStream 创建可读流,核心参数:

  • start:读取起始位置(字节)
  • end:读取结束位置(字节)
  • highWaterMark:单次读取字节数,默认 64KB
js 复制代码
const fs = require('fs');

// 创建可读流:读取 foo.txt,从第3字节到第8字节,每次读4字节
const readStream = fs.createReadStream('./foo.txt', {
  start: 3,
  end: 8,
  highWaterMark: 4
});

// 文件打开事件
readStream.on('open', (fd) => {
  console.log('文件已打开');
});

// 分段读取数据(核心事件)
readStream.on('data', (data) => {
  console.log('读取到数据:', data.toString('utf-8'));
  // 暂停读取 2 秒
  readStream.pause();
  setTimeout(() => {
    readStream.resume(); // 恢复读取
  }, 2000);
});

// 读取完成
readStream.on('end', () => {
  console.log('文件读取完毕');
});

// 流关闭
readStream.on('close', () => {
  console.log('文件流已关闭');
});

3. Writable 可写流(写入文件)

使用 fs.createWriteStream 创建可写流,核心参数:

  • flagsw 覆盖写入、a+ 追加写入
  • start:写入起始位置

注意:可写流不会自动关闭 ,必须手动调用 close()end()

js 复制代码
const fs = require('fs');

// 创建可写流:追加写入,从第8字节开始写
const writeStream = fs.createWriteStream('./foo.txt', {
  flags: 'a+',
  start: 8
});

writeStream.on('open', () => {
  console.log('写入流已打开');
});

// 写入数据
writeStream.write('你好啊', (err) => {
  if (err) console.error('写入失败:', err);
  else console.log('片段写入成功');
});

// finish:所有数据写入完成(end 触发)
writeStream.on('finish', () => {
  console.log('全部内容写入完成');
});

// close:流真正关闭
writeStream.on('close', () => {
  console.log('写入流已关闭');
});

// end:写入收尾数据 + 自动关闭流(等价 write + close)
writeStream.end(' Hello World');

4. pipe 管道(流的拷贝)

pipe 是流的核心方法,自动将可读流数据流转到可写流 ,无需手动监听 data 事件,简化文件拷贝逻辑。

js 复制代码
const fs = require('fs');

// 源文件可读流、目标文件可写流
const read = fs.createReadStream('./foo.txt');
const write = fs.createWriteStream('./bar.txt');

// 一行代码完成文件拷贝
read.pipe(write);

read.on('end', () => {
  console.log('文件拷贝完成');
});

二、Node.js 原生 HTTP Web 服务


Node.js 内置 http 模块,无需第三方框架,即可快速搭建 Web 服务器。

1. 基础服务搭建

http.createServer 创建服务,回调包含两个核心对象:

  • req(Request):请求对象,存储客户端请求信息
  • res(Response):响应对象,用于给客户端返回数据

最简 Web 服务代码

js 复制代码
const http = require('http');
const PORT = 8000;

// 创建服务
const server = http.createServer((req, res) => {
  // 给客户端返回数据,end 表示响应结束
  res.end('Hello World');
});

// 监听端口和主机
// host: 0.0.0.0 允许局域网访问;127.0.0.1 仅本机访问
server.listen(PORT, '0.0.0.0', () => {
  console.log(`服务器启动成功,访问地址:http://localhost:${PORT}`);
});

2. 解析 Request 请求对象

通过 req 可获取 请求地址、请求方式、请求头、请求参数

2.1 路由分发(根据 URL 处理不同接口)

js 复制代码
const http = require('http');
const PORT = 8000;

const server = http.createServer((req, res) => {
  const url = req.url;
  // 根据路径路由
  if (url === '/login') {
    res.end('欢迎登录');
  } else if (url === '/products') {
    res.end('商品列表数据');
  } else {
    res.end('404 页面不存在');
  }
});

server.listen(PORT, () => {
  console.log(`服务运行在 ${PORT} 端口`);
});

2.2 解析 URL 拼接参数(query 参数)

使用内置 url + querystring 模块解析 ?name=xxx&age=xxx 格式参数:

js 复制代码
const http = require('http');
const url = require('url');
const qs = require('querystring');
const PORT = 8000;

const server = http.createServer((req, res) => {
  // 解析完整 URL
  const parseUrl = url.parse(req.url);
  const { pathname, query } = parseUrl;

  // 解析 query 字符串为对象
  const queryObj = qs.parse(query);
  console.log('请求参数:', queryObj);

  if (pathname === '/login') {
    res.end(`用户名:${queryObj.name},密码:${queryObj.password}`);
  } else {
    res.end('404');
  }
});

server.listen(PORT, () => {
  console.log(`服务启动:http://localhost:${PORT}`);
});

访问测试:http://localhost:8000/login?name=test&password=123456

2.3 获取 POST 请求 Body 数据

POST 数据放在请求体中,需监听 dataend 事件分段接收:

js 复制代码
const http = require('http');
const PORT = 8000;

const server = http.createServer((req, res) => {
  // 只处理 /users 接口 + POST 请求
  if (req.url === '/users' && req.method === 'POST') {
    req.setEncoding('utf-8'); // 设置编码为字符串
    let body = '';

    // 分段接收数据
    req.on('data', (chunk) => {
      body += chunk;
    });

    // 数据接收完成
    req.on('end', () => {
      // 解析 JSON 格式请求体
      const user = JSON.parse(body);
      console.log('接收POST数据:', user);
      res.end('创建用户成功');
    });
  } else {
    res.end('请求地址错误');
  }
});

server.listen(PORT, () => {
  console.log(`服务启动`);
});

3. 配置 Response 响应对象

3.1 响应状态码 + 响应头

常用状态码:200(成功)、201(创建成功)、404(未找到)、500(服务异常)

设置响应头:setHeader(单个)、writeHead(批量+状态码)

js 复制代码
const http = require('http');
const PORT = 8000;

const server = http.createServer((req, res) => {
  // 方式1:单独设置状态码 + 单个响应头
  // res.statusCode = 200;
  // res.setHeader('Content-Type', 'application/json; charset=utf-8');

  // 方式2:一次性设置状态码 + 所有响应头(推荐)
  res.writeHead(200, {
    'Content-Type': 'application/json; charset=utf-8'
  });

  // 分段返回数据
  res.write(JSON.stringify({ code: 200, msg: '请求成功' }));
  // 结束响应(必须调用,否则客户端一直等待)
  res.end();
});

server.listen(PORT, () => {
  console.log(`服务运行在 ${PORT}`);
});

三、Node.js 实现文件上传(原生写法)


文件上传请求头 Content-Typemultipart/form-data,数据包含分隔符 boundary、文件类型、二进制文件流,需要手动解析截取二进制数据。

js 复制代码
const http = require('http');
const fs = require('fs');
const PORT = 8000;

const server = http.createServer((req, res) => {
  if (req.url === '/upload' && req.method === 'POST') {
    // 1. 重点:不设置任何编码,保留原始 Buffer 二进制数据
    const contentType = req.headers['content-type'];
    const totalSize = Number(req.headers['content-length']);
    let currentSize = 0;
    // 使用 Buffer 数组存储二进制块,代替字符串
    const chunks = [];

    // 分段接收二进制流
    req.on('data', (chunk) => {
      currentSize += chunk.length;
      const progress = (currentSize / totalSize * 100).toFixed(2);
      res.write(`上传进度:${progress}%\r\n`);
      chunks.push(chunk);
    });

    req.on('end', () => {
      // 合并所有二进制块为完整 Buffer
      const buffer = Buffer.concat(chunks);
      const body = buffer.toString('latin1'); // 仅用来解析文本格式头,不处理图片

      try {
        // 提取 boundary 分隔符
        const boundary = contentType.split('; ').find(item => item.startsWith('boundary=')).split('=')[1];
        const boundaryStart = `--${boundary}`;
        const boundaryEnd = `--${boundary}--`;

        // 截取两个分隔符中间的内容(文件本体)
        const startIdx = body.indexOf(boundaryStart) + boundaryStart.length;
        const endIdx = body.indexOf(boundaryEnd, startIdx);
        const filePart = body.slice(startIdx, endIdx);

        // 分割报文头部 和 真正的文件二进制(multipart 头部和文件之间有 \r\n\r\n)
        const fileContent = filePart.split('\r\n\r\n')[1];
        // 转回二进制 Buffer(latin1 无损互转二进制)
        const fileBuffer = Buffer.from(fileContent, 'latin1');

        // 写入图片文件
        fs.writeFile('./upload.png', fileBuffer, (err) => {
          if (err) {
            res.writeHead(500);
            res.end('文件写入失败');
            console.error(err);
            return;
          }
          res.end('文件上传完成!可正常打开图片');
        });
      } catch (err) {
        res.writeHead(400);
        res.end('解析上传数据失败');
        console.error(err);
      }
    });

  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' });
    res.end('接口不存在');
  }
});

server.listen(PORT, () => {
  console.log(`文件上传服务启动:http://localhost:${PORT}`);
});

测试上传

可使用 Postman/ApiFox 发起请求:

  1. 请求地址:http://localhost:8000/upload
  2. 请求方式:POST
  3. 请求体:选择 form-data,添加文件字段并上传图片
  4. 服务根目录会生成 upload.png,即为上传后的文件。

四、总结


  1. Stream 流 :适合大文件分段读写,核心 ReadableWritablepipe 快速拷贝文件,避免内存溢出。
  2. HTTP 服务http.createServer 搭建服务,req 解析请求(URL、参数、Body),res 控制响应(状态码、响应头、返回数据)。
  3. 文件上传 :原生需手动解析 multipart/form-data 格式,截取二进制流写入本地;实际项目可使用 multer 等第三方库简化开发。
相关推荐
IT_陈寒4 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码5 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo5 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志6 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..6 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects6 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰6 小时前
一些基础和问题解决
后端
陈随易7 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN7 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana7 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端