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

news2024/12/27 4:44:05

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:port
        this.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() 事件,打印 origin
        this.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) => {0
        try {
            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.1
                const 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 it
                    request.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.coloradmin.cn/o/2252903.html

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

相关文章

pytest(二)excel数据驱动

一、excel数据驱动 excel文件内容 excel数据驱动使用方法 import openpyxl import pytestdef get_excel():excel_obj openpyxl.load_workbook("../pytest结合数据驱动-excel/data.xlsx")sheet_obj excel_obj["Sheet1"]values sheet_obj.valuescase_li…

类和对象(中)(类的默认成员函数)+日期类实现

1.类的默认成员函数 默认成员函数就是用户没有显示实现&#xff0c;编译器会自动生成的成语函数称为默认成员函数。一个类&#xff0c;我们不写的情况下编译器会默认生成6个默认成员函数。C11后还增加了两个默认成员函数&#xff0c;移动构造和移动赋值&#xff08;这一节暂时…

详解版本控制工作原理及优势,常见的版本控制系统对比(HelixCore、Git、SVN等)

什么是版本控制软件&#xff1f;从基础层面来说&#xff0c;版本控制&#xff08;也可称版本管理&#xff09;就是随时间跟踪和管理文件变更的过程&#xff0c;而版本控制软件有助于实现这一过程的自动化。但这仅仅是其功能及其重要性的开端。 什么是版本控制&#xff1f; 版本…

记录一次网关异常

记一次网关异常 网关时不时就会出现下面的异常。关键是不知道什么时候就会报错&#xff0c;并且有时候就算什么都不操作&#xff0c;也会导致这个异常。 ERROR org.springframework.scheduling.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in schedul…

SpringMVC跨域问题解决方案

当Web应用程序尝试从一个源&#xff08;例如 http://localhost:9090&#xff09;向另一个不同的源&#xff08;例如 http://localhost:8080&#xff09;发起请求时&#xff0c;发现报错&#xff1a; 报错原因&#xff1a;请求被CORS策略拦截了 跨域问题概述 当Web应用程序尝试…

现在的电商风口已经很明显了

随着电商行业的不断发展&#xff0c;直播带货的热潮似乎正逐渐降温&#xff0c;而货架电商正成为新的焦点。抖音等平台越来越重视货架电商&#xff0c;强调搜索功能的重要性&#xff0c;预示着未来的电商中心将转向货架和搜索。 在这一转型期&#xff0c;AI技术与电商的结合为…

芯驰X9SP与汽车麦克风-打造无缝驾驶体验

当今汽车技术的进步不仅提升了驾驶体验&#xff0c;还改变了我们与车辆互动的方式。汽车麦克风作为车内语音控制系统的重要组成部分&#xff0c;正逐渐成为现代汽车的标配。 技术原理 汽车麦克风主要依赖于声音传感技术&#xff0c;通常包括电容式麦克风和动圈式麦克风。这些…

vue3项目搭建-6-axios 基础配置

axios 基础配置 安装 axios npm install axios 创建 axios 实例&#xff0c;配置基地址&#xff0c;配置拦截器,目录&#xff1a;utils/http.js 基地址&#xff1a;在每次访问时&#xff0c;自动作为相对路径的根 // axios 基础封装 import axios from "axios";…

【北京迅为】iTOP-4412全能版使用手册-第三十二章 网络通信-TCP套字节

iTOP-4412全能版采用四核Cortex-A9&#xff0c;主频为1.4GHz-1.6GHz&#xff0c;配备S5M8767 电源管理&#xff0c;集成USB HUB,选用高品质板对板连接器稳定可靠&#xff0c;大厂生产&#xff0c;做工精良。接口一应俱全&#xff0c;开发更简单,搭载全网通4G、支持WIFI、蓝牙、…

量子人工智能产业发展现状及趋势(上)

文章目录 前言一、量子人工智能产业发展现状1.产业链上游&#xff1a;涵盖基础硬件与量子计算整机开发&#xff0c;参与厂商众多&#xff0c;发展相对成熟2.产业链中游&#xff1a;涉及人工智能算法与应用开发&#xff0c;参与企业均在积极探索以赢得市场先机3.产业链下游&…

企业如何构建自己的 AI 编码能力

文章摘要 在数字化转型的浪潮中&#xff0c;企业对于提升开发效率和代码质量的需求日益迫切。AI 编码能力作为一种新兴的技术力量&#xff0c;正逐渐成为企业技术竞争力的关键。本文将探讨企业如何结合代码大模型和私域数据&#xff0c;构建属于自己的 AI 编码能力。 全文阅读…

算法日记 40 day 单调栈

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

SpringBoot集成Kafka和avro和Schema注册表

Schema注册表 为了提升kafka的性能&#xff0c;减少网络传输和存储的数据大小&#xff0c;可以把数据的schema部分单独存储到外部的schema注册表中&#xff0c;整体架构如下图所示&#xff1a; 1&#xff09;把所有数据需要用到的 schema 保存在注册表里&#xff0c;然后在记…

c++领域展开第一幕——入门基础(命名空间、iostream、缺省参数、函数重载、nullptr、inline(内联函数))超详细!!!!

文章目录 前言一、c的第一个程序二、命名空间2.1 namespace 的价值2.2 namespace 的定义2.3 命名空间的使用 三、c的输入和输出四、缺省参数五、函数重载六、nullptr七、inline总结 前言 今天小编带着大家进入c的大门&#xff0c;虽然c难&#xff0c;但好事多磨&#xff0c;一起…

Java Web 1HTML快速入门

目录 一、Web开发介绍 1.什么是Web&#xff1f; 2.初识Web前端 二、HTML快速入门 1.什么是HTML、CSS&#xff1f; 2、案例练习 3.小结 三、VS Code开发工具 四、基础标签&样式&#xff08;HTML&#xff09; 2、实现标题--样式1&#xff08;新闻标题的颜色&#xff0…

【Python网络爬虫笔记】7-网络爬虫的搜索工具re模块

目录 一、网络爬虫中的正则表达式和re模块&#xff08;一&#xff09;数据提取的精确性&#xff08;二&#xff09;处理复杂的文本结构&#xff08;三&#xff09;提高数据处理效率 二、正则表达式的内涵&#xff08;一&#xff09;、常用元字符&#xff08;二&#xff09;、量…

42_GAN网络详解(2)---常见的GAN

DCGAN CGAN 条件生成对抗网络&#xff08;Conditional Generative Adversarial Networks, CGAN&#xff09;是生成对抗网络&#xff08;Generative Adversarial Networks, GAN&#xff09;的一种变体&#xff0c;由Mehdi Mirza和Simon Osindero在2014年提出。CGAN的主要改进在…

PC端阅读器--koodo reader

官网&#xff1a;请在必应搜索引擎上输入 koodo reader GitHub&#xff1a;GitHub - koodo-reader/koodo-reader: Windows, macOS, Linux and Web 123云windows版&#xff1a;Koodo-Reader-1.5.1.exe下载 提取码&#xff1a;4455 优&#xff1a; 1.开源&#xff0c;懂&#x…

PyQt设计界面优化 #qss #ui设计 #QMainWindow

思维导图 通过qss实现ui界面设计优化 Qss是Qt程序界面中用来设置控件的背景图片、大小、字体颜色、字体类型、按钮状态变化等属性&#xff0c;它是用来美化UI界面。实现界面和程序的分离&#xff0c;快速切换界面。 首先我们在Pytchram创建一个新目录 然后将我们所需要的图片打…

多维数组及其应用————13

1. 二维数组 如果我们把 ⼀维数组做为数组的元 素&#xff0c;这时候就是⼆维数组&#xff0c; ⼆维数组作为数组元素的数组被为三维数组&#xff0c;⼆维数组以上的数组统称 为多维数组。 1.1 二维数组的创建 先行后列 其实也可以这样理解&#xff1a;把二维数组当成特殊的一维…