深入学习http模块
- 前言
- http
- 一个Web服务器
- 项目创建
- 代码运行
- 代码解析
- Server
- 属性:keepAlive
- 属性:keepAliveTimeout
- 属性:maxHeaderSize
- 属性:requestTimeout
- 属性:maxRequestsPerSocket
- 方法:close()
- 方法:closeAllConnections()
- 方法:setTimeout()
- 方法:listen()
- 事件:connection
- 事件:dropRequest
- 事件:request
- 案例
- ClientRequest
- 创建ClientRequest对象
- 实例方法
- 事件监听
- ServerResponse
- 属性
- 方法
- IncomingMessage
- 其他属性
- http.METHODS
- http.STATUS_CODES
- 其他
前言
本章详细介绍了http
模块的各部分概念和用途,包括创建一个简单的 Web 服务器、配置服务器属性、处理客户端请求以及响应客户端。
在学习本章之前,你最好具有一些前置的基础知识,如:计算机网络知识、Ajax或者Fetch请求、跨域相关知识等。
http
一个Web服务器
项目创建
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
命令即可:
npm run start
运行之后,命令行会输出:
我们的Web服务就启动成功了,在浏览器打开 http://127.0.0.1:3000/
,就可以看到Hello, World!
。
代码解析
我们来简单解析一下上面的代码,如果你有一定的Nodejs
基础,这段代码是可以看得懂的:
- 导入
http
模块; - 定义主机名和端口号;
- 创建Web服务,并在回调中设置响应状态、响应头和响应体;
- 监听设定的端口号,并在服务启动成功之后给出提示信息。
代码的核心在于http.createServer
和server.listen
它们分别用于创建服务和监听端口号。
我们对Web服务器的要求也很简单,能够接收客户端请求,并做出响应即可,这段代码正好符合我们的要求。
不过这段代码有点问题,比如,
- 所有发送到
localhost:3000
的请求,无论路径和参数是什么,它总会返回Hello,World!
; - 如果发生异常,如大并发、网络缓慢、服务端出错等,它无法做出“人性化”反馈。
第一点是我们必须要考虑的问题,我们希望不同的请求路径响应不同的结果;
第二点对于普通项目来说需要考虑的场景并不是很多,但是我们也希望能够做出适当配置,让项目达到一个较好的运行状态。
Server
Server
类用于创建一个http实例。
官方给出了http.server
相关的方法和属性以及“事件”,我们只需要学会常用的配置即可。
你可以将配置写在一个options
对象中,并作为http.createServer
的第一个参数传入,也可以将配置作为http
实例的属性直接修改。
一般我们用第一种方式为服务设置通用配置信息,而第二种方式用于定制化项目配置,根据自己需要选择。
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
实例上配置:
server.maxRequestsPerSocket = 3;
方法:close()
关闭服务器本身,使其停止监听新的连接请求。
server.close(() => {console.log('server on port 8000 closed successfully');
});
方法:closeAllConnections()
关闭所有连接到此服务器的连接,包括那些正在处理请求或等待响应的连接,以及空闲连接。
我们一般先调用server.close
再调用server.closeAllConnections()
,这样既能够保证清晰的逻辑顺序也能确保服务被正确关闭。
server.closeAllConnections();
方法:setTimeout()
除了在创建服务时配置的timeout
之外,还可以使用server.setTimeout
方法进行设置。
// 设置连接超时时间为 5000 毫秒(5 秒)
server.setTimeout(5000, () => {console.log('A connection was closed due to inactivity.');
});
方法:listen()
开始监听特定端口或路径,你可以监听多个端口来实现不同的项目。
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
时间来查看相关信息。
server.on('connection', (socket) => {console.log('Connection connected!', socket.localAddress, socket.localPort, socket.remoteAddress, socket.remotePort);
});
事件:dropRequest
当连接的请求超过maxRequestsPerSocket
的阈值时,连接会删除新的请求,并触发dropRequest
,不过在浏览器端并不会发生该事件,而是会将请求分成多批次进行响应。
事件:request
每当有一个请求都会触发该事件,一个连接中的多个请求都会触发一次。
server.on('request', (request) => {console.log('Received request', request.url);
});
案例
我们来写一个案例测试上面的配置:
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个请求。
<!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,然后快速点击按钮:
server.maxRequestsPerSocket = 0;
设置为0表示不限制单个连接的请求数量,由于点击速度在1秒内,因此除了第一次,后面每次点击无需再次建立连接:
在你最后一次点击之后,停留1秒之后再次点击,则会再次触发connection
事件,我们设置的超时时间为1秒,超过一秒原来的连接会中断并建立新链接。
ClientRequest
ClientRequest
是一个非常重要的类,用于表示一个正在进行中的 HTTP 客户端请求。
通俗一点就是,Nodejs
向其它服务器发出请求时所创建的请求对象。
我们学习它是为了更好地理解后面的请求对象。
创建ClientRequest对象
我们通常使用http.request()
方法创建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: ${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.request
向http://www.example.com
发出了一个get请求,并监听其回调。
回调函数接受一个参数,这个参数称为响应对象,类型为“IncomingMessage”,在后面也会再次遇到。
实例方法
ClientRequest
提供了多种方法来配置和发送请求,我们可以用这些方法来定制化请求。
- setHeader(name, value):设置请求头的单个字段。
req.setHeader('Content-Type', 'application/json');
- getHeader(name):获取请求头的单个字段的值。
const contentType = req.getHeader('Content-Type');
- removeHeader(name):移除请求头的某个字段。
req.removeHeader('Content-Type');
- write(chunk[, encoding]):将数据写入请求体,get请求无效。
req.write('{"name": "John Doe"}');
- end([data][, encoding][, callback]):结束请求并发送数据,如果你用过
write
则无需再添加参数。
req.end('{"email": "john.doe@example.com"}');
- abort():终止请求。
req.abort(); // 立即终止请求
- setTimeout(timeout[, callback]):设置请求的超时时间。
req.setTimeout(5000, () => {console.log('Request timed out');req.abort();
});
事件监听
我们可以通过监听事件在不同阶段做出一些操作。
- ‘response’:当接收到响应头时触发,传递一个 http.IncomingMessage 对象作为参数。
req.on('response', (res) => {console.log(`STATUS: ${res.statusCode}`);
});
- ‘socket’:当为请求分配了一个 net.Socket 时触发。
req.on('socket', (socket) => {console.log('Socket assigned.');
});
- ‘error’:当请求过程中发生错误时触发。
req.on('error', (e) => {console.error(`problem with request: ${e.message}`);
});
- ‘abort’:当请求被终止时触发。
req.on('abort', () => {console.log('Request aborted.');
});
- ‘timeout’:当请求超时时触发。
req.on('timeout', () => {console.log('Request timed out.');req.abort();
});
在实际开发中,我们一般会用第三方工具辅助开发,但是最基本的原理也要学会,这样才能应付出现的问题。
ServerResponse
ServerResponse
用于表示 HTTP 服务器的响应对象。每当 HTTP 服务器接收到请求时,都会创建一个http.ServerResponse
对象,因此我们无需手动创建该对象。
你可能会有疑问
在上面的案例中:
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
的案例中,有这样一段代码:
server.on('request', (request) => {console.log('Received request', request.url);
});
注意,这里的request可不是ClientRequest
,它只是被我们碰巧命名为request
,为了区分,可以将它改为req
。
我们思考一下,IncomingMessage
叫“即将到来的消息”,对于服务器来说,发送过来的请求是“即将到来的消息”,对于客户端来说发送过来的响应是“即将到来的消息”。我们要理清一下关系:
- 客户端通过
ClientRequest
创建一个请求; - 请求发送到服务器,这个请求对象被包装变成
Server
的request
事件的回调函数的参数,称为IncomingMessage
,它用于获取请求信息; Server
的request
事件的回调函数还接受第二个参数,作为响应对象,也就是上面所说的ServerResponse
,我们给ServerResponse
进行配置之后,返回给客户端;- 客户端通过
ClientRequest
的回调响应请求,拿到服务端发送回来的ServerResponse
,此时它也被称为IncomingMessage
。
记住这个关系,后面还会遇到。
属性
作为一个响应对象,它会有下列常用属性:
- statusCode,状态码;
- statusMessage:状态消息
- header:响应头(使用方法代替)
- 响应体
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 状态码及其对应的描述性文本。
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或者查询字符串提取数据的能力,需要我们自己进行提取,因此我们要在学完Buffer
和Stream
之后再来解析参数。