[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.rhkb.cn/news/483375.html

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

相关文章

文生视频、图生视频 AI 大模型开源项目介绍【持续更新】

Open-Sora 介绍&#xff1a;Open-Sora是一个由北京大学和兔展科研团队推出的开源项目&#xff0c;旨在推动视频生成技术的发展。Open-Sora致力于高效制作高质量视频&#xff0c;通过开源原则&#xff0c;使高级视频生成技术变得民主化&#xff0c;并提供一个简化且用户友好的平…

Burp Suite 实战指南:Proxy 捕获与修改流量、HTTP History 筛选与分析

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&a…

基于Vue实现的移动端手机商城项目 电商购物网站 成品源码

&#x1f4c2;文章目录 一、&#x1f4d4;网站题目 二、✍️网站描述 三、&#x1f4da;网站介绍 四、&#x1f310;网站演示 &#x1f4f8;部分截图 &#x1f3ac;视频演示 五、⚙️网站代码 &#x1f9f1;项目结构 &#x1f492;vue代码预览 六、&#x1f527;完整…

.NET 9 中 LINQ 新增功能实现过程

本文介绍了.NET 9中LINQ新增功能&#xff0c;包括CountBy、AggregateBy和Index方法,并提供了相关代码示例和输出结果&#xff0c;感兴趣的朋友跟随我一起看看吧 LINQ 介绍 语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。 数据查询历来都表示为简单的…

yarn install遇到问题处理

1、Yarn在尝试安装一个依赖项时遇到了问题。具体来说&#xff0c;这个错误指出期望提升&#xff08;hoist&#xff09;的包的manifest文件丢失了&#xff0c;这通常是因为缓存中的数据损坏或不一致所致。 解决方法&#xff1a;有以下两种 1、清除Yarn缓存&#xff1a;运行 yarn…

遇到问题:hive中的数据库和sparksql 操作的数据库不是同一个。

遇到的问题&#xff1a; 1、hive中的数据库和sparksql 操作的数据库不同步。 观察上面的数据库看是否同步 &#xff01;&#xff01;&#xff01; 2、查询服务器中MySQL中hive的数据库&#xff0c;发现创建的位置没有在hdfs上&#xff0c;而是在本地。 这个错误产生的原因是&…

大数据-240 离线数仓 - 广告业务 测试 ADS层数据加载 DataX数据导出到 MySQL

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇开始了&#xff01; 目前开始更新 MyBatis&#xff0c;一起深入浅出&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff0…

计算机网络-网络安全

网络安全介绍 端口扫描 安全包括那些方面&#xff1a; 数据存储安全、应用程序安全、操作系统安全、网络安全、物理安全、用户安全教育 一、网络安全问题概述 1. 计算机网络面临的安全性威胁 计算机网络上的通信面临以下的四种威胁&#xff1a; 截获——从网络上窃听他人…

linux 获取公网流量 tcpdump + python + C++

前言 需求为&#xff0c;统计linux上得上下行公网流量&#xff0c;常规得命令如iftop 、sar、ifstat、nload等只能获取流量得大小&#xff0c;不能区分公私网&#xff0c;所以需要通过抓取网络包并排除私网段才能拿到公网流量。下面提供了一些有效得解决思路&#xff0c;提供了…

【CSS in Depth 2 精译_066】11.2 颜色的定义(上)

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第四部分 视觉增强技术 ✔️【第 11 章 颜色与对比】 ✔️ 11.1 通过对比进行交流 11.1.1 模式的建立11.1.2 还原设计稿 11.2 颜色的定义 ✔️ 11.2.1 色域与色彩空间11.2.2 深入理解颜色表示法 文…

论文导读 I RAFT:使语言模型适应特定领域的RAG

摘要 随着大语言模型&#xff08;LLMs&#xff09;的发展&#xff0c;这些模型在广泛的任务中展现出了卓越的性能。然而&#xff0c;当这些模型应用于特定领域时&#xff0c;如何有效融入新信息仍然是一个未解决的问题。本文提出了检索增强微调&#xff08;RAFT&#xff09;&a…

华为HarmonyOS 让应用快速拥有账号能力 -- 2 获取用户头像昵称

场景介绍 如应用需要完善用户头像昵称信息&#xff0c;可使用Account Kit提供的头像昵称授权能力&#xff0c;用户允许应用获取头像昵称后&#xff0c;可快速完成个人信息填写。以下只针对Account kit提供的头像昵称授权能力进行介绍&#xff0c;若要获取头像还可通过场景化控…

高校数字化运营平台解决方案:构建统一的服务大厅、业务平台、办公平台,助力打造智慧校园

教育数字化是建设教育强国的重要基础&#xff0c;利用技术和数据助推高校管理转型&#xff0c;从而更好地支撑教学业务开展。 近年来&#xff0c;国家多次发布政策&#xff0c;驱动教育行业的数字化转型。《“十四五”国家信息化规划》&#xff0c;推进信息技术、智能技术与教育…

华为HarmonyOS 让应用快速拥有账号能力 -- 1 华为账号一键登录

概述 华为账号一键登录是基于OAuth 2.0协议标准和OpenID Connect协议标准构建的OAuth2.0 授权登录系统&#xff0c;应用可以通过华为账号一键登录能力方便地获取华为账号用户的身份标识和手机号&#xff0c;快速建立应用内的用户体系。 优势&#xff1a; 利用系统账号的安全…

C语言:指针与数组

一、. 数组名的理解 int arr[5] { 0,1,2,3,4 }; int* p &arr[0]; 在之前我们知道要取一个数组的首元素地址就可以使用&arr[0]&#xff0c;但其实数组名本身就是地址&#xff0c;而且是数组首元素的地址。在下图中我们就通过测试看出&#xff0c;结果确实如此。 可是…

是什么阻断了kafka与zk的链接?

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 问题描述&#xff1a; 前几天部署一套环境&#xff0c;先把zk集群起来了&#xff0c;之后第二天在启动kafka的时候&#xff0c;…

MAUI APP开发蓝牙协议的经验分享:与跳绳设备对接

在开发MAUI应用程序时&#xff0c;蓝牙协议的应用是一个重要的环节&#xff0c;尤其是在需要与外部设备如智能跳绳进行数据交换的场景中。以下是我在开发过程中的一些经验和心得&#xff0c;希望能为你的项目提供帮助。 1. 蓝牙协议基础 蓝牙协议是无线通信的一种标准&#x…

算法日记 40 day 单调栈

最后两题了&#xff0c;直接上题目。 题目&#xff1a;接雨水 42. 接雨水 - 力扣&#xff08;LeetCode&#xff09; 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1…

浏览器渲染原理

渲染原理 第一步解析Html第二步样式计算第三步布局第四步分层第五步绘制第六步分块第七步光栅化第八步画常见面试题什么是回流reflow&#xff1f;什么是重绘repaint&#xff1f; 当浏览器的网络线程收到HTML文档之后&#xff0c;会产生一个渲染任务并且会将其传递给渲染主线程的…

嵌入式系统应用-LVGL的应用-平衡球游戏 part2

平衡球游戏 part2 4 mpu60504.1 mpu6050 介绍4.2 电路图4.3 驱动代码编写 5 游戏界面移植5.1 移植源文件5.2 添加头文件 6 参数移植6.1 4 mpu6050 4.1 mpu6050 介绍 MPU6050是一款由InvenSense公司生产的加速度计和陀螺仪传感器&#xff0c;广泛应用于消费电子、机器人等领域…