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 等第三方库简化开发。
相关推荐
Kir1to1 小时前
分布式锁基础与三种实现方式对比
后端
程序边界1 小时前
凌晨三点批量掉授权,我花了四小时才搞明白LAC心跳链路是怎么算的
后端
叫我:松哥1 小时前
基于Flask的在线考试刷题系统设计与实现,集智能练习、过程追踪、深度分析与个性化引导
数据库·人工智能·后端·python·flask·boostrap
AI人工智能_电脑小能手1 小时前
【大白话说Java面试题 第106题】【并发篇】第6题:synchronized 锁的锁对象可以是什么?
java·后端·面试
Rain5091 小时前
2.3. 安全配置:环境变量与 API 密钥管理
前端·人工智能·后端·安全·ai·node.js·ai编程
yinchnag1 小时前
Go 语言 map 底层实现
后端·源码阅读
MariaH1 小时前
Express框架使用
后端
MacroZheng1 小时前
横空出世!Claude Code画图神器来了,比Visio快10倍!
java·人工智能·后端
布局呆星1 小时前
Spring Boot + AOP 操作日志实战:自定义注解、切面编程、SecurityContext 全链路贯通,一次讲透
java·spring boot·后端