【6】深入学习http模块(万字)-Nodejs开发入门

深入学习http模块

前言

本章详细介绍了http模块的各部分概念和用途,包括创建一个简单的 Web 服务器、配置服务器属性、处理客户端请求以及响应客户端。

在学习本章之前,你最好具有一些前置的基础知识,如:计算机网络知识、Ajax或者Fetch请求、跨域相关知识等。

http

一个Web服务器

项目创建

javascript 复制代码
const http = require('node:http');

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

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

这是官方给出的一个案例,我们创建一个目录为:node-server,并创建index.js文件,将上面内容写入。

通过npm init创建一个package.json配置文件,根据提示填入相应的配置信息。

package.json中找到scripts配置,写入:"start": "node ./index.js"

代码运行

我们可以通过node index.js的方式运行上面的文件,但是为了方便操作,我们写入了npm命令脚本,脚本的内容就是node index.js,这样我们只需要运行start命令即可:

shell 复制代码
npm run start

运行之后,命令行会输出:

我们的Web服务就启动成功了,在浏览器打开 http://127.0.0.1:3000/,就可以看到Hello, World!

代码解析

我们来简单解析一下上面的代码,如果你有一定的Nodejs基础,这段代码是可以看得懂的:

  1. 导入http模块;
  2. 定义主机名和端口号;
  3. 创建Web服务,并在回调中设置响应状态、响应头和响应体;
  4. 监听设定的端口号,并在服务启动成功之后给出提示信息。

代码的核心在于http.createServerserver.listen它们分别用于创建服务和监听端口号。

我们对Web服务器的要求也很简单,能够接收客户端请求,并做出响应即可,这段代码正好符合我们的要求。

不过这段代码有点问题,比如,

  • 所有发送到localhost:3000的请求,无论路径和参数是什么,它总会返回Hello,World!
  • 如果发生异常,如大并发、网络缓慢、服务端出错等,它无法做出"人性化"反馈。

第一点是我们必须要考虑的问题,我们希望不同的请求路径响应不同的结果;

第二点对于普通项目来说需要考虑的场景并不是很多,但是我们也希望能够做出适当配置,让项目达到一个较好的运行状态。

Server

Server类用于创建一个http实例。

官方给出了http.server相关的方法和属性以及"事件",我们只需要学会常用的配置即可。

你可以将配置写在一个options对象中,并作为http.createServer的第一个参数传入,也可以将配置作为http实例的属性直接修改。

一般我们用第一种方式为服务设置通用配置信息,而第二种方式用于定制化项目配置,根据自己需要选择。

javascript 复制代码
const server = http.createServer({ ...options }, () => {});

属性:keepAlive

我们知道,http的无状态的,服务器无法知晓客户端状态,客户端每发送一次请求,客户端都会建立三次握手(SYN, SYN-ACK, ACK),这会消耗时间和资源。

假设你的官网加载了很多资源(图片、文本、视频...),服务器的握手会很占用时间,有没有一种可能,我们让服务器在某个设定的时间段内在客户端和服务器之间保持持久连接,从而允许在同一个 TCP 连接上发送多个 HTTP 请求和接收响应,而不是为每个请求都打开和关闭一个新的连接。

这种做法既可以保证连接的安全性,也极大提升了"高并发"下的加载速度。

HTTP/1.1中,keep-alive是默认行为,不需要显式设置 ,你可以在一些请求中看到相关的信息:

单个请求的KeepAlive并没有什么意义,我们会在学习完其他属性之后进行测试。

属性:keepAliveTimeout

为了"高并发"下的请求效率,浏览器默认启用了KeepAlive,我们不考虑其他非浏览器情况。

这里又会产生一个问题,开启KeepAlive之后,服务器需要分配一部分内存来管理连接,如果瞬时用户量很大,可能会造成服务器崩溃。

为了解决这个问题,我们约定,给KeepAlive一个时长,在这个时长范围内保持链接,超过时长则断开,这个时长默认为5000ms,当然你也可以通过设置keepAliveTimeout来自定义时长,它的单位为毫秒,当客户端发送请求在这个范围内时,会一直使用同一个连接。

连接会自动延长,当下一次请求触发之后,会自动顺延时长。

属性:maxHeaderSize

请求头的最大长度,没什么特殊的地方,默认长度为16384,也就是16k。

属性:requestTimeout

请求超时时长,默认是300000ms,也就是300秒,可以应付大部分需求,如果你需要上传大问题,可以在指定的请求内修改并覆盖。

属性:maxRequestsPerSocket

每次连接的最大请求数量,默认为0表示不限制,这个属性需要放在server实例上配置:

javascript 复制代码
server.maxRequestsPerSocket = 3;

方法:close()

关闭服务器本身,使其停止监听新的连接请求。

javascript 复制代码
server.close(() => {
	console.log('server on port 8000 closed successfully');
});

方法:closeAllConnections()

关闭所有连接到此服务器的连接,包括那些正在处理请求或等待响应的连接,以及空闲连接。

我们一般先调用server.close再调用server.closeAllConnections(),这样既能够保证清晰的逻辑顺序也能确保服务被正确关闭。

javascript 复制代码
server.closeAllConnections();

方法:setTimeout()

除了在创建服务时配置的timeout之外,还可以使用server.setTimeout方法进行设置。

javascript 复制代码
// 设置连接超时时间为 5000 毫秒(5 秒)
server.setTimeout(5000, () => {
  console.log('A connection was closed due to inactivity.');
});

方法:listen()

开始监听特定端口或路径,你可以监听多个端口来实现不同的项目。

javascript 复制代码
server.listen(3344, () => {
    console.log(`Server running at http://localhost:${3344}/`);
});

server.listen(1122, () => {
    console.log(`Server running at http://localhost:${1122}/`);
});

事件:connection

当有新的 TCP 连接时,'connection' 事件被触发,我们可以用connection时间来查看相关信息。

javascript 复制代码
server.on('connection', (socket) => {
    console.log('Connection connected!', socket.localAddress, socket.localPort, socket.remoteAddress, socket.remotePort);
});

事件:dropRequest

当连接的请求超过maxRequestsPerSocket的阈值时,连接会删除新的请求,并触发dropRequest,不过在浏览器端并不会发生该事件,而是会将请求分成多批次进行响应。

事件:request

每当有一个请求都会触发该事件,一个连接中的多个请求都会触发一次。

javascript 复制代码
server.on('request', (request) => {
    console.log('Received request', request.url);
});

案例

我们来写一个案例测试上面的配置:

javascript 复制代码
const http = require('node:http');

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

const server = http.createServer({
    keepAlive: true,
    keepAliveTimeout: 1000
},(req, res) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', '*');
    res.setHeader('access-control-allow-headers', '*');
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain');
    res.end('Hello, World!\n');
});
server.maxRequestsPerSocket = 3;
server.on('connection', (socket) => {
    console.log('Connection connected!', socket.localAddress, socket.localPort, socket.remoteAddress, socket.remotePort);
});
server.on('request', (request) => {
    console.log('Received request', request.url);
});
server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
});

我们启用了KeepAlive,并设置1000毫秒的持续时长,同时设置maxRequestsPerSocket为3表示一个连接最多传递3个请求。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <button onclick="button()">点击</button>
    <script>
        function button() {
            Promise.all([request(),request(),request(),request(),request()])
        }
        function request(url = 'http://127.0.0.1:3000'){
            fetch(url, {
                method: 'get',
                keepalive: true,
            })
        }
    </script>
</body>
</html>

我们在html中同时发送五个请求,我们来尝试运行,注意由于index.html并没有通过Nodejs的服务访问,因此存在跨域,我们在代码中做了简单处理。

当我们点击按钮,浏览器会发出五次请求,由于限定了maxRequestsPerSocket的大小,connection事件会被触发两次。

我们修改maxRequestsPerSocket为0,然后快速点击按钮:

javascript 复制代码
server.maxRequestsPerSocket = 0;

设置为0表示不限制单个连接的请求数量,由于点击速度在1秒内,因此除了第一次,后面每次点击无需再次建立连接:

在你最后一次点击之后,停留1秒之后再次点击,则会再次触发connection事件,我们设置的超时时间为1秒,超过一秒原来的连接会中断并建立新链接。

ClientRequest

ClientRequest是一个非常重要的类,用于表示一个正在进行中的 HTTP 客户端请求。

通俗一点就是,Nodejs向其它服务器发出请求时所创建的请求对象。

我们学习它是为了更好地理解后面的请求对象。

创建ClientRequest对象

我们通常使用http.request()方法创建ClientRequest对象,下面的是一个案例告诉你如何创建:

javascript 复制代码
const http = require('http');

const options = {
  hostname: 'www.example.com',
  port: 80,
  path: '/',
  method: 'GET'
};

const req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.');
  });
});

req.on('error', (e) => {
  console.error(`problem with request: ${e.message}`);
});

req.end();

我们通过http.requesthttp://www.example.com发出了一个get请求,并监听其回调。

回调函数接受一个参数,这个参数称为响应对象,类型为"IncomingMessage",在后面也会再次遇到。

实例方法

ClientRequest提供了多种方法来配置和发送请求,我们可以用这些方法来定制化请求。

  • setHeader(name, value):设置请求头的单个字段。
javascript 复制代码
req.setHeader('Content-Type', 'application/json');
  • getHeader(name):获取请求头的单个字段的值。
javascript 复制代码
const contentType = req.getHeader('Content-Type');
  • removeHeader(name):移除请求头的某个字段。
javascript 复制代码
req.removeHeader('Content-Type');
  • write(chunk[, encoding]):将数据写入请求体,get请求无效。
javascript 复制代码
req.write('{"name": "John Doe"}');
  • end([data][, encoding][, callback]):结束请求并发送数据,如果你用过write则无需再添加参数。
javascript 复制代码
req.end('{"email": "john.doe@example.com"}');
  • abort():终止请求。
javascript 复制代码
req.abort(); // 立即终止请求
  • setTimeout(timeout[, callback]):设置请求的超时时间。
javascript 复制代码
req.setTimeout(5000, () => {
  console.log('Request timed out');
  req.abort();
});

事件监听

我们可以通过监听事件在不同阶段做出一些操作。

  • 'response':当接收到响应头时触发,传递一个 http.IncomingMessage 对象作为参数。
javascript 复制代码
req.on('response', (res) => {
  console.log(`STATUS: ${res.statusCode}`);
});
  • 'socket':当为请求分配了一个 net.Socket 时触发。
javascript 复制代码
req.on('socket', (socket) => {
  console.log('Socket assigned.');
});
  • 'error':当请求过程中发生错误时触发。
javascript 复制代码
req.on('error', (e) => {
  console.error(`problem with request: ${e.message}`);
});
  • 'abort':当请求被终止时触发。
javascript 复制代码
req.on('abort', () => {
  console.log('Request aborted.');
});
  • 'timeout':当请求超时时触发。
javascript 复制代码
req.on('timeout', () => {
  console.log('Request timed out.');
  req.abort();
});

在实际开发中,我们一般会用第三方工具辅助开发,但是最基本的原理也要学会,这样才能应付出现的问题。

ServerResponse

ServerResponse用于表示 HTTP 服务器的响应对象。每当 HTTP 服务器接收到请求时,都会创建一个http.ServerResponse对象,因此我们无需手动创建该对象。

你可能会有疑问

在上面的案例中:

javascript 复制代码
const req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.');
  });
});

通过http.request创建请求的第二个参数表示回调,我们上面说了,这个回调函数的参数类型是IncomingMessage,按照我们正常的逻辑来说,它应该是响应对象,那为什么会是IncomingMessage呢?

如果你问出了这样的疑问,说明有思考,我们需要区分一下场景。

上面说到了Server,这是个服务器,服务器接受请求,发送响应;

上面说到了ClientRequest,这是个客户端工具,用于请求服务器;

现在说的ServerResponse,这是个中介,它在服务器叫ServerResponse,当它被发送到客户端之后称为IncomingMessage,因此很容易被迷惑。

如果再网上,在Server的案例中,有这样一段代码:

javascript 复制代码
server.on('request', (request) => {
    console.log('Received request', request.url);
});

注意,这里的request可不是ClientRequest,它只是被我们碰巧命名为request,为了区分,可以将它改为req

我们思考一下,IncomingMessage叫"即将到来的消息",对于服务器来说,发送过来的请求是"即将到来的消息",对于客户端来说发送过来的响应是"即将到来的消息"。我们要理清一下关系:

  1. 客户端通过ClientRequest创建一个请求;
  2. 请求发送到服务器,这个请求对象被包装变成Serverrequest事件的回调函数的参数,称为IncomingMessage,它用于获取请求信息;
  3. Serverrequest事件的回调函数还接受第二个参数,作为响应对象,也就是上面所说的ServerResponse,我们给ServerResponse进行配置之后,返回给客户端;
  4. 客户端通过ClientRequest的回调响应请求,拿到服务端发送回来的ServerResponse,此时它也被称为IncomingMessage

记住这个关系,后面还会遇到。

属性

作为一个响应对象,它会有下列常用属性:

  • statusCode,状态码;
  • statusMessage:状态消息
  • header:响应头(使用方法代替)
  • 响应体
javascript 复制代码
response.statusCode = 200;
response.setHeader('Content-Type', 'text/plain');
response.end('Goodbye!');

方法

  • setHeader(name, value),设置响应头的单个字段。
  • getHeader(name),获取响应头的单个字段的值。
  • removeHeader(name),移除响应头的某个字段。
  • write(chunk[, encoding][, callback]),向响应体写入数据块。
  • writeHead(statusCode[, statusMessage][, headers]),同时设置响应状态码、状态消息和响应头,然后准备发送响应体。
  • end([data][, encoding][, callback]),结束响应过程,并可选地发送数据块。

IncomingMessage

IncomingMessage用于表示 HTTP 请求(在服务器端)或 HTTP 响应(在客户端)。它提供了访问请求/响应头和读取请求/响应体的接口。

  • 在服务器端:
    • http.Server对象在接收到请求时自动创建,并通过回调函数的第一个参数传递给开发者。
  • 在客户端:
    • http.ClientRequest对象在接收到响应时自动创建,并通过response事件传递给开发者。

这个对象的相关属性和方法其实在上面基本上都讲过了,我们需要做的就是理清楚它在客户端与服务器之间具体的用途。

其他属性

http.METHODS

一个字符串数组,列出来所有能被解析的请求方法。

http.STATUS_CODES

包含了所有标准 HTTP 状态码及其对应的描述性文本。

javascript 复制代码
console.log(http.STATUS_CODES[200]); // 输出: 'OK'
console.log(http.STATUS_CODES[404]); // 输出: 'Not Found'
console.log(http.STATUS_CODES[500]); // 输出: 'Internal Server Error'

其他

通过上面的内容学习,我们能够实现一个简单的http服务器,它能够接收请求并做出响应,同时能够根据请求的url和method做出简单的路由,但是我们还有一些部分没有提到,如参数的解析,nodejs本身没有直接从body或者查询字符串提取数据的能力,需要我们自己进行提取,因此我们要在学完BufferStream之后再来解析参数。

相关推荐
90后的晨仔7 分钟前
在macOS上无缝整合:为Claude Code配置魔搭社区免费API完全指南
前端
沿着路走到底37 分钟前
JS事件循环
java·前端·javascript
子春一21 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶1 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied3 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一23 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记