[node.js] [HTTP/S] 实现 requests 发起 HTTP/S/1.1/2.0 请求

node.js 使用 V8 引擎来编译运行 javascript 代码,与浏览器中的环境不同的是,node.js 不包含 DOM 和 BOM 模块。

本文使用 node.js 的官方库来实现一个简单的 requests() 函数,可以用来发送 HTTP/1.1 和 HTTP/2.0 的请求。有关 HTTP/1.1 和 HTTP/2.0 请参见往期的文章 HTTP 版本的演进 。

在 node.js http2 默认支持 keep-alive 连接,使用 http2 来发起 HTTP 请求需要我们自己来管理 client (TCP 连接)。

思路:创建一个 TCPConnection 类,用来保存 client 对象。创建一个 ConnectionPool 类,用来自动管理连接池,并定期清理长期没有请求的 client 对象。 requests() 函数可以控制 HTTP 的版本 HTTP/1.1 还是 HTTP/2.0,也可以选择 GET 或者 POST 方法,接收的数据经过解压缩后和响应头一起封装到对象中进行返回。

代码如下:(代码中加入了足够多的注释)

// requests.js
import http from 'node:http';
import https from 'node:https';
import http2 from 'node:http2';
import zlib from 'node:zlib';// http2.connect 创建的 client 实例维护者一个 TCP 连接
// 将 client 封装成类,实现 TCP 连接的复用
class TCPConnection {  constructor(client, origin) {  this.client = client;  this.origin = origin;  // 记录 client 的 origin: https://example.com:portthis.expires = Date.now() + 2 * 60 * 1000; // 设置过期时间为2分钟后  // 监听 error 事件,打印错误信息,关闭 client this.client.on('error', (err) => {  console.error(`Client error for origin ${this.origin}:`, err);  this.close(); // 在出现错误时关闭连接  });  // 监听 client 的 close() 事件,打印 originthis.client.on('close', () => {  console.log(`Connection closed for origin ${this.origin}`);  // 连接关闭后,不需要再次关闭,因为 this.client.close() 已经被调用或者即将被调用  // 但可以更新任何相关的状态或日志  });  }isExpired() {  return Date.now() > this.expires;     // 返回 client 是否过期}  close() {  this.client.close();     // 用来关闭过期的 client,过多的 client 会消耗系统的资源}
} // 维护一个连接池,保存所有带有 client 的 TCPConnection 类的实例
class ConnectionPool {  constructor() {  this.connections = new Map(); // 使用 Map 来存储连接,以便按 origin 查找  this.checkInterval = setInterval(() => this.checkExpiredConnections(), 3 * 60 * 1000); // 每3分钟检查一次  }  // 添加一个带有 origin 的连接  addConnection(origin) {  const client = http2.connect(origin);  const connection = new TCPConnection(client, origin);  this.connections.set(origin, connection); // 使用 origin 作为键  return connection;  }  // 根据 origin 获取连接  getConnection(origin) {  let connection = this.connections.get(origin);  if (connection && !connection.isExpired()) {  console.log(`使用现有的连接: ${connection.origin}`);      // 测试是否使用了现有的连接connection.expires = Date.now() + 2 * 60 * 1000;     // 连接被重新使用,重置 expires 过期时间return connection.client; // 返回现有的 client  } else {  // 如果连接不存在或已过期,则创建新连接  connection = this.addConnection(origin);  return connection.client;  }  }  checkExpiredConnections() {  for (const [origin, connection] of this.connections) {  if (connection.isExpired()) {  connection.close(); // 关闭过期的连接  this.connections.delete(origin); // 从 Map 中移除  }  }  }  closeAll() {     // 程序结束后调用,关闭所有的连接for (const connection of this.connections.values()) {  connection.close();  }this.connections.clear(); // 清空 Map  clearInterval(this.checkInterval); // 清除定期检查  }  
}const conPool = new ConnectionPool();/*** 解压缩数据* @param {Buffer} data - 要解压缩的数据* @param {string} encoding - 数据的编码方式* @returns {Promise<Buffer>} - 解压缩后的数据*/
async function decompressData(data, encoding) {return new Promise((resolve, reject) => {switch (encoding) {case 'gzip':zlib.gunzip(data, (err, decoded) => {if (err) reject(err);else resolve(decoded);});break;case 'deflate':zlib.inflate(data, (err, decoded) => {if (err) reject(err);else resolve(decoded);});break;case 'br':zlib.brotliDecompress(data, (err, decoded) => {if (err) reject(err);else resolve(decoded);});break;/*case 'zstd':break;     //以后实现*/default:resolve(data); // 如果内容未经任何编码或者压缩,亦或者是图片、视频,直接返回原始数据}});
}/*** 调用前确定好 http 的版本,向 http/1.1 的服务器发送 2.0 的请求会报 Protocol Error 的错误。* @param {string} url - 想要请求的 url* @param {string} method - 想要使用的方法 'GET' / 'POST' 等* @param {string} httpVersion -  控制 http 的版本: ['1.1' | '2.0']* @param {object} headers - request headers 请求头* @param {Buffer|string} [data] - 适用于 POST 方法(可选)* @returns {Promise<{data: Buffer, headers: object}>} - 返回响应数据和 headers 的 Promise*/
export default async function requests(url, method, httpVersion, headers, data) {return new Promise((resolve, reject) => {0try {const reqUrl = new URL(url);/*const myURL = new URL('https://example.com:8080/path?query=param#hash');// 访问各个部分console.log(myURL.origin);   // "https://example.com:8080"console.log(myURL.protocol); // "https:"console.log(myURL.hostname);  // "example.com"console.log(myURL.port);      // "8080"console.log(myURL.pathname);  // "/path"console.log(myURL.search);    // "?query=param"console.log(myURL.hash);      // "#hash"*///console.log(reqUrl);if ( httpVersion === '1.1' ) {     // 使用 http/1.1 发出请求,node:http、node:https 版本都是 1.1const options = {hostname: reqUrl.hostname,port: reqUrl.port || (reqUrl.protocol === 'http:' ? 80 : 443),method: method,path: `${reqUrl.pathname}${reqUrl.search}`,headers: headers};//console.log(options);const request = (reqUrl.protocol === 'http:' ? http : https).request(options, (response) => {var resData = Buffer.alloc(0);     //创建一个大小为 0 字节的 Buffer 实例response.on('data', (chunk) => {resData = Buffer.concat([resData, chunk]);   // 将resData和新的数据块chunk合并});response.on('end', () => {decompressData(resData, response.headers['content-encoding'])    // 检索响应头的 content-encoding 字段进行响应的解码操作.then((decodedData) => {resolve({ data: decodedData, headers: response.headers });// 如果数据经过了 gzip / deflate / br 压缩,就执行解压操作再返回// 数据 和 响应头 封装到对象中一起返回,返回响应头的必要性:方便后续的查看响应头,以进行一些操作}).catch((err) => {reject(`Decompression error: ${err.message}`);});});});request.on('error', (e) => {reject(`Problem with request: ${e.message}`);});if (method === 'POST' && data) {// Ensure the data is a Buffer or convert itrequest.write(Buffer.isBuffer(data) ? data : Buffer.from(data));}request.end();} else if ( httpVersion === '2.0' ) {const client = conPool.getConnection(reqUrl.origin);/*const client = http2.connect(reqUrl.origin);http2.connect 会为每个连接创建新的 TCP 连接。如果想要重用连接,需要使用相同的 client 实例。http2 不允许 connection: keep-alive 。 因为 http2 本身就设计为支持持久连接的。这意味着,HTTP/2 连接自动保持打开状态,以便在同一连接上处理多个请求和响应。*/client.on('error', (err)=> { reject(`Create client error: ${err}`);});const options = {':authority': reqUrl.host,':method': method,':path': `${reqUrl.pathname}${reqUrl.search}`,':scheme': reqUrl.protocol.slice(0, -1), // 去掉末尾的 ':'...headers   // headers 所有属性合并到 options 中};/*// 将 headers 中的属性合并到 options 中,除了 ...headers,也可以使用以下语句:Object.assign(options, headers);*///console.log(options);const request = client.request(options);var resHeaders;request.on('response', (headers) => {resHeaders = headers;});var resData = Buffer.alloc(0);request.on('data', (chunk) => {resData = Buffer.concat([resData, chunk]);});request.on('end', () => {decompressData(resData, resHeaders['content-encoding']).then((decodeData) => {resolve({ data: decodeData, headers: resHeaders});}).catch((err) => {reject(`Decompression error: ${err.message}`);});// client.close(); 使用 ConnectionPool 类来管理 client/*当所有请求完成后,调用 client.close() 以关闭连接。只有在不再需要连接时才应该关闭。client.close(); 会关闭 TCP 连接,新的 http 请求将无法重用 TCP 连接。为了有效地重用连接,应保持 client 对象的引用,以便后续请求可以使用同一连接。可以在一个更大的作用域中定义 client,并在多个请求中复用。*/});request.on('error', (e) => {reject(`Problem with request: ${e.message}`);// client.close();});if (method === 'POST' && data) {request.write(Buffer.isBuffer(data) ? data : Buffer.from(data));}request.end();} else {reject(`Unsupported http version ${httpVersion}`);}} catch (error) {reject(`Invalid URL: ${error.message}`);};});
}process.on('exit', () => {console.log("执行清理工作: 清理所有的 client 连接 ...");conPool.closeAll();
});

在 app.js 中用来发起请求测试,请求图片,和请求我之前发布一段命令行下旋转 cube 的视频:

import requests from './requests/requests.js';
//app.js
//测试
import fs from 'fs';//请求图片并写入文件
requests('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png', 'GET', "1.1", {}).then(resObj => {console.log(resObj.data);console.log(resObj.headers);fs.writeFile('baidu.png', resObj.data, {encoding: 'binary'}, error => {   //以二进制写入文件if (error) {console.error('写入文件时出错:', err);} else {console.log('文件已成功写入。');}});}).catch(error => {console.error("Error: ", error);});//请求一段视频
var videos = ['32b018315e66b2f02a2c08433b42fcc0_0.ts','32b018315e66b2f02a2c08433b42fcc0_1.ts','32b018315e66b2f02a2c08433b42fcc0_2.ts','32b018315e66b2f02a2c08433b42fcc0_3.ts','32b018315e66b2f02a2c08433b42fcc0_4.ts'
];
var baseUrl = "https://v-blog.csdnimg.cn/asset/999efa6d97215aa8905a1a05f7398e9f/play_video/";async function downloadAndWriteVideos() {for (let index = 0; index < videos.length; index++) {let url = baseUrl + videos[index];console.log(`index ${index}/${videos.length - 1} : requesting Url: ${url}.`);try {const resObj = await requests(url, 'GET', "2.0", {});//写入每个视频段到单独的文件await fs.promises.writeFile('cube_' + index + '.ts', resObj.data, { encoding: 'binary' });console.log('数据写入成功: cube_' + index + '.ts');// 追加到 cube.ts 文件await fs.promises.appendFile('cube.ts', resObj.data, { encoding: 'binary' });console.log('数据追加成功。');} catch (error) {console.error("Error: ", error);}}
}//调用函数
downloadAndWriteVideos();process.on('SIGINT', () => {console.log("接收到 SIGINT 信号,程序即将退出 ...");process.exit();
});process.on('SIGTERM', () => {console.log("接收到 SIGTERM 信号,程序即将退出 ...");process.exit();
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/61818.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

spring boot 微服务 redis集群配置

spring boot 微服务 redis集群配置 1.redis 有三种集群模式 主从模式 哨兵模式&#xff08;Sentinel&#xff09; Cluster模式引入redis依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis&…

【2024 re:Invent现场session参加报告】打造生成式AI驱动的车间智能助手

前言 这次参加了 re:Invent 2024 的 Builders Session 「Building a generative AI–powered shop floor assistant」&#xff0c;在这里和大家分享一下内容&#xff01; Session 概要 Learn how to build a generative AI assistant to analyze data from industrial IoT se…

Golang HTTP 标准库的使用实现原理

一.使用&#xff1a; 启动http服务&#xff1a; package mainimport "net/http"func main() {http.HandleFunc("/wecunge", func(w http.ResponseWriter, r *http.Request) {w.Write([]byte("wecunge"))})http.ListenAndServe(":8080&quo…

深入理解 GitHub 高级应用:从分支管理到自动化工作流

GitHub 是世界上最流行的代码托管平台&#xff0c;提供了丰富的功能来帮助开发者管理和协作项目。从基本的代码版本控制到复杂的协作工作流&#xff0c;GitHub 提供了大量高级功能来提升团队效率和代码质量。本文将介绍 GitHub 的一些高级应用和功能&#xff0c;包括分支管理、…

【C++】数组

1.概述 所谓数组&#xff0c;就是一个集合&#xff0c;该集合里面存放了相同类型的数据元素。 数组特点&#xff1a; &#xff08;1&#xff09;数组中的每个数据元素都是相同的数据类型。 &#xff08;2&#xff09;数组是有连续的内存空间组成的。 2、一维数组 2.1维数组定…

速盾:高防cdn的搜索引擎回源是什么?

高防CDN&#xff08;Content Delivery Network&#xff09;是一种用于加速网站访问速度和增加安全性的服务&#xff0c;它通过将静态和动态内容缓存在全球分布的服务器上&#xff0c;从而将用户请求的响应时间降至最低&#xff0c;并提供有效的防御攻击的能力。在实际使用过程中…

[VUE]框架网页开发02-如何打包Vue.js框架网页并在服务器中通过Tomcat启动

在现代Web开发中&#xff0c;Vue.js已经成为前端开发的热门选择之一。然而&#xff0c;将Vue.js项目打包并部署到生产环境可能会让一些开发者感到困惑。本文将详细介绍如何将Vue.js项目打包&#xff0c;并通过Tomcat服务器启动运行。 1. 准备工作 确保你的项目能够正常运行,项…

ESP32-S3模组上跑通ES8388(13)

接前一篇文章&#xff1a;ESP32-S3模组上跑通ES8388&#xff08;12&#xff09; 二、利用ESP-ADF操作ES8388 2. 详细解析 上一回解析了es8388_init函数中的第6段代码&#xff0c;本回继续往下解析。为了便于理解和回顾&#xff0c;再次贴出es8388_init函数源码&#xff0c;在…

openEuler 22.03 使用cephadm安装部署ceph集群

目录 目的步骤规格步骤ceph部署前准备工作安装部署ceph集群ceph集群添加node与osdceph集群一些操作组件服务操作集群进程操作 目的 使用ceph官网的cephadm无法正常安装&#xff0c;会报错ERROR: Distro openeuler version 22.03 not supported 在openEuler上实现以cephadm安装部…

DevOps工程技术价值流:GitLab源码管理与提交流水线实践

在当今快速迭代的软件开发环境中&#xff0c;DevOps&#xff08;开发运维一体化&#xff09;已经成为提升软件交付效率和质量的关键。而GitLab&#xff0c;作为一个全面的开源DevOps平台&#xff0c;不仅提供了强大的版本控制功能&#xff0c;还集成了持续集成/持续交付(CI/CD)…

C++ 游戏开发入门

一、为什么选择 C 进行游戏开发 C 在游戏开发领域具有独特的地位。它兼具高效性与对底层硬件的良好控制能力&#xff0c;这使得它非常适合开发对性能要求极高的游戏核心引擎部分。许多知名的大型游戏&#xff0c;如《使命召唤》系列、《虚幻竞技场》等&#xff0c;其底层架构都…

Spring 邮件发送

Spring 邮件发送 1. 主要内容&#xff08;了解&#xff09; 2. JavaMail 概述&#xff08;了解&#xff09; JavaMail&#xff0c;顾名思义&#xff0c;提供给开发者处理电⼦邮件相关的编程接⼝。JavaMail 是由 Sun 定义的⼀套收发电⼦邮件的 API&#xff0c;它可以⽅便地执⾏⼀…

VSCode如何关闭Vite项目本地自启动

某些情况下VSCode打开Vite项目不需要自动启动&#xff0c;那么如何关闭该功能 文件>首选项>设置 搜索vite 将Vite:Auto Start 勾选取消即可

算法训练营day27(回溯算法03:组合总和,组合总和2,分割回文串)

第七章 回溯算法part03● 39. 组合总和 ● 40.组合总和II ● 131.分割回文串详细布置 39. 组合总和 本题是 集合里元素可以用无数次&#xff0c;那么和组合问题的差别 其实仅在于 startIndex上的控制题目链接/文章讲解&#xff1a;https://programmercarl.com/0039.%E7%BB%84%E…

一种多功能调试工具设计方案开源

一种多功能调试工具设计方案开源 设计初衷设计方案具体实现HUB芯片采用沁恒微CH339W。TF卡功能网口功能SPI功能IIC功能JTAG功能下行USB接口 安路FPGA烧录器功能Xilinx FPGA烧录器功能Jlink OB功能串口功能RS232串口RS485和RS422串口自适应接口 CAN功能烧录器功能 目前进度后续计…

【Go底层】select原理

目录 1、背景2、go版本3、 selectgo函数解释【1】函数参数解释【2】函数具体解释第一步&#xff1a;遍历pollorder&#xff0c;选出准备好的case第二步&#xff1a;将当前goroutine放到所有case通道中对应的收发队列上第三步&#xff1a;唤醒groutine 4、总结 1、背景 select多…

Spark和MapReduce场景应用和区别

文章目录 Spark和MapReduce场景应用和区别一、引言二、MapReduce和Spark的应用场景1. MapReduce的应用场景2. Spark的应用场景 三、MapReduce和Spark的区别1. 内存使用和性能2. 编程模型和易用性3. 实时计算支持 四、使用示例1. MapReduce代码示例2. Spark代码示例 五、总结 Sp…

Python办公——openpyxl处理Excel每个sheet每行 修改为软雅黑9号剧中+边框线

目录 专栏导读背景1、库的介绍①&#xff1a;openpyxl 2、库的安装3、核心代码4、完整代码5、最快的方法(50万行44秒)——表头其余单元格都修改样式总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️‍…

【C#】书籍信息的添加、修改、查询、删除

文章目录 一、简介二、程序功能2.1 Book类属性&#xff1a;方法&#xff1a; 2.2 Program 类 三、方法&#xff1a;四、用户界面流程&#xff1a;五、程序代码六、运行效果 一、简介 简单的C#控制台应用程序&#xff0c;用于管理书籍信息。这个程序将允许用户添加、编辑、查看…

01-树莓派基本配置-基础配置配置

树莓派基本配置 文章目录 树莓派基本配置前言硬件准备树莓派刷机串口方式登录树莓派接入网络ssh方式登录树莓派更换国内源xrdp界面登录树莓派远程文件传输FileZilla 前言 树莓派是一款功能强大且价格实惠的小型计算机&#xff0c;非常适合作为学习编程、物联网项目、家庭自动化…