Node.js HTTP 服务器进阶

核心知识点

高级路由处理

在实际应用中,我们需要处理更复杂的路由,包括:

  • 带参数的路由(如 /users/:id
  • 正则表达式路由
  • 路由中间件

静态文件服务

静态文件服务是 Web 服务器的重要功能,用于提供:

  • HTML 文件
  • CSS 文件
  • JavaScript 文件
  • 图片、字体等静态资源

请求体解析

对于 POST、PUT 等请求,需要解析请求体:

  • 表单数据(application/x-www-form-urlencoded
  • JSON 数据(application/json
  • 文件上传(multipart/form-data

会话管理

会话管理用于跟踪用户状态:

  • Cookie
  • Session
  • Token

错误处理

完善的错误处理机制:

  • 404 错误
  • 500 错误
  • 自定义错误页面

实用案例

案例一:高级路由处理

const http = require('http');
const url = require('url');

class Router {
  constructor() {
    this.routes = [];
  }

  // 注册路由
  add(method, path, handler) {
    this.routes.push({ method, path, handler });
  }

  // 处理请求
  handle(req, res) {
    const parsedUrl = url.parse(req.url, true);
    const pathname = parsedUrl.pathname;
    
    // 查找匹配的路由
    for (const route of this.routes) {
      if (route.method === req.method) {
        // 处理带参数的路由
        const match = this.matchPath(route.path, pathname);
        if (match) {
          // 将参数添加到请求对象
          req.params = match.params;
          req.query = parsedUrl.query;
          
          // 调用处理函数
          return route.handler(req, res);
        }
      }
    }

    // 没有匹配的路由
    this.handle404(req, res);
  }

  // 匹配路径
  matchPath(routePath, requestPath) {
    const routeParts = routePath.split('/').filter(Boolean);
    const requestParts = requestPath.split('/').filter(Boolean);

    if (routeParts.length !== requestParts.length) {
      return null;
    }

    const params = {};
    for (let i = 0; i < routeParts.length; i++) {
      const routePart = routeParts[i];
      const requestPart = requestParts[i];

      if (routePart.startsWith(':')) {
        // 捕获参数
        const paramName = routePart.substring(1);
        params[paramName] = requestPart;
      } else if (routePart !== requestPart) {
        // 路径不匹配
        return null;
      }
    }

    return { params };
  }

  // 处理 404 错误
  handle404(req, res) {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain');
    res.end('404 Not Found\n');
  }
}

// 创建路由器
const router = new Router();

// 注册路由
router.add('GET', '/', (req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

router.add('GET', '/users', (req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('User List\n');
});

router.add('GET', '/users/:id', (req, res) => {
  const userId = req.params.id;
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end(`User ID: ${userId}\n`);
});

router.add('GET', '/products/:category/:id', (req, res) => {
  const { category, id } = req.params;
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end(`Product: ${category}/${id}\n`);
});

// 创建服务器
const server = http.createServer((req, res) => {
  router.handle(req, res);
});

const hostname = '127.0.0.1';
const port = 3000;

server.listen(port, hostname, () => {
  console.log(`服务器运行在 http://${hostname}:${port}/`);
});

案例二:静态文件服务

const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');

// MIME 类型映射
const mimeTypes = {
  '.html': 'text/html',
  '.css': 'text/css',
  '.js': 'text/javascript',
  '.json': 'application/json',
  '.png': 'image/png',
  '.jpg': 'image/jpg',
  '.gif': 'image/gif',
  '.svg': 'image/svg+xml',
  '.wav': 'audio/wav',
  '.mp4': 'video/mp4',
  '.woff': 'application/font-woff',
  '.ttf': 'application/font-ttf',
  '.eot': 'application/vnd.ms-fontobject',
  '.otf': 'application/font-otf',
  '.wasm': 'application/wasm'
};

class StaticServer {
  constructor(rootDir) {
    this.rootDir = rootDir;
  }

  // 处理静态文件请求
  serve(req, res) {
    const parsedUrl = url.parse(req.url, true);
    let pathname = parsedUrl.pathname;

    // 防止路径遍历攻击
    pathname = path.normalize(pathname).replace(/^(/\.\.[/\\])+/, '');

    // 构建文件路径
    let filePath = path.join(this.rootDir, pathname);

    // 如果是目录,默认加载 index.html
    fs.stat(filePath, (err, stats) => {
      if (err) {
        this.handleError(err, res);
        return;
      }

      if (stats.isDirectory()) {
        filePath = path.join(filePath, 'index.html');
      }

      // 读取文件
      fs.readFile(filePath, (err, data) => {
        if (err) {
          this.handleError(err, res);
          return;
        }

        // 设置响应头
        const extname = path.extname(filePath);
        const contentType = mimeTypes[extname] || 'application/octet-stream';

        res.statusCode = 200;
        res.setHeader('Content-Type', contentType);
        res.setHeader('Content-Length', stats.size);
        res.end(data);
      });
    });
  }

  // 处理错误
  handleError(err, res) {
    if (err.code === 'ENOENT') {
      // 文件不存在
      res.statusCode = 404;
      res.setHeader('Content-Type', 'text/html');
      res.end('<h1>404 Not Found</h1><p>The requested file was not found.</p>');
    } else {
      // 服务器错误
      res.statusCode = 500;
      res.setHeader('Content-Type', 'text/html');
      res.end('<h1>500 Internal Server Error</h1><p>An error occurred while processing the request.</p>');
      console.error('服务器错误:', err);
    }
  }
}

// 创建静态服务器实例
const staticServer = new StaticServer(path.join(__dirname, 'public'));

// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
  // 处理静态文件请求
  staticServer.serve(req, res);
});

const hostname = '127.0.0.1';
const port = 3000;

server.listen(port, hostname, () => {
  console.log(`静态文件服务器运行在 http://${hostname}:${port}/`);
});

案例三:请求体解析

const http = require('http');
const url = require('url');

class RequestParser {
  // 解析请求体
  static parseBody(req) {
    return new Promise((resolve, reject) => {
      let body = '';
      
      req.on('data', chunk => {
        body += chunk;
        // 防止请求体过大
        if (body.length > 1e6) {
          req.connection.destroy();
          reject(new Error('Request body too large'));
        }
      });
      
      req.on('end', () => {
        resolve(body);
      });
      
      req.on('error', err => {
        reject(err);
      });
    });
  }

  // 解析表单数据
  static parseFormData(body) {
    const data = {};
    body.split('&').forEach(pair => {
      const [key, value] = pair.split('=');
      data[decodeURIComponent(key)] = decodeURIComponent(value || '');
    });
    return data;
  }

  // 解析 JSON 数据
  static parseJson(body) {
    try {
      return JSON.parse(body);
    } catch (error) {
      throw new Error('Invalid JSON');
    }
  }
}

// 创建服务器
const server = http.createServer(async (req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;

  if (pathname === '/api/form') {
    try {
      // 解析请求体
      const body = await RequestParser.parseBody(req);
      
      // 根据 Content-Type 解析数据
      let data;
      const contentType = req.headers['content-type'];
      
      if (contentType === 'application/x-www-form-urlencoded') {
        data = RequestParser.parseFormData(body);
      } else if (contentType === 'application/json') {
        data = RequestParser.parseJson(body);
      } else {
        data = { raw: body };
      }
      
      // 发送响应
      res.statusCode = 200;
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({
        message: 'Data received successfully',
        data: data
      }));
    } catch (error) {
      res.statusCode = 400;
      res.setHeader('Content-Type', 'application/json');
      res.end(JSON.stringify({
        error: error.message
      }));
    }
  } else if (pathname === '/form') {
    // 返回表单页面
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end(`
      <!DOCTYPE html>
      <html>
      <head>
        <title>Form Example</title>
      </head>
      <body>
        <h1>Form Example</h1>
        
        <h2>HTML Form</h2>
        <form action="/api/form" method="POST">
          <div>
            <label>Name:</label>
            <input type="text" name="name">
          </div>
          <div>
            <label>Email:</label>
            <input type="email" name="email">
          </div>
          <button type="submit">Submit</button>
        </form>
        
        <h2>JSON Form</h2>
        <button onclick="submitJson()">Submit JSON</button>
        
        <script>
          function submitJson() {
            fetch('/api/form', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({
                name: 'John Doe',
                email: 'john@example.com'
              })
            })
            .then(response => response.json())
            .then(data => console.log(data))
            .catch(error => console.error(error));
          }
        </script>
      </body>
      </html>
    `);
  } else {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello, World!\n');
  }
});

const hostname = '127.0.0.1';
const port = 3000;

server.listen(port, hostname, () => {
  console.log(`服务器运行在 http://${hostname}:${port}/`);
});

案例四:会话管理

const http = require('http');
const url = require('url');
const crypto = require('crypto');

class SessionManager {
  constructor() {
    this.sessions = {};
    this.secret = 'your-secret-key';
  }

  // 生成会话 ID
  generateSessionId() {
    return crypto.randomBytes(32).toString('hex');
  }

  // 创建会话
  createSession() {
    const sessionId = this.generateSessionId();
    this.sessions[sessionId] = {
      data: {},
      createdAt: Date.now(),
      lastAccessed: Date.now()
    };
    return sessionId;
  }

  // 获取会话
  getSession(sessionId) {
    if (this.sessions[sessionId]) {
      // 更新最后访问时间
      this.sessions[sessionId].lastAccessed = Date.now();
      return this.sessions[sessionId].data;
    }
    return null;
  }

  // 设置会话数据
  setSessionData(sessionId, key, value) {
    if (this.sessions[sessionId]) {
      this.sessions[sessionId].data[key] = value;
      this.sessions[sessionId].lastAccessed = Date.now();
      return true;
    }
    return false;
  }

  // 销毁会话
  destroySession(sessionId) {
    if (this.sessions[sessionId]) {
      delete this.sessions[sessionId];
      return true;
    }
    return false;
  }

  // 清理过期会话
  cleanupExpiredSessions(expiryTime = 3600000) { // 默认 1 小时
    const now = Date.now();
    for (const sessionId in this.sessions) {
      if (now - this.sessions[sessionId].lastAccessed > expiryTime) {
        delete this.sessions[sessionId];
      }
    }
  }

  // 解析 Cookie
  parseCookie(cookieHeader) {
    const cookies = {};
    if (cookieHeader) {
      cookieHeader.split(';').forEach(cookie => {
        const [name, value] = cookie.trim().split('=');
        cookies[name] = decodeURIComponent(value);
      });
    }
    return cookies;
  }

  // 设置 Cookie
  setCookie(res, name, value, options = {}) {
    let cookieString = `${name}=${encodeURIComponent(value)}`;
    
    if (options.maxAge) {
      cookieString += `; Max-Age=${options.maxAge}`;
    }
    
    if (options.expires) {
      cookieString += `; Expires=${options.expires.toUTCString()}`;
    }
    
    if (options.path) {
      cookieString += `; Path=${options.path}`;
    }
    
    if (options.domain) {
      cookieString += `; Domain=${options.domain}`;
    }
    
    if (options.secure) {
      cookieString += '; Secure';
    }
    
    if (options.httpOnly) {
      cookieString += '; HttpOnly';
    }
    
    res.setHeader('Set-Cookie', cookieString);
  }
}

// 创建会话管理器
const sessionManager = new SessionManager();

// 定期清理过期会话
setInterval(() => {
  sessionManager.cleanupExpiredSessions();
}, 60000); // 每分钟清理一次

// 创建服务器
const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;

  // 解析 Cookie
  const cookies = sessionManager.parseCookie(req.headers.cookie);
  let sessionId = cookies.sessionId;

  // 获取或创建会话
  if (!sessionId || !sessionManager.getSession(sessionId)) {
    sessionId = sessionManager.createSession();
    sessionManager.setCookie(res, 'sessionId', sessionId, {
      httpOnly: true,
      maxAge: 3600000 // 1 小时
    });
  }

  if (pathname === '/') {
    // 获取会话数据
    const sessionData = sessionManager.getSession(sessionId);
    const visitCount = sessionData.visitCount || 0;
    
    // 更新访问次数
    sessionManager.setSessionData(sessionId, 'visitCount', visitCount + 1);
    
    // 发送响应
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    res.end(`
      <!DOCTYPE html>
      <html>
      <head>
        <title>Session Example</title>
      </head>
      <body>
        <h1>Welcome!</h1>
        <p>You have visited this page ${visitCount + 1} times.</p>
        <p><a href="/reset">Reset Counter</a></p>
      </body>
      </html>
    `);
  } else if (pathname === '/reset') {
    // 重置会话数据
    sessionManager.setSessionData(sessionId, 'visitCount', 0);
    
    // 重定向回首页
    res.statusCode = 302;
    res.setHeader('Location', '/');
    res.end();
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/html');
    res.end('<h1>404 Not Found</h1>');
  }
});

const hostname = '127.0.0.1';
const port = 3000;

server.listen(port, hostname, () => {
  console.log(`服务器运行在 http://${hostname}:${port}/`);
});

学习目标

  1. 掌握高级路由处理:能够处理带参数的路由和复杂的路由规则
  2. 实现静态文件服务:学会提供 HTML、CSS、JavaScript 等静态资源
  3. 解析请求体:能够处理表单数据和 JSON 数据
  4. 管理会话:学会使用 Cookie 和 Session 管理用户状态
  5. 处理错误:能够优雅地处理 404 和 500 错误
  6. 构建完整服务器:能够将这些功能整合到一个完整的 Web 服务器中

代码优化建议

1. 使用模块化设计

不好的做法

// 所有代码都在一个文件中
const http = require('http');

// 路由处理
function handleRoute1(req, res) { ... }
function handleRoute2(req, res) { ... }

// 静态文件服务
function serveStatic(req, res) { ... }

// 会话管理
const sessions = {};
function manageSession(req, res) { ... }

// 服务器创建
const server = http.createServer((req, res) => {
  // 处理所有逻辑
});

好的做法

// router.js
class Router { ... }
module.exports = Router;

// static-server.js
class StaticServer { ... }
module.exports = StaticServer;

// session-manager.js
class SessionManager { ... }
module.exports = SessionManager;

// server.js
const http = require('http');
const Router = require('./router');
const StaticServer = require('./static-server');
const SessionManager = require('./session-manager');

// 使用模块
const router = new Router();
const staticServer = new StaticServer('./public');
const sessionManager = new SessionManager();

const server = http.createServer((req, res) => {
  // 处理请求
});

2. 使用流式处理大文件

不好的做法

fs.readFile(filePath, (err, data) => {
  if (err) {
    // 处理错误
  } else {
    res.end(data); // 一次性读取所有数据到内存
  }
});

好的做法

const stream = fs.createReadStream(filePath);
stream.pipe(res); // 使用流传输数据

stream.on('error', (err) => {
  // 处理错误
  res.statusCode = 500;
  res.end('Internal Server Error');
});

3. 实现中间件机制

不好的做法

const server = http.createServer((req, res) => {
  // 日志中间件
  console.log(`${new Date()} ${req.method} ${req.url}`);
  
  // 认证中间件
  if (!isAuthenticated(req)) {
    res.statusCode = 401;
    res.end('Unauthorized');
    return;
  }
  
  // 路由处理
  if (req.url === '/') {
    // 处理根路径
  } else if (req.url === '/api') {
    // 处理 API
  }
  
  // 错误处理
  // ...
});

好的做法

class App {
  constructor() {
    this.middlewares = [];
    this.routes = [];
  }

  use(middleware) {
    this.middlewares.push(middleware);
  }

  addRoute(method, path, handler) {
    this.routes.push({ method, path, handler });
  }

  handle(req, res) {
    let index = 0;
    
    const next = () => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        middleware(req, res, next);
      } else {
        // 处理路由
        this.handleRoute(req, res);
      }
    };
    
    next();
  }

  handleRoute(req, res) {
    // 路由处理逻辑
  }
}

// 使用
const app = new App();

// 添加中间件
app.use((req, res, next) => {
  console.log(`${new Date()} ${req.method} ${req.url}`);
  next();
});

app.use((req, res, next) => {
  if (!isAuthenticated(req)) {
    res.statusCode = 401;
    res.end('Unauthorized');
    return;
  }
  next();
});

// 添加路由
app.addRoute('GET', '/', (req, res) => {
  res.end('Hello World');
});

const server = http.createServer((req, res) => {
  app.handle(req, res);
});

常见问题与解决方案

问题1:路径遍历攻击

原因

  • 攻击者通过 ../ 等路径访问服务器上的敏感文件

解决方案

  • 规范化路径并验证路径安全性
  • 使用 path.normalize() 和路径验证

问题2:请求体过大

原因

  • 客户端发送过大的请求体,导致服务器内存不足

解决方案

  • 设置请求体大小限制
  • 使用流式处理大文件
  • 实现超时机制

问题3:会话管理安全

原因

  • 会话 ID 泄露
  • Cookie 未正确配置

解决方案

  • 使用 httpOnly Cookie
  • 设置合理的过期时间
  • 实现会话验证
  • 使用 HTTPS

问题4:性能优化

原因

  • 服务器响应速度慢
  • 资源消耗高

解决方案

  • 使用缓存
  • 压缩静态资源
  • 优化数据库查询
  • 使用集群模式

总结

通过本教程的学习,你应该能够:

  1. 实现高级路由处理,包括带参数的路由
  2. 提供静态文件服务,支持各种类型的静态资源
  3. 解析不同格式的请求体数据
  4. 实现会话管理,跟踪用户状态
  5. 构建模块化、可扩展的 Web 服务器
  6. 应用各种优化技巧提高服务器性能

这些进阶知识为你学习更高级的 Web 框架(如 Express、Koa)打下了坚实的基础。在后续章节中,我们将学习如何使用 Express 框架来更快速、更便捷地构建 Web 应用。

« 上一篇 Node.js HTTP 服务器基础 下一篇 » Node.js Express 框架入门