# Node.js 内置模块笔记

参考

# child_process

child_process 模块允许打开一个子进程去执行其他任务,该功能使 node 程序可以执行指定的 shell 脚本,可以用于自动化部署、定时任务

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']); // 执行 ls -lh /usr 命令

ls.stdout.on('data', (data) => {
  // ls 产生的 terminal log 在这里 console
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  // 如果发生错误,错误从这里输出
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  // 执行完成后正常退出就是 0 
  console.log(`child process exited with code ${code}`);
});

执行写好的 shell 脚本

# deploy.sh
# chmod +x deploy.sh 注意要加可执行权限
# 执行 ./deploy.sh  或者 sh ./deploy.sh

echo "开始部署"

# 显示当前执行路径,用于排查路径错误
pwd 

# 拉取最新代码
git pull

# pm2 重启服务 node 服务,也可以是其他逻辑
pm2 stop xxx
pm2 start src/index.js -n 'xxx'

echo "完成部署"

用于执行 shell 脚本的 node 程序

// deploy.js
const { spawn } = require('child_process');
const child = spawn('sh', ['./deploy.sh']); // sh ./deploy.sh 运行脚本

child.stdout.on('data', (data) => {
  // 运行命令产生的 terminal log 在这里 console
  console.log(`stdout: ${data}`);
});

child.stderr.on('data', (data) => {
  // 如果发生错误,错误从这里输出
  console.error(`stderr: ${data}`);
});

child.on('close', (code) => {
  // 执行完成后正常退出就是 0 
  console.log(`child process exited with code ${code}`);
});

# http

# http.ClientRequest 类 http.get()、http.request()

可以使用 http.get()、http.request() 发送 http 请求。这两个函数返回 http.ClientRequest 对象

  • http.request(url[, options][, callback]) 发送 http 请求
  • http.get(url[, options][, callback]) 发送 get 请求方式的 http 请求,自动调用 req.end()

http.ClientRequest 对象(假设命名为 request,一般简写为 req) 支持以下方法、事件

  • request.write(chunk[, encoding][, callback]) Sends a chunk of the body 发送一个请求主体(body)的数据块。POST 传送数据时使用
  • request.end([data[, encoding]][, callback]) 完成发送请求。 req.end(data, encoding, cb) 相当于调用 req.write(data, encoding) 后再调用 req.end(cb)
  • request.destroy([error]) 销毁请求,用于替代之前的 request.abort()
  • error 事件 req.on('error', cb) 如果请求出错,需要监听该事件接收错误,使用 try/catch 是无法捕获错误的。

发送 GET 请求

const http = require('http')
const req = http.request('http://fe.zuo11.com', {
  // hostname: 'localhost',
  // port: 80,
  // agent: false, // 是否使用代理
  path: '/'
}, (res) => {
  const { statusCode } = res;
  const contentType = res.headers['content-type'];
  console.log(statusCode, contentType) // 200 text/html; charset=utf-8
  res.setEncoding('utf8');
  let rawData = '';
  res.on('data', (chunk) => { rawData += chunk; });
  res.on('end', () => {
    // res 文本数据,如果是 JSON 字符串数据,需使用 JSON.parse(rawData);
    console.log(rawData)
  });
}).on('error', (e) => {
  // 请求返回的 http.ClientRequest 类,可以监听上面的一些方法
  console.error(`请求出现错误: ${e.message}`);
});
req.end() // 必须

由于大多请求是不带请求体(body data)的 GET 请求,于是 Node.js 提供了更便捷的 http.get() 方法,和 http.request 的区别是无法设置 method 为 POST 等,而且内部会自动调用 req.end() 完成发送请求。

const http = require('http')
http.get('http://fe.zuo11.com', {
  // hostname: 'localhost',
  // port: 80,
  // agent: false, // 是否使用代理
  path: '/'
}, (res) => {
  const { statusCode } = res;
  const contentType = res.headers['content-type'];
  console.log(statusCode, contentType) // 200 text/html; charset=utf-8
  res.setEncoding('utf8');
  let rawData = '';
  res.on('data', (chunk) => { rawData += chunk; });
  res.on('end', () => {
    // res 文本数据,如果是 JSON 字符串数据,需使用 JSON.parse(rawData);
    console.log(rawData)
  });
}).on('error', (e) => {
  // 请求返回的 http.ClientRequest 类,可以监听上面的一些方法
  console.error(`请求出现错误: ${e.message}`);
});

发送 POST 请求,如果需要在请求体携带数据,注意设置 Content-Type 请求头

const http = require('http')
const querystring = require('querystring')
const req = http.request('http://127.0.0.1', {
  path: '/user',
  port: 8000,
  method: 'POST',
  headers: {
    // 'Content-Type': 'application/json',
    'Content-Type': 'application/x-www-form-urlencoded',
    // 'Content-Length': Buffer.byteLength(postData)
    'Referer': 'www.zuo11.com'
  }
}, (res) => {
  // res IncomingMessage
  const { statusCode } = res;
  const contentType = res.headers['content-type'];
  console.log(statusCode, contentType) // 200 application/json; charset=utf-8
  res.setEncoding('utf8');
  let rawData = '';
  res.on('data', (chunk) => { rawData += chunk; });
  res.on('end', () => {
    // res 文本数据,如果是 JSON 字符串数据,需使用 JSON.parse(rawData);
    console.log(rawData) // {"code":200,"msg":"Success","data":{"b":1}}
  });
}).on('error', (e) => {
  // 请求返回的 http.ClientRequest 类,可以监听上面的一些方法
  console.error(`请求出现错误: ${e.message}`);
});
// req.write(JSON.stringify({ a: 1, b: 2 }))
req.write(querystring.stringify({ a: 1, b: 2 })) // 'a=1&b=2'
req.end() // 必须

对应的 koa 接口服务

const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
app.use(require('koa-bodyparser')())
const router = new Router()

router.post('/user', ctx => {
  console.log(ctx)
  console.log(ctx.request.body)
  ctx.body = {
   code: 200,
   msg: 'Success',
   data: {
     b: 1
   }
  }
})

app.use(router.routes())
app.listen(8000, () => console.log('server listen on 8000 port'))

注意:使用 http 模块发送请求时,可以伪造 Referer。上面的测试中,在 Koa 里可以接收到 headers 参数

referer伪造.png

# http.Server 类 http.createServer()

http 模块除了可以发送 http 请求外,还可以使用创建 http 服务,监听处理 http 请求。使用 http.createServer() 创建 http 服务,返回 http.Server 实例,该实例调用 listen 方法开始监听服务

const http = require('http');

const server = http.createServer((req, res) => {
  // console.log('req', req) // req IncomingMessage
  // console.log('res', res) // res ServerResponse 
  res.end('123'); // 接收到请求后,返回 "123"
});
console.log(server)
server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.listen(8000);

# http.ServerResponse 类

http.createServer 用于 http 模块在接收到 http 请求后,响应数据。是 http.createServer() 回调函数的第二个参数。

// http.ServerResponse 类实例 response
response.end([data[, encoding]][, callback])

# http.IncomingMessage 类

http.IncomingMessage 类有两个常见的用处

  1. 在接收到 http 请求时,用于接收请求信息,是 http.createServer() 回调函数的第一个参数。
  2. 在发送 http 请求后,用于接收响应数据,是http.request() 和 http.get() 回调函数的参数

用法大致如下

const http = require('http')
const querystring = require('querystring')
const req = http.request('http://127.0.0.1', {
  path: '/',
}, (res) => {
  console.log(res) // IncomingMessage
  const { statusCode } = res;
  const contentType = res.headers['content-type'];
  console.log(statusCode, contentType)
  res.setEncoding('utf8');
  let rawData = '';
  res.on('data', (chunk) => { rawData += chunk; });
  res.on('end', () => {
    console.log(rawData)
  });
}).on('error', (e) => {
  // 请求返回的 http.ClientRequest 类,可以监听上面的一些方法
  console.error(`请求出现错误: ${e.message}`);
});
req.end() // 必须

# http.METHODS、http.STATUS_CODES

http 模块包含两个常量属性,分别表示支持 http 请求方法,http 响应状态码集合。

METHODS: [
  'ACL',         'BIND',       'CHECKOUT',
  'CONNECT',     'COPY',       'DELETE',
  'GET',         'HEAD',       'LINK',
  'LOCK',        'M-SEARCH',   'MERGE',
  'MKACTIVITY',  'MKCALENDAR', 'MKCOL',
  'MOVE',        'NOTIFY',     'OPTIONS',
  'PATCH',       'POST',       'PROPFIND',
  'PROPPATCH',   'PURGE',      'PUT',
  'REBIND',      'REPORT',     'SEARCH',
  'SOURCE',      'SUBSCRIBE',  'TRACE',
  'UNBIND',      'UNLINK',     'UNLOCK',
  'UNSUBSCRIBE'
],
STATUS_CODES: {
  '100': 'Continue',
  '101': 'Switching Protocols',
  '102': 'Processing',
  '103': 'Early Hints',
  '200': 'OK',
  '201': 'Created',
  '202': 'Accepted',
  '203': 'Non-Authoritative Information',
  '204': 'No Content',
  '205': 'Reset Content',
  '206': 'Partial Content',
  '207': 'Multi-Status',
  '208': 'Already Reported',
  '226': 'IM Used',
  '300': 'Multiple Choices',
  '301': 'Moved Permanently',
  '302': 'Found',
  '303': 'See Other',
  '304': 'Not Modified',
  '305': 'Use Proxy',
  '307': 'Temporary Redirect',
  '308': 'Permanent Redirect',
  '400': 'Bad Request',
  '401': 'Unauthorized',
  '402': 'Payment Required',
  '403': 'Forbidden',
  '404': 'Not Found',
  '405': 'Method Not Allowed',
  '406': 'Not Acceptable',
  '407': 'Proxy Authentication Required',
  '408': 'Request Timeout',
  '409': 'Conflict',
  '410': 'Gone',
  '411': 'Length Required',
  '412': 'Precondition Failed',
  '413': 'Payload Too Large',
  '414': 'URI Too Long',
  '415': 'Unsupported Media Type',
  '416': 'Range Not Satisfiable',
  '417': 'Expectation Failed',
  '418': "I'm a Teapot",
  '421': 'Misdirected Request',
  '422': 'Unprocessable Entity',
  '423': 'Locked',
  '424': 'Failed Dependency',
  '425': 'Unordered Collection',
  '426': 'Upgrade Required',
  '428': 'Precondition Required',
  '429': 'Too Many Requests',
  '431': 'Request Header Fields Too Large',
  '451': 'Unavailable For Legal Reasons',
  '500': 'Internal Server Error',
  '501': 'Not Implemented',
  '502': 'Bad Gateway',
  '503': 'Service Unavailable',
  '504': 'Gateway Timeout',
  '505': 'HTTP Version Not Supported',
  '506': 'Variant Also Negotiates',
  '507': 'Insufficient Storage',
  '508': 'Loop Detected',
  '509': 'Bandwidth Limit Exceeded',
  '510': 'Not Extended',
  '511': 'Network Authentication Required'
}

# https

http 模块不支持发送 https 请求,不支持监听 https 服务。这就需要使用 https 模块了。

发送 https 请求,和 http 模块基本一致,将 https 换成 http 即可,注意 options 里面的 port 默认为 443

在创建 https 服务时,需要增加 SSL 证书相关文件

const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

// 或者
// const options = {
//   pfx: fs.readFileSync('test/fixtures/test_cert.pfx'),
//   passphrase: '密码'
// };

https.createServer(options, (req, res) => {
  res.writeHead(200);
  res.end('你好,世界\n');
}).listen(8000);

# http2

由于 HTTP/2 相比 HTTP 1.x 增加了很多特殊处理,需要使用专门的 http2 模块来处理。HTTP/2 必须是 https,不支持 http

客户端发送 http 请求

const http2 = require('http2');
const fs = require('fs');
const client = http2.connect('https://localhost:8443', {
  ca: fs.readFileSync('证书.pem')
});
client.on('error', (err) => console.error(err));

const req = client.request({ ':path': '/' });

req.on('response', (headers, flags) => {
  for (const name in headers) {
    console.log(`${name}: ${headers[name]}`);
  }
});

req.setEncoding('utf8');
let data = '';
req.on('data', (chunk) => { data += chunk; });
req.on('end', () => {
  console.log(`\n${data}`);
  client.close();
});
req.end();

创建监听 http 服务

const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('密钥.pem'),
  cert: fs.readFileSync('证书.pem')
});
server.on('error', (err) => console.error(err));

server.on('stream', (stream, headers) => {
  // 流是一个双工流。
  stream.respond({
    'content-type': 'text/html; charset=utf-8',
    ':status': 200
  });
  stream.end('<h1>你好世界</h1>');
});

server.listen(8443);

# querystring

可以用于发送 Content-Typeapplication/x-www-form-urlencoded 时的数据处理

const querystring = require('querystring');
querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
// 返回 'foo=bar&baz=qux&baz=quux&corge='

querystring.stringify({ foo: 'bar', baz: 'qux' }, ';', ':');
// 返回 'foo:bar;baz:qux'

querystring.parse('foo=bar&abc=xyz&abc=123') 
// {
//   foo: 'bar',
//   abc: ['xyz', '123']
// }

# dns

# dns.lookup()

DNS 查询,根据 hostname(主机名) 获取 IP 地址以及对应的版本。内部使用 getaddrinfo 系统调用,会读取 /etc/hosts 的配置

// dns.js
const dns = require('dns');

// 注意不能使用 http 等协议
const arr = [
  'www.zuo11.com',
  'fe.zuo11.com'
]

arr.forEach(host => {
  // dns 查询,
  dns.lookup(host, (err, address, family) => {
    console.log('host: %j \naddress: %j family: IPv%s', host, address, family);
  });
})

// node dns.js
// host: "fe.zuo11.com" 
// address: "120.77.166.5" family: IPv4
// host: "zuo11.com" 
// address: "47.107.190.93" family: IPv4

# dns.resolveAny()

DNS 解析记录查询,比 dns.lookup() 查询的信息更详细,准确。忽略 /etc/hosts 的配置,始终通过网络执行 DNS 查询。可以根据 hostname 获取对应的解析类型、解析值。

const dns = require('dns');

// 注意不能使用 http 等协议
const arr = [
  'www.zuo11.com',
  'fe.zuo11.com'
]

arr.forEach(host => {
  dns.resolveAny(host, (err, ret) => {
    console.log(err, ret)
  })
})
// null [
//   { value: 'fe-zuo11-com.oss-cn-shenzhen.aliyuncs.com', type: 'CNAME' }
// ]
// null [ { address: '47.107.190.93', ttl: 600, type: 'A' } ]

除了 dns.resolveAny() 外,还有粒度更细的相关 API,参考 ns_dns_resolve_hostname (opens new window)

  • dns.resolveAny() Uses the DNS protocol to resolve all records (also known as ANY or * query).
  • dns.resolve4() Uses the DNS protocol to resolve a IPv4 addresses (A records) for the hostname.
  • dns.resolve6() Uses the DNS protocol to resolve a IPv6 addresses (AAAA records) for the hostname.
  • dns.resolveCname() Uses the DNS protocol to resolve CNAME records for the hostname.
  • dns.resolveNs() Uses the DNS protocol to resolve name server records (NS records) for the hostname.
  • ...

# dns.reverse()

反向 DNS 查询

const ipArr = [
  '47.107.190.93',
]

ipArr.forEach(ip => {
  // 使用 getHostByAddr 系统调用
  // 执行一个反向 DNS 查询,将 IPv4 或 IPv6 地址解析为主机名数组。
  dns.reverse(ip, (err, hostnames) => {
    console.log('dns.reverse', err, hostnames)
  })
})

一般服务供应商不允许反向 DNS 查询,会报错 Error: getHostByAddr ENOTFOUND 47.107.190.93,参考: Firebase reverse dns lookup ENOTFOUND error node.js dns (opens new window)

// dns.reverse Error: getHostByAddr ENOTFOUND 47.107.190.93
//     at QueryReqWrap.onresolve [as oncomplete] (dns.js:203:19) {
//   errno: 'ENOTFOUND',
//   code: 'ENOTFOUND',
//   syscall: 'getHostByAddr',
//   hostname: '47.107.190.93'
// } undefined

// don't allow reverse DNS lookups

参考: DNS | Node.js v14.15.4 Documentation (opens new window)

# dns.getServers()

返回 IP 地址字符串的数组,该字符串根据 RFC 5952 进行了格式化,作为当前 DNS 解析。如果使用自定义端口,则字符串将包含端口部分。

console.log(`dns.getServers()`, dns.getServers())
// dns.getServers() [ '192.168.31.1' ] 本地路由地址

# dns Promise 形式接口

使用 require('dns').promises 相当于之前的 dns,相关 API 都是 Promise 形式

const dnsPromises = require('dns').promises

dnsPromises.lookup('fe.zuo11.com').then((result) => {
  console.log('address: %j family: IPv%s', result.address, result.family);
  // address: "120.77.166.5" family: IPv4
});

dnsPromises.resolveAny('fe.zuo11.com').then((ret) => {
  console.log(ret)
});
// [
//   { value: 'fe-zuo11-com.oss-cn-shenzhen.aliyuncs.com', type: 'CNAME' }
// ]

dnsPromises.reverse('120.77.166.5').then(console.log).catch(err => {
  console.log(err) // 错误
})
// Error: getHostByAddr ENOTFOUND 120.77.166.5
上次更新: 2021/2/19 12:16:05