NodeJS 之 HTTP 模块(实现一个基本的 HTTP 服务器)
- 参考
- 描述
- HTTP 模块
- 搭建 HTTP 服务器
- http.createServer()
- 监听
- 检测服务器端口是否被占用
- 终端
- Error Code
- 超时处理
- 处理客户端的请求
- request 事件
- http.IncomingMessage
- http.ServerResponse
- 中文乱码问题
- 问题
- 解决
- 代码总汇
- 遗留问题
参考
项目 | 描述 |
---|---|
Node.js 权威指南 | 陆凌牛 |
NodeJS | API 官方文档 |
描述
项目 | 描述 |
---|---|
NodeJS | 18.13.0 |
操作系统 | Windows 10 专业版 |
HTTP 模块
HTTP 模块是 NodeJS 中核心的内置模块,你可以通过该模块搭建 HTTP 或 HTTPS 服务器,而无需像 PHP 等服务器端语言需要其他软件(Aphche、Nginx、IIS等)提供服务器功能。
你可以向运行在 NodeJS 运行时中的 JavaScript 文件中添加如下代码以导入该模块。
const http = require('http');
搭建 HTTP 服务器
http.createServer()
在 NodeJS 中,你可以通过 http.createServer() 创建一个服务器对象,我们可以通过这个服务器对象来实现许多功能。所以,请记住它:
http.createServer([, requestListener])
注:
- 你可以向 http.createServer() 方法传递一个回调函数以指定当服务器端接收到客户端的请求时需要执行的回调函数。该函数可以由两个形参,用以接收 http.IncomingMessage 对象(该对象可用于向客户端发送请求) 及 http.ServerResponse 对象(该对象代表一个服务器端响应对象)。
- 如果你没有为 http.createServer() 方法传递一个回调函数,那么你可以通过 http.createServer() 返回的服务器对象的 on 方法来监听 request 事件并为该事件指定当服务器端接收到客户端的请求时需要执行的回调函数。就像这样:
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
// 此处存放指定当服务器端接收到客户端的
// 请求时需要执行的 JavaScript 代码。
});
监听
你可以通过服务器对象的 listen() 来监听某个端口,计算机可以向这个端口发送请求与服务器进行交互。
server.listen(port, [host], [backlog], [callback]);
其中:
项目 | 描述 |
---|---|
port | 使用该参数为服务器指定需要监听的端口,如果提供的值为 0 或没有为该参数提供一个值,则 NodeJS 将为服务器随机分配一个端口号。 |
host | 用于指定服务器需要监听的地址,如果该省略该参数,则服务器将监听来自任何计算机的请求。 |
backlog | 用于指定位于等待队列中的客户端请求的最大数量,一旦超过设定的长度,服务器将拒绝新的连接请求。该参数的默认值为 511 。 |
callback | 用于指定服务器开始监听时需要调用的回调函数。 |
注:
当服务器开始监听后,将触发 listening 事件。如果你没有为 server.listen() 提供一个回调函数来指定服务器开始监听时需要进行的处理。那么,你可以通过 server.on() 方法来监听 listening 事件并为其指定服务器开始监听时需要调用的回调函数。
检测服务器端口是否被占用
在创建服务器时,你需要为服务器指定一个监听端口。如果该端口已经被其他应用所占用,则 NodeJS 将抛出错误。你应该对该错误进行捕获,否则,后续代码将无法正常执行。
你可以提前在终端中使用命令来检测某个端口是否已经被使用;你也可以通过 NodeJS 来对错误进行捕获,并在该错误是由于已经被其他应用所占用使用其他的端口号。
终端
在 Windows 终端中你可以输入如下命令来检测某个端口是否被占用:
netstat -ano | findstr <端口号>
举个栗子:
下面我们来尝试检测端口 3360 是否被占用:
注:
若执行命令后,终端中没有输出任何内容,则表明该端口没有被使用;
若执行命令后,终端中输出的内容与图中类似,则表明该端口已经被使用。
Error Code
除了可以使用命令来查看外,我们还可以通过对错误对象(可能是因为端口被占用而抛出的错误对象)的错误代码来判断是否是由于目标端口被占用而导致监听失败并对此进行处理(尝试使用另一个端口)。
const http = require('http');
// 创建服务器对象
const server = http.createServer();
// 默认监听端口 3360
server.listen(3360);
// 对错误进行捕获
server.on('error', (err) => {
if(err.code == 'EADDRINUSE'){
// 如果目标端口被占用将使用
// NodeJS 随机分配的端口号
server.listen(0);
}
});
// 在成功监听后,向终端输出被监听的端口号
server.on('listening', () => {
console.log('【HTTP Server is running at http://127.0.0.1:' + server.address().port + ' 】')
})
打印结果:
【服务器正在监听本机端口 56398】
注:
- 由于本机端口 3360 已被使用,所以该程序使用了 NodeJS 为其分配的随机端口 56398 。
- 我们可以通过 server.on() 方法来捕获错误,并在为该方法提供的回调函数中提供一个参数用以接收被捕获错误的错误对象。
- 错误对象的 code 属性包含了错误代码,当错误代码为 EADDRINUSE 时,表明该错误是由于被监听端口已经被占用所产生的。
- EADDRINUSE 的全称应该为 Error Address In Use ,记住它有助于记住错误代码。
- 使用 server.listen() 或 server.listen(0) 在目标端口被占用的情况下,使用由 NodeJS 提供的随机端口。如果随机端口仍被占用,则将继续触发错误代码为 EADDRINUSE 的错误,并再次使用 NodeJS 提供的随机端口,直到成功的监听了一个端口。
超时处理
处理客户端的请求
在服务器对本机端口监听后,客户端便可以通过本机 IP 地址及端口号对本机进行访问。此时我们并没有对来自客户端的请求进行处理,所以此时如果你访问服务器,你可能会看到空白的网页以及一个永不停止的进度条(这是由于我们没有对客户端进行响应而导致的):
我们可以通过监听 request 事件来对客户端的请求进行响应,也可以通过 http.createServer() 创建服务器对象时,为该函数提供一个回调函数来处理客户端的请求。
这里我们将通过监听 request 事件来对客户端的请求进行响应。
request 事件
server.on('request', (req, res) => {});
你可以向 server.on() 方法传递一个回调函数以指定当服务器端接收到客户端的请求时需要执行的回调函数。该函数可以由两个形参,分别用以接收 http.IncomingMessage 对象(该对象可用于向客户端发送请求) 及 http.ServerResponse 对象(该对象代表一个服务器端响应对象)。
http.IncomingMessage
该对象的部分属性及方法如下:
项目 | 描述 |
---|---|
headers | 该属性指向一个对象,其中包含了客户端的请求头信息。 |
httpVersion | 客户端使用的 HTTP 版本号。 |
method | 客户端使用的请求方式(GET、POST 等)。 |
url | 客户端请求的 URL 地址的路径及参数信息(不包含主机地址及端口号等其他信息)。 |
http.ServerResponse
该对象的部分属性及方法如下:
项目 | 描述 |
---|---|
setHeader() | 你可以通过该方法设置服务器端的请求头信息。该方法接收两个参数,第一个参数为响应字段,第二个为响应字段值。你可以传递一个数值来设置多个响应字段值。 |
writeHead() | 该方法接收两个参数,第一个参数用于设置响应状态码;你需要为第二个参数提供一个对象,用于设置服务器端的请求信息。该方法与 setHeader() 不同的是,该方法一次可以设置多个请求字段,而 setHeader 仅能一次仅能设置单个请求头字段。 |
getHeader() | 你可以通过向该方法传递一个响应字段,用于查看其对应的响应字段值。 |
removeHeader() | 你可以通过向该方法传递一个响应字段,NodeJS 将从响应头中删除该字段。 |
headersSent | 如果响应信息已被发送,则该属性的值将为 true,否则将为 false 。 |
statusCode | 该属性可用于设置响应状态码。 |
write() | 该方法接收三个参数,仅第一个参数为必填项;第一个参数为将发送的响应信息,第二个参数为响应信息的编码方式,第三个参数为发送成功后需要执行的回调函数。 |
end() | 此方法与 write() 方法类似,但此方法还能够向服务器发出信号,表明所有响应头和正文都已发送。 |
注:
- 如果你在一个响应中使用了 write() 及 end() 则 end() 方法应该位于所有 write() 之后,否则将有部分 write() (位于 end() 之后的 write())将无法发挥其功能。例如:
const http = require('http');
// 创建服务器对象
const server = http.createServer();
// 默认监听端口 3360
server.listen(3360);
// 对错误进行捕获
server.on('error', (err) => {
if(err.code == 'EADDRINUSE'){
// 如果目标端口被占用将使用
// NodeJS 随机分配的端口号
server.listen(0);
}
});
// 在成功监听后,向终端输出被监听的端口号
server.on('listening', () => {
console.log('【HTTP Server is running at http://127.0.0.1:' + server.address().port + ' 】')
})
// 对客户端的请求进行响应
server.on('request', (req, res) => {
res.write('<h1>Hello World</h1>', () => {
console.log('win');
});
res.write('<h3>Hello NodeJS</h3>')
})
效果:
而如果我们将该代码中的第一个 write() 使用 end() 进行替换:
// 对客户端的请求进行响应
server.on('request', (req, res) => {
res.end('<h1>Hello World</h1>', () => {
console.log('win');
});
res.write('<h3>Hello NodeJS</h3>')
})
效果:
- 你应该为所有的响应使用 end() 方法,这有利于节约服务器的资源。
中文乱码问题
问题
如果你直接向客户端发送包含中文的响应数据,将会产生中文乱码问题:
const http = require('http');
// 创建服务器对象
const server = http.createServer();
// 默认监听端口 3360
server.listen(3360);
// 对错误进行捕获
server.on('error', (err) => {
if(err.code == 'EADDRINUSE'){
// 如果目标端口被占用将使用
// NodeJS 随机分配的端口号
server.listen(0);
}
});
// 在成功监听后,向终端输出被监听的端口号
server.on('listening', () => {
console.log('【HTTP Server is running at http://127.0.0.1:' + server.address().port + ' 】')
})
// 对客户端的请求进行响应
server.on('request', (req, res) => {
res.write('<h1>Hello World</h1>', () => {
console.log('win');
});
// 向客户端发送包含中文的响应内容
res.end('<h3>Hello 中国</h3>')
})
效果:
解决
你可以通过设置合适的响应头信息来使客户端的浏览器正确解析中文字符以解决中文乱码问题。
const http = require('http');
// 创建服务器对象
const server = http.createServer();
// 默认监听端口 3360
server.listen(3360);
// 对错误进行捕获
server.on('error', (err) => {
if(err.code == 'EADDRINUSE'){
// 如果目标端口被占用将使用
// NodeJS 随机分配的端口号
server.listen(0);
}
});
// 在成功监听后,向终端输出被监听的端口号
server.on('listening', () => {
console.log('【HTTP Server is running at http://127.0.0.1:' + server.address().port + ' 】')
})
// 对客户端的请求进行响应
server.on('request', (req, res) => {
// 通过响应头告知客户端发送内容的文本类型及编码方式
res.setHeader('content-type', 'text/html; charset=utf-8')
res.write('<h1>Hello World</h1>', () => {
console.log('Win');
});
// 向客户端发送包含中文的响应内容
res.end('<h3>Hello 中国</h3>')
})
效果:
代码总汇
const http = require('http');
// 创建服务器对象
const server = http.createServer();
// 默认监听端口 3360
server.listen(3360);
// 对错误进行捕获
server.on('error', (err) => {
if(err.code == 'EADDRINUSE'){
// 如果目标端口被占用将使用
// NodeJS 随机分配的端口号
server.listen(0);
}
});
// 在成功监听后,向终端输出被监听的端口号
server.on('listening', () => {
console.log('【HTTP Server is running at http://127.0.0.1:' + server.address().port + ' 】')
})
// 对客户端的请求进行响应
server.on('request', (req, res) => {
// 通过响应头告知客户端发送内容的文本类型及编码方式
res.setHeader('content-type', 'text/html; charset=utf-8')
res.write('<h1>Hello World</h1>', () => {
console.log('Win');
});
// 向客户端发送包含中文的响应内容
res.end('<h3>Hello 中国</h3>')
})
遗留问题
在执行最终代码后,你若对服务器进行访问,服务器将打印两次 Win。也就是说 访问一次服务器将触发两次 request 事件
,具体原因暂不明确。如有知道的朋友,还望不吝赐教。