MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js 使用 http 模块处理客户端请求

2021-05-074.8k 阅读

Node.js 的 http 模块概述

在 Node.js 的生态系统中,http 模块是构建网络服务器的核心基础。它提供了一种简单且高效的方式来创建 HTTP 服务器和客户端,使得 Node.js 能够轻松地与外界进行 HTTP 通信。这个模块内置于 Node.js,无需额外安装即可使用,极大地方便了开发者快速搭建基于 HTTP 协议的应用。

为何使用 http 模块

在 Web 开发领域,HTTP 协议是应用最为广泛的协议之一。无论是网页加载、API 交互还是各种网络服务通信,HTTP 无处不在。Node.js 的 http 模块让开发者能够在 JavaScript 的环境中直接处理 HTTP 相关的操作,充分利用 Node.js 的异步 I/O 特性,实现高性能、可扩展的网络应用。通过 http 模块,开发者可以完全掌控 HTTP 请求与响应的各个环节,从请求头解析到响应体生成,这为构建复杂的 Web 应用和服务端 API 提供了坚实的基础。

http 模块的核心组件

  1. http.Server:这是 http 模块中用于创建 HTTP 服务器的构造函数。通过实例化 http.Server,我们可以监听指定端口,接收客户端发来的 HTTP 请求,并进行相应的处理。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  // 处理请求
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. http.IncomingMessage:当服务器接收到一个 HTTP 请求时,会生成一个 http.IncomingMessage 的实例,它代表了客户端的请求。通过这个实例,我们可以获取请求的各种信息,如请求方法、请求头、请求 URL 等。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  console.log('Request method:', req.method);
  console.log('Request URL:', req.url);
  console.log('Request headers:', req.headers);
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. http.ServerResponse:这是服务器用于向客户端发送响应的对象。通过 http.ServerResponse 实例,我们可以设置响应头、响应状态码,并发送响应体。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, World!');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. http.ClientRequest:用于在 Node.js 中发起 HTTP 请求到其他服务器。通过 http.request 方法创建 http.ClientRequest 实例,我们可以设置请求方法、请求头,并发送请求体。例如:
const http = require('http');
const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};
const req = http.request(options, (res) => {
  console.log('Status code:', res.statusCode);
  console.log('Headers:', res.headers);
  res.on('data', (chunk) => {
    console.log('Received data:', chunk.toString());
  });
  res.on('end', () => {
    console.log('No more data in response.');
  });
});
req.end();
  1. http.Agent:它负责管理 HTTP 客户端的连接池。通过复用连接,http.Agent 可以显著提高性能,减少资源消耗。默认情况下,Node.js 会为每个 http.request 创建一个 http.Agent 实例。开发者也可以自定义 http.Agent,以满足特定的需求,比如设置最大连接数等。例如:
const http = require('http');
const agent = new http.Agent({ keepAlive: true, maxSockets: 10 });
const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET',
  agent: agent
};
const req = http.request(options, (res) => {
  // 处理响应
});
req.end();

处理客户端请求

请求方法

HTTP 定义了多种请求方法,常见的有 GETPOSTPUTDELETE 等。在 Node.js 的 http 模块中,通过 http.IncomingMessage 实例的 method 属性可以获取客户端使用的请求方法。

  1. GET 请求GET 请求通常用于从服务器获取资源。例如,当用户在浏览器中输入网址并回车时,浏览器会发送一个 GET 请求。在 http 模块中处理 GET 请求可以像这样:
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.method === 'GET') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is a GET request');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. POST 请求POST 请求常用于向服务器提交数据,比如用户注册、登录时提交的表单数据。与 GET 请求不同,POST 请求的数据通常包含在请求体中。要处理 POST 请求的数据,我们需要监听 dataend 事件。例如:
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
  if (req.method === 'POST') {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      const data = querystring.parse(body);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
    });
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 其他请求方法:对于 PUTDELETE 等请求方法,处理方式与 GETPOST 类似,只是在业务逻辑上有所不同。例如,PUT 方法通常用于更新资源,DELETE 方法用于删除资源。
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.method === 'PUT') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is a PUT request, used to update a resource');
  } else if (req.method === 'DELETE') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is a DELETE request, used to delete a resource');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

请求 URL 与路径

  1. 解析请求 URLhttp.IncomingMessage 实例的 url 属性包含了客户端请求的完整 URL。然而,在实际应用中,我们通常只关心路径部分。可以使用 url 模块来解析 URL。例如:
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  const pathname = parsedUrl.pathname;
  const query = parsedUrl.query;
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ pathname, query }));
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 路由:路由是根据请求的 URL 路径来决定执行不同的处理逻辑。我们可以通过简单的条件判断来实现基本的路由功能。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url === '/home') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is the home page');
  } else if (req.url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is the about page');
  } else {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('Not Found');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

随着应用的复杂性增加,可以使用专门的路由库,如 express 来管理路由,express 提供了更简洁、灵活的路由定义方式。

请求头

请求头包含了关于客户端请求的附加信息,如客户端类型、支持的内容类型、缓存控制等。通过 http.IncomingMessage 实例的 headers 属性可以获取请求头信息,它是一个包含所有请求头字段的对象。例如:

const http = require('http');
const server = http.createServer((req, res) => {
  console.log('Request headers:', req.headers);
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Headers have been logged');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 常见请求头字段
    • User - Agent:用于标识客户端的类型和版本。例如,浏览器发送的 User - Agent 可能包含浏览器名称、版本号、操作系统等信息。通过解析 User - Agent,服务器可以为不同类型的客户端提供不同的内容或功能。
    • Content - Type:指定请求体的数据类型。在处理 POST 请求时,根据 Content - Type 来决定如何解析请求体数据。常见的 Content - Typeapplication/x - www - form - urlencoded(表单数据)、application/json(JSON 数据)、multipart/form - data(用于文件上传等)。
    • Accept:表示客户端能够接受的响应内容类型。服务器可以根据这个字段来决定返回的数据格式,例如,如果客户端接受 application/json,服务器可以返回 JSON 格式的数据。
  2. 根据请求头进行处理:根据不同的请求头字段,服务器可以执行不同的逻辑。例如,根据 Accept 头返回不同格式的数据:
const http = require('http');
const server = http.createServer((req, res) => {
  const accept = req.headers.accept;
  if (accept.includes('application/json')) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: 'This is JSON data' }));
  } else if (accept.includes('text/plain')) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('This is plain text data');
  } else {
    res.writeHead(406, { 'Content-Type': 'text/plain' });
    res.end('Not Acceptable');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

请求体

请求体包含了客户端发送给服务器的数据,通常在 POSTPUT 等请求中使用。要处理请求体数据,需要监听 http.IncomingMessagedataend 事件。

  1. 处理 application/x - www - form - urlencoded 格式的数据:这种格式常用于表单提交。数据以 key1=value1&key2=value2 的形式编码在请求体中。可以使用 querystring 模块来解析这种格式的数据。例如:
const http = require('http');
const querystring = require('querystring');
const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.headers['content - type'] === 'application/x - www - form - urlencoded') {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      const data = querystring.parse(body);
      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(data));
    });
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 处理 application/json 格式的数据:在现代 Web 开发中,application/json 是非常常见的数据格式,用于 API 交互等场景。处理这种格式的数据需要将请求体中的字符串解析为 JavaScript 对象。可以使用 JSON.parse 方法。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.headers['content - type'] === 'application/json') {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
      } catch (error) {
        res.writeHead(400, { 'Content-Type': 'text/plain' });
        res.end('Invalid JSON data');
      }
    });
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 处理 multipart/form - data 格式的数据:这种格式常用于文件上传等场景,数据结构较为复杂。Node.js 没有内置的简单方法来处理 multipart/form - data,通常需要使用第三方库,如 busboy。以下是使用 busboy 处理文件上传的示例:
const http = require('http');
const Busboy = require('busboy');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.headers['content - type'].startsWith('multipart/form - data')) {
    const busboy = new Busboy({ headers: req.headers });
    busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
      const saveTo = path.join(__dirname, 'uploads', filename);
      const writeStream = fs.createWriteStream(saveTo);
      file.pipe(writeStream);
    });
    busboy.on('finish', () => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('File uploaded successfully');
    });
    req.pipe(busboy);
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

响应客户端

设置响应头

通过 http.ServerResponse 实例的 writeHead 方法可以设置响应头。writeHead 方法接受两个参数,第一个参数是状态码,第二个参数是包含响应头字段的对象。例如:

const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/html',
    'Cache-Control': 'no - cache',
    'Set - Cookie': 'username=John; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/'
  });
  res.end('<html><body><h1>Hello, World!</h1></body></html>');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 常见响应头字段
    • Content - Type:指定响应体的数据类型,如 text/htmlapplication/jsonimage/png 等。浏览器会根据这个字段来正确显示或处理响应数据。
    • Cache - Control:用于控制缓存行为。例如,no - cache 表示不使用缓存,每次都从服务器获取最新数据;max - age = 3600 表示缓存有效期为 3600 秒。
    • Set - Cookie:用于设置客户端的 Cookie。Cookie 可以存储用户相关的信息,如登录状态、用户偏好等。
  2. 动态设置响应头:根据不同的请求或业务逻辑,可以动态设置响应头。例如,根据请求的语言偏好设置 Content - Language 头:
const http = require('http');
const server = http.createServer((req, res) => {
  const lang = req.headers['accept - language'];
  let contentLanguage = 'en';
  if (lang && lang.includes('zh - CN')) {
    contentLanguage = 'zh - CN';
  }
  res.writeHead(200, {
    'Content-Type': 'text/plain',
    'Content - Language': contentLanguage
  });
  if (contentLanguage === 'zh - CN') {
    res.end('你好,世界!');
  } else {
    res.end('Hello, World!');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

设置响应状态码

响应状态码表示服务器对请求的处理结果。通过 http.ServerResponse 实例的 writeHead 方法的第一个参数来设置响应状态码。常见的状态码有:

  1. 2xx 成功:表示请求成功处理。例如,200 OK 表示请求成功,服务器已成功返回响应内容;201 Created 通常用于 POSTPUT 请求成功创建新资源时。
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(201, { 'Content-Type': 'text/plain' });
  res.end('Resource created successfully');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 3xx 重定向:表示需要客户端采取进一步的操作来完成请求。例如,301 Moved Permanently 表示资源已永久移动,客户端应使用新的 URL 进行后续请求;302 Found 表示资源临时移动,客户端应继续使用原 URL。
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(301, {
    'Location': 'https://www.new - location.com'
  });
  res.end();
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 4xx 客户端错误:表示客户端请求存在错误。例如,400 Bad Request 表示客户端请求语法错误,服务器无法理解;401 Unauthorized 表示请求需要身份验证,但客户端未提供有效的认证信息;404 Not Found 表示服务器找不到请求的资源。
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(404, { 'Content-Type': 'text/plain' });
  res.end('Resource not found');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 5xx 服务器错误:表示服务器在处理请求时发生错误。例如,500 Internal Server Error 表示服务器内部发生了错误,无法完成请求;503 Service Unavailable 表示服务器暂时不可用,通常是由于维护或过载等原因。
const http = require('http');
const server = http.createServer((req, res) => {
  try {
    // 模拟一个错误
    throw new Error('Internal server error');
  } catch (error) {
    res.writeHead(500, { 'Content-Type': 'text/plain' });
    res.end('Internal server error');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

发送响应体

  1. 使用 res.endhttp.ServerResponse 实例的 res.end 方法可以发送响应体并结束响应。它可以接受一个字符串、Buffer 或 null 作为参数。如果不传递参数,相当于发送一个空的响应体。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('This is the response body');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 使用 res.writeres.endres.write 方法用于逐步写入响应体数据,最后通过 res.end 结束响应。这种方式适用于响应体数据较大,需要分块发送的情况。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.write('Part 1 of the response body');
  res.write('Part 2 of the response body');
  res.end('Part 3 of the response body');
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 发送文件作为响应体:在实际应用中,经常需要将文件内容作为响应体发送给客户端,比如发送图片、文档等。可以使用 fs.createReadStream 方法创建可读流,并将其管道到 http.ServerResponse 实例。例如:
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'example.txt');
  const readStream = fs.createReadStream(filePath);
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  readStream.pipe(res);
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

错误处理

在处理客户端请求和响应过程中,可能会发生各种错误,如网络错误、请求解析错误、文件读取错误等。正确处理这些错误对于保证应用的稳定性和可靠性至关重要。

  1. 请求解析错误:例如,在处理 application/json 格式的请求体时,如果数据格式不正确,JSON.parse 会抛出错误。可以通过 try - catch 块来捕获这种错误,并返回合适的响应。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.headers['content - type'] === 'application/json') {
    let body = '';
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    req.on('end', () => {
      try {
        const data = JSON.parse(body);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify(data));
      } catch (error) {
        res.writeHead(400, { 'Content-Type': 'text/plain' });
        res.end('Invalid JSON data');
      }
    });
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 文件读取错误:当发送文件作为响应体时,如果文件不存在或无法读取,fs.createReadStream 可能会触发 error 事件。可以监听这个事件来处理错误。例如:
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'nonexistent.txt');
  const readStream = fs.createReadStream(filePath);
  readStream.on('error', (error) => {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('File not found');
  });
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  readStream.pipe(res);
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 服务器监听错误:在启动服务器时,可能会因为端口被占用等原因导致监听失败。可以通过 server.listen 的回调函数中的错误参数来捕获这种错误。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello, World!');
});
server.listen(3000, (error) => {
  if (error) {
    console.error('Server failed to listen:', error);
  } else {
    console.log('Server is running on port 3000');
  }
});

性能优化

  1. 连接池:通过 http.Agent 管理连接池可以复用连接,减少建立新连接的开销。在高并发场景下,这能显著提高性能。例如,设置 keepAlive: true 可以保持连接活跃,以便后续请求复用。
const http = require('http');
const agent = new http.Agent({ keepAlive: true, maxSockets: 10 });
const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET',
  agent: agent
};
const req = http.request(options, (res) => {
  // 处理响应
});
req.end();
  1. 缓存:合理使用缓存可以减少服务器的负载和响应时间。可以通过设置 Cache - Control 等响应头来控制客户端和中间代理的缓存行为。例如,对于不经常变化的静态资源,可以设置较长的缓存时间。
const http = require('http');
const server = http.createServer((req, res) => {
  if (req.url === '/static/file.css') {
    res.writeHead(200, {
      'Content-Type': 'text/css',
      'Cache - Control':'max - age = 3600'
    });
    // 发送 CSS 文件内容
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 异步处理:Node.js 的异步特性在处理请求时非常关键。尽量避免在请求处理过程中执行阻塞操作,如同步文件读取或数据库查询。使用异步操作并结合回调函数、Promise 或 async/await 来提高性能和响应能力。例如,使用 fs.readFile 的异步版本读取文件:
const http = require('http');
const fs = require('fs');
const path = require('path');
const util = require('util');
const server = http.createServer(async (req, res) => {
  const filePath = path.join(__dirname, 'example.txt');
  try {
    const data = await util.promisify(fs.readFile)(filePath, 'utf8');
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end(data);
  } catch (error) {
    res.writeHead(404, { 'Content-Type': 'text/plain' });
    res.end('File not found');
  }
});
server.listen(3000, () => {
  console.log('Server is running on port 3000');
});

通过深入理解和运用 Node.js 的 http 模块处理客户端请求的各个方面,开发者能够构建出高性能、可靠且功能丰富的 Web 应用和服务端 API。从请求的接收、解析到响应的生成、发送,以及错误处理和性能优化,每个环节都相互关联,共同构成了一个完整的 HTTP 服务器处理流程。在实际开发中,结合具体的业务需求和场景,灵活运用这些知识和技巧,能够为用户提供优质的网络体验。