不会的要多查多问,不然不会的永远不会,哪怕你离会就差了那么一点点
第一页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]
C#代码
  1. 地址栏输入:chrome://flags/,回车。搜索找到Block insecure private network requests,设置禁用(Disabled),然后重启浏览器即可  

fetch获取wasm模块实例

[不指定 2024/11/14 11:18 | by 刘新修 ]

fetch获取wasm模块实例

JavaScript代码
  1. /*** 完整的实例 ***/  
  2. fetch("/pkg/wasm-lib/wasm_lib_bg.wasm")  
  3.     .then((response) => response.arrayBuffer())  
  4.     .then(bytes => {  
  5.         var module = new WebAssembly.Module(bytes);  
  6.         var imports = {  
  7.             "__wbindgen_init_externref_table": ()=> {}  
  8.         };  
  9.         var instance = new WebAssembly  
  10.             .Instance(module, { wbg: imports});  
  11.         console.log(instance.exports.add(1,2));  
  12.     })  

axios获取wasm模块实例

JavaScript代码
  1. /*** 请求.wasm文件流 ***/  
  2. this.$store.dispatch("test/getWasmStream",{}).then(async (res) => {  
  3.     WebAssembly.compile(res.data).then(module => {  
  4.         // 使用编译好的模块  
  5.   
  6.         // 打印静态属性  
  7.         var imports = WebAssembly.Module.imports(module);  
  8.         var exports = WebAssembly.Module.exports(module);  
  9.         var strJSON = JSON.stringify({"imports":imports, "exports":exports}, null, 2)  
  10.         console.log('strJSON', strJSON)  
  11.   
  12.         var imports = {  
  13.             "__wbindgen_init_externref_table": ()=> {}  
  14.         };  
  15.   
  16.         /*** 使用WASM模块(运行实例) ***/  
  17.         var instance = new WebAssembly.Instance(module, {wbg: imports})  
  18.         //console.log(instance.exports.add(3,6))  
  19.   
  20.         this.exports = instance.exports;  
  21.   
  22.     }).catch(error => {  
  23.         console.error(error);  
  24.     });  
  25. }).catch((error)=> {  
  26.     console.log('Action failed', error);  
  27. });  

loadWebAssemblyModule.js 封装

JavaScript代码
  1. /* 
  2.  * @Description: loadWebAssemblyModule.js 
  3.  * @Version: 1.0 
  4.  * @Author: Jesse Liu 
  5.  * @Date: 2022-11-14 10:32:06 
  6.  * @LastEditors: Jesse Liu 
  7.  * @LastEditTime: 2024-11-14 10:55:25 
  8.  */  
  9.   
  10. /*** AJAX加载WASM模块的函数 ***/  
  11. const getWebAssemblyModule = (url) =>  {  
  12.     /*** 返回一个Promise ***/  
  13.     return new Promise(function(resolve, reject) {  
  14.         var xhr = new XMLHttpRequest();  
  15.         xhr.open('GET', url, true);  
  16.         xhr.responseType = 'arraybuffer';  
  17.         xhr.onload = function() {  
  18.             if (xhr.status === 200) {  
  19.                 // 加载成功,编译WASM模块  
  20.                 var arrayBuffer = xhr.response;  
  21.                 WebAssembly.compile(arrayBuffer).then(function(module) {  
  22.                     resolve(module);  
  23.                 }).catch(function(error) {  
  24.                     reject(error);  
  25.                 });  
  26.             } else {  
  27.                 // 出错了  
  28.                 reject(new Error('Error loading WASM module: ' + xhr.statusText));  
  29.             }  
  30.         };  
  31.         xhr.send();  
  32.     });  
  33. }  
  34.   
  35. /*** fetch加载WASM模块的函数 ***/  
  36. const fetchWebAssemblyModule = (url) =>  {  
  37.     /*** 返回一个Promise ***/  
  38.     return new Promise(function(resolve, reject) {  
  39.         fetch(url)  
  40.         .then((response) => response.arrayBuffer())  
  41.         .then(bytes => {  
  42.             var module = new WebAssembly.Module(bytes);  
  43.             resolve(module);  
  44.         })  
  45.         .catch(error => {  
  46.             console.error('Fetch 错误:', error)  
  47.             reject(error);  
  48.         });  
  49.     });  
  50. }  
  51.   
  52. export { fetchWebAssemblyModule, getWebAssemblyModule };  

 

 

centos 安装 Emscripten

[不指定 2024/11/08 15:53 | by 刘新修 ]

centos 安装 Emscripten

在CentOS上安装Emscripten需要几个步骤。以下是基本的安装指南:
 
更新系统包:
 
sudo yum update
安装Emscripten需要的依赖项:
 
sudo yum install git clang make python nodejs
获取Emscripten源代码:
 
git clone https://github.com/emscripten-core/emsdk.git
进入emsdk目录并安装最新的Emscripten SDK:
 
cd emsdk
./emsdk install latest
激活安装的SDK:
 
./emsdk activate latest
加载Emscripten环境变量,可以将以下命令加入到你的.bashrc或.bash_profile中,以便在每个新的终端会话中自动设置环境变量:
 
source ./emsdk_env.sh
完成以上步骤后,Emscripten应该就安装并配置好了。
 
可以通过运行emcc --version来检查是否安装成功。
 
[root@localhost emsdk]# emcc --version
shared:INFO: (Emscripten: Running sanity checks)
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.71 (4171ae200b77a6c266b0e1ebb507d61d1ade3501)
Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

安装Rust

[不指定 2024/11/08 15:50 | by 刘新修 ]

Rust 是一种系统级编程语言,旨在提供高性能和内存安全,同时避免常见的编程错误。
由 Mozilla Research 推出,Rust 自推出以来因其独特的设计理念和强大的功能而在开发者社区中迅速获得了广泛的关注和采用。

安装命令:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

分解说明:

curl:这是一个用于在命令行下传输数据的工具,支持多种协议(如 HTTP、HTTPS、FTP 等)。

--proto '=https':指定只允许使用 HTTPS 协议进行传输,确保数据传输的安全性。

--tlsv1.2:强制 curl 使用 TLS 1.2 协议,这是一种安全的传输层协议。

-s:静默模式(silent),在执行过程中不会显示进度条或错误信息。

-Sf:

-S:当使用 -s(静默模式)时,-S 可以让 curl 在发生错误时仍然显示错误信息。

-f:如果服务器返回一个错误状态码(如 404),curl 会失败并返回一个错误,而不是输出错误页面的内容。

https://sh.rustup.rs:这是 Rust 官方提供的安装脚本的 URL。

| sh:管道符号(|)将前一个命令(curl)的输出传递给后一个命令(sh)。也就是说,下载的安装脚本将直接由 sh(shell)执行。

整体作用:

这个命令通过安全的 HTTPS 连接下载 Rust 的安装脚本,并立即在您的终端中执行该脚本,以便安装 Rust 编程语言及其工具链。

 

输出解释

C#代码
  1. info: downloading installer  
  2.   
  3. Welcome to Rust!  
  4.   
  5. This will download and install the official compiler for the Rust  
  6. programming language, and its package manager, Cargo.  
  7.   
  8. Rustup metadata and toolchains will be installed into the Rustup  
  9. home directory, located at:  
  10.   
  11.   /home/jjmczd/.rustup  
  12.   
  13. This can be modified with the RUSTUP_HOME environment variable.  
  14.   
  15. The Cargo home directory is located at:  
  16.   
  17.   /home/jjmczd/.cargo  
  18.   
  19. This can be modified with the CARGO_HOME environment variable.  
  20.   
  21. The cargo, rustc, rustup and other commands will be added to  
  22. Cargo's bin directory, located at:  
  23.   
  24.   /home/jjmczd/.cargo/bin  
  25.   
  26. This path will then be added to your PATH environment variable by  
  27. modifying the profile files located at:  
  28.   
  29.   /home/jjmczd/.profile  
  30.   /home/jjmczd/.bashrc  
  31.   
  32. You can uninstall at any time with rustup self uninstall and  
  33. these changes will be reverted.  
  34.   
  35. Current installation options:  
  36.   
  37.    default host triple: x86_64-unknown-linux-gnu  
  38.      default toolchain: stable (default)  
  39.                profile: default  
  40.   modify PATH variable: yes  
  41.   
  42. 1) Proceed with standard installation (default - just press enter)  
  43. 2) Customize installation  
  44. 3) Cancel installation  

逐行解释:

info: downloading installer

解释:安装程序正在下载过程中。

Welcome to Rust!

解释:欢迎使用 Rust!

接下来的几行

解释:这些行说明了安装过程将会下载和安装 Rust 官方编译器(rustc)以及其包管理器(Cargo)。

Rustup metadata and toolchains will be installed into the Rustup home directory, located at:

解释:Rustup 的元数据和工具链将被安装到指定的 Rustup 主目录中,默认路径为 /home/jjmczd/.rustup。您可以通过设置 RUSTUP_HOME 环境变量来修改此路径。

The Cargo home directory is located at:

解释:Cargo 的主目录位于 /home/jjmczd/.cargo。同样,您可以通过设置 CARGO_HOME 环境变量来修改此路径。

The cargo, rustc, rustup and other commands will be added to Cargo's bin directory, located at:

解释:cargo、rustc、rustup 以及其他相关命令将被添加到 Cargo 的 bin 目录中,即 /home/jjmczd/.cargo/bin。

This path will then be added to your PATH environment variable by modifying the profile files located at:

解释:安装程序会将上述 bin 目录路径添加到您的 PATH 环境变量中,这通过修改您的 shell 配置文件(如 /home/jjmczd/.profile 和 /home/jjmczd/.bashrc)来实现。这样,您可以在任何终端会话中直接运行 Rust 的命令。

You can uninstall at any time with rustup self uninstall and these changes will be reverted.

解释:如果您在任何时候想要卸载 Rust,可以运行 rustup self uninstall 命令,这将撤销所有安装的更改。

Current installation options:

解释:当前的安装选项如下:

default host triple: x86_64-unknown-linux-gnu

解释:默认的主机三元组(host triple)是 x86_64-unknown-linux-gnu,表示安装的是适用于 64 位 Linux 系统的 Rust 工具链。

default toolchain: stable (default)

解释:默认的工具链是 stable 版本,这是 Rust 的稳定版本,适合大多数用户和生产环境使用。

profile: default

解释:使用的是默认的安装配置文件,包含基本的组件和设置。

modify PATH variable: yes

解释:安装程序将修改您的 PATH 环境变量,以便您可以在终端中直接使用 Rust 的命令。

安装选项菜单:

C#代码
  1. 1) Proceed with standard installation (default - just press enter)  
  2. 2) Customize installation  
  3. 3) Cancel installation  
1) Proceed with standard installation (default - just press enter)
 
解释:继续标准安装(默认选项)。如果您按回车键,将使用上述默认设置进行安装。
2) Customize installation
 
解释:自定义安装。选择此选项可以让您自定义安装路径、选择不同的工具链版本或调整其他安装选项。
3) Cancel installation
 
解释:取消安装。选择此选项将终止 Rust 的安装过程。
接下来的步骤
选择安装选项:
 
标准安装:如果您不需要自定义安装,直接按回车键继续。这将使用默认设置进行安装。
自定义安装:如果您需要更改安装路径或选择特定的工具链版本,可以输入 2 并按照提示进行操作。
取消安装:如果您暂时不想安装 Rust,可以输入 3 取消。
完成安装:
 
安装完成后,确保重新启动终端或重新加载 shell 配置文件,以便新的 PATH 设置生效。
 
您可以通过运行以下命令来验证 Rust 是否安装成功:
 
C#代码
  1. rustc --version  
  2. cargo --version  
 
这两个命令应分别返回 Rust 编译器和 Cargo 的版本信息。
更新 Rust(可选):
如果您已经安装过 Rust,可以通过以下命令更新到最新版本:
 
C#代码
  1. rustup update  
PATH 环境变量未更新:
如果安装后运行 rustc --version 提示找不到命令,可能是因为 PATH 环境变量未正确更新。您可以手动添加 Cargo 的 bin 目录到 PATH 中,例如:
export PATH="$HOME/.cargo/bin:$PATH"
 
将上述行添加到您的 ~/.bashrc 或 ~/.profile 文件中,然后重新加载配置:
source ~/.bashrc
 
 
wasm-pack编译命令:
 
C#代码
  1. [root@localhost brotli-compress-vue-component]# wasm-pack build --target web --out-name compress --out-dir pkg  
 
 

 开发本地项目时,访问接口如果遇到类似net::ERR_FAILED、Error: Network Error、Preflight等情形,可排查下是否因浏览器自身所致。

用新版chrome(版本92+)跨域请求本地接口,比如http://192.168.xxx.xxx/token。

点击右上角红色警告按钮,提示如下:

大概意思:

一个站点从一个网络中请求一个资源,由于其用户的特权网络位置,它只能访问这个资源。这些请求将设备和服务器暴露给互联网,增加了跨站点请求伪造(CSRF)攻击和/或信息泄漏的风险。
为了降低这些风险,Chrome弃用从非安全上下文发起的对非公共子资源的请求,并将在Chrome 92中开始屏蔽它们(2021年7月)。
若要解决此问题,请将需要访问本地资源的网站迁移到HTTPS。如果目标资源不在本地主机上提供服务,那么它也必须通过HTTPS提供服务,以避免混合内容问题。
管理员可以使用企业策略的insecureprivatenetworkrequestallowed和InsecurePrivateNetworkRequestsAllowedForUrls在所有或某些网站上暂时禁用此限制。

其实还是跨域方面的问题,按照要求设置即可。最简单的方法是暂时禁用此功能,毕竟本地测试的项目,一般安全性都不需要太高。

地址栏输入:chrome://flags/,回车。搜索找到Block insecure private network requests,设置禁用(Disabled),然后重启浏览器即可

 我们在下载大文件时,通常会使用多线程下载的方式来加快下载速度。例如常用的多线程下载工具(Gopeed、Aria2、XDM等等),都是通过多线程下载技术充分利用了网络带宽,以提高下载速度。

那么多线程下载是怎么实现的呢?多个线程发送网络请求,是怎么做到同时下载一个文件呢?事实上,借助HTTP协议中的一些机制就可以实现了!

今天我们就通过使用Go语言为例,从了解HTTP请求相关的一些机制开始,实现一个多线程下载的示例。

1,多线程下载原理

事实上,多线程下载的原理很简单,主要的步骤如下:

  • 获取待下载文件大小
  • 每个线程下载文件的一部分
  • 全部下载完成后,拼接为完整文件

实现这些步骤,就涉及到HTTP协议的下列相关机制。

(1) HEAD请求 - 只获取请求头

我们通常发送HTTP请求大多数是GET或者POST类型,发送请求后我们会立即获取响应体,浏览器则会根据响应体的类型来处理内容,例如返回的是text/html就会作为网页显示,返回image/png就会解码为图片等等,响应体的类型由响应头Content-Type标识。当我们下载文件时,事实上也是发送HTTP请求,只不过服务器返回的响应体就是文件本身了!其类型则是application/octet-stream,浏览器也知道这是个文件需要下载。

当然,文件作为响应体通常比起网页、图片要大得多,在多线程下载时,我们就要先获取文件的大小,而不是立即获取文件本身,这时我们就可以向服务器发起HEAD请求而不是GET请求。

服务器收到HEAD请求后,就只会返回对应的响应头,而不会返回响应体,这样我们就可以在下载文件之前,读取响应头中的Content-Length来先获取待下载文件大小。

(2) Range请求头 - 只获取部分响应体

知道了文件大小,我们就需要让每个线程只下载一部分文件,借助HTTP的Range请求头,就可以实现只让服务端返回响应体内容的一部分,而不是返回完整的响应体。

这里我们先来借助书籍《图解HTTP》中对Range请求头的讲解,来学习一下:

XML/HTML代码
  1. Range: bytes=5001-10000  

那么服务端就只会返回响应体的第5001到第10000字节的内容部分,包含第5001和第10000字节,0表示响应体的第一个字节。

这样,在多个线程同时下载文件时,我们在每个线程的请求中使用Range请求头,就可以实现一个线程只下载文件的一部分了!

(3) 为什么多线程下载可以提升速度?

事实上,在我们客户端(下载文件的)和服务端双向网络通信情况都很好的情况下,使用单线程和多线程下载的速度是几乎没有差异的,也就是说能够跑满我们客户端的全部带宽,那么这种情况下我们使用单线程下载反而更能够节省硬件和网络资源。

但是在我们客户端和服务端之间网络波动较大的情况下,例如我们国内从Github下载文件的时候,就会发现多线程下载速度比单线程快得多,反之使用单线程完全无法充分利用我们的网络带宽。

这种现象事实上是因为TCP连接的慢启动机制导致的,众所周知HTTP是基于TCP的协议,每次我们建立HTTP连接时,包括下载文件,都是在传输层基于TCP协议进行传输。TCP慢启动机制是TCP 协议中一种拥塞控制的机制,目的是在开始数据传输时逐步探测网络的容量,避免瞬间发送大量数据而导致网络拥塞。慢启动不是字面意义上的“慢”,而是相对于立即使用最大带宽而言,它会逐渐增加传输速率。

慢启动机制的过程简要概括如下:

  • 一开始建立连接:当一个新的TCP连接建立后,发送方并不知道当前网络的拥塞情况。因此,发送方不会马上发送大量数据,而是会使用慢启动机制来逐步增加数据传输的速率,在TCP中使用阻塞窗口cwnd来限制发送的数据量,也就是说一开始cwnd是非常小的
  • 拥塞窗口增长:在建立连接后,每当接收到一个确认ACK包时,cwnd指数级增长,直到达到网络的带宽限制或者某个拥塞控制的阈值(称为慢启动阈值ssthresh),这个过程会一直持续,直到发送方探测到网络出现了拥塞(比如丢包或者确认延迟变长),或者cwnd达到了某个预定义的慢启动阈值ssthresh
  • 慢启动的终止:慢启动机制会在以下情况终止:
    • 达到慢启动阈值ssthresh:当拥塞窗口cwnd增长到慢启动阈值ssthresh时,慢启动机制停止,此时TCP会进入另一种拥塞控制机制,称为拥塞避免,这时cwnd增长变为线性而非指数级
    • 发生拥塞(如丢包或超时):如果发送方检测到数据包丢失(例如没有收到确认),它会认为网络已经出现拥塞,此时ssthresh会被调整为当前cwnd的一半,然后cwnd会重置为1 MSS,重新进入慢启动阶段

可见TCP连接使用cwnd限制两者发送的数据量的大小,并逐步“试探”两者传输数据速率的上限并增加传输的数据量。

在我们下载文件时,事实上是服务端在向我们发送文件,如果网络波动较大、不稳定,TCP连接机会一直将cwnd限制在一个较小的值,在单位时间内,服务端也无法向我们发送更大的数据量。

此时,如果我们使用多线程下载,和服务端建立多个TCP连接,这样即使每个TCP连接的cwnd较小,所有TCP连接加起来传输的数据量仍然可以占满我们的带宽。

2,Go代码实现

知道了HTTP的上述几个机制,相信大家就知道如何实现一个简单的多线程下载了!我们可以总结主要步骤如下:

  • 发送HEAD类型请求,通过Content-Length请求头获取待下载文件大小
  • 根据给定的线程数量,结合待下载文件大小,确定每个线程下载的范围部分,也就是每个线程的Range请求头字节范围
  • 启动所有线程,使得每个线程下载它们对应的部分文件,并等待全部线程下载完成
  • 合并每个线程下载的部分为最终文件
  • 清理每个线程下载的文件部分

这里分别设计下列类(结构体),用于存放多线程下载时的传入参数和状态量:

 

上述ShardTask类表示一个线程的下载任务,其中会完成一个分片(文件的一部分)的下载请求操作,它有如下作为参数的属性:

  • Url 下载的文件地址
  • Order 分片序号
  • ShardFilePath 这个分片文件的保存路径
  • RangeStartRangeEnd 下载的文件起始范围和结束范围,用于设定Range请求头

此外,还有作为下载状态的属性:

  • DownloadSize 下载任务进行时,这个线程已下载的文件部分大小
  • TaskDone 这个线程的下载任务是否完成

该类的成员方法如下:

  • DoShardGet 执行分片下载任务,在其中会根据RangeStartRangeEnd设定对应的HTTP请求头,发送请求并下载对应的文件部分

然后就是ParallelGetTask类,表示一整个多线程下载任务,其中包含了一个多线程下载任务的参数和状态量,并且实现了多线程下载的每个步骤,它有如下作为参数的属性:

  • Url 文件的下载链接
  • FilePath 文件下载完成后的保存位置
  • Concurrent 下载并发数,即同时下载的分片数量
  • TempFolder 临时分片文件的保存文件夹

此外还有作为状态的属性:

  • TotalSize 待下载文件的总大小
  • ShardTaskList 存储所有分片任务对象指针的列表

该类中的方法主要是分片下载的一些步骤如下:

  • getLength 发送HEAD请求获取Content-Length以获取文件大小,获取后将其设定到TotalSize属性
  • allocateTask 根据给定的线程数和获取到的文件大小,计算每个线程下载的文件内容范围,并创建对应的ShardTask结构体放入ShardTaskList
  • downloadShard 为每一个ShardTask对象创建一个线程(Goroutine)并在新的线程中调用ShardTask对象的下载分片方法,以启动所有线程的下载任务,并通过sync.WaitGroup来等待全部线程完成
  • mergeFile 下载完成后,合并每个分片为最终文件
  • cleanShard 合并完成后,清理下载的每个分片文件
  • printTotalProcess 这是一个附加的辅助方法,用于实时输出下载进度
  • Run 启动整个多线程下载任务,该函数是暴露的公开函数,其中对上述每个步骤函数进行了组织,按顺序调用执行

下面,我们来看一下它们的代码实现。

(1) ShardTask - 一个线程的下载任务

C#代码
  1. package model  
  2.   
  3. import (  
  4.     "bufio"  
  5.     "fmt"  
  6.     "github.com/fatih/color"  
  7.     "io"  
  8.     "net/http"  
  9.     "os"  
  10.     "sync"  
  11. )  
  12.   
  13. // 全局HTTP客户端  
  14. var httpClient = http.Client{  
  15.     Transport: &http.Transport{  
  16.         // 关闭keep-alive确保一个线程就使用一个TCP连接  
  17.         DisableKeepAlives: true,  
  18.     },  
  19. }  
  20.   
  21. // ShardTask 单个分片下载任务的任务参数和状态量  
  22. type ShardTask struct {  
  23.     // 下载链接  
  24.     Url string  
  25.     // 分片序号,从1开始  
  26.     Order int  
  27.     // 这个分片文件的路径  
  28.     ShardFilePath string  
  29.     // 分片的起始范围(字节,包含)  
  30.     RangeStart int64  
  31.     // 分片的结束范围(字节,包含)  
  32.     RangeEnd int64  
  33.     // 已下载的部分(字节)  
  34.     DownloadSize int64  
  35.     // 该任务是否完成  
  36.     TaskDone bool  
  37. }  
  38.   
  39. // NewShardTask 构造函数  
  40. func NewShardTask(url string, order int, shardFilePath string, rangeStart int64, rangeEnd int64) *ShardTask {  
  41.     return &ShardTask{  
  42.         // 设定任务参数  
  43.         Url:           url,  
  44.         Order:         order,  
  45.         ShardFilePath: shardFilePath,  
  46.         RangeStart:    rangeStart,  
  47.         RangeEnd:      rangeEnd,  
  48.         // 初始化状态量  
  49.         DownloadSize: 0,  
  50.         TaskDone:     false,  
  51.     }  
  52. }  
  53.   
  54. // DoShardGet 开始下载这个分片(该方法在goroutine中执行)  
  55. func (task *ShardTask) DoShardGet(waitGroup *sync.WaitGroup) {  
  56.     // 创建文件  
  57.     file, e := os.OpenFile(task.ShardFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)  
  58.     if e != nil {  
  59.         color.Red("任务%d创建文件失败!", task.Order)  
  60.         color.HiRed("%s", e)  
  61.         return  
  62.     }  
  63.     // 准备请求  
  64.     request, e := http.NewRequest("GET", task.Url, nil)  
  65.     if e != nil {  
  66.         color.Red("任务%d创建请求出错!", task.Order)  
  67.         color.HiRed("%s", e)  
  68.         return  
  69.     }  
  70.     // 设定请求头  
  71.     request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.RangeStart, task.RangeEnd))  
  72.     // 发送请求  
  73.     response, e := httpClient.Do(request)  
  74.     if e != nil {  
  75.         color.Red("任务%d发送下载请求出错!", task.Order)  
  76.         color.HiRed("%s", e)  
  77.         return  
  78.     }  
  79.     // 读取请求体  
  80.     body := response.Body  
  81.     // 读取缓冲区  
  82.     buffer := make([]byte, 8092)  
  83.     // 准备写入文件  
  84.     writer := bufio.NewWriter(file)  
  85.     for {  
  86.         // 读取一次内容至缓冲区  
  87.         readSize, readError := body.Read(buffer)  
  88.         if readError != nil {  
  89.             // 如果读取完毕则退出循环  
  90.             if readError == io.EOF {  
  91.                 break  
  92.             } else {  
  93.                 color.Red("任务%d读取响应错误!", task.Order)  
  94.                 color.HiRed("%s", readError)  
  95.                 return  
  96.             }  
  97.         }  
  98.         // 把缓冲区内容追加至文件  
  99.         _, writeError := writer.Write(buffer[0:readSize])  
  100.         if writeError != nil {  
  101.             color.Red("任务%d写入文件时出现错误!", task.Order)  
  102.             color.HiRed("%s", writeError)  
  103.             return  
  104.         }  
  105.         _ = writer.Flush()  
  106.         // 记录下载进度  
  107.         task.DownloadSize += int64(readSize)  
  108.     }  
  109.     // 关闭全部资源  
  110.     _ = body.Close()  
  111.     _ = file.Close()  
  112.     // 标记任务完成  
  113.     task.TaskDone = true  
  114.     // 使线程组中计数器-1  
  115.     waitGroup.Done()  
  116. }  

构造函数NewShardTask负责完成ShardTask的参数传入和状态量初始化,而DoShardGet方法实现了下载一个文件分片的完整步骤,从创建文件准备写入,到设定请求头,发出请求,最后读取响应体保存到文件。

此外,可见这里的http.Client对象中,我们将其DisableKeepAlives设为了true即关闭keep-alive,这是因为默认情况下Go语言的HTTP客户端会复用TCP连接,即使你多个线程发起请求,也会使用一个TCP连接进行

而多线程下载需要每个线程持有一个单独的TCP连接来达到突破cwnd的限制,因此这里关闭keep-alive实现每个线程发起请求时,使用单独的TCP连接。

(2) ParallelGetTask - 一整个多线程下载任务

C#代码
  1. package model  
  2.   
  3. import (  
  4.     "bufio"  
  5.     "fmt"  
  6.     "gitee.com/swsk33/shard-download-demo/util"  
  7.     "github.com/fatih/color"  
  8.     "io"  
  9.     "net/http"  
  10.     "os"  
  11.     "path/filepath"  
  12.     "strconv"  
  13.     "sync"  
  14.     "time"  
  15. )  
  16.   
  17. // ParallelGetTask 多线程下载任务类,存放一个多线程下载任务的参数和状态量  
  18. type ParallelGetTask struct {  
  19.     // 文件的下载链接  
  20.     Url string  
  21.     // 文件的最终保存位置  
  22.     FilePath string  
  23.     // 下载并发数  
  24.     Concurrent int  
  25.     // 下载的分片临时文件保存文件夹  
  26.     TempFolder string  
  27.     // 下载文件的总大小  
  28.     TotalSize int64  
  29.     // 全部的下载分片任务参数列表  
  30.     ShardTaskList []*ShardTask  
  31. }  
  32.   
  33. // NewParallelGetTask 构造函数  
  34. func NewParallelGetTask(url string, filePath string, concurrent int, tempFolder string) *ParallelGetTask {  
  35.     return &ParallelGetTask{  
  36.         // 参数赋值  
  37.         Url:        url,  
  38.         FilePath:   filePath,  
  39.         Concurrent: concurrent,  
  40.         TempFolder: tempFolder,  
  41.         // 初始化状态量  
  42.         TotalSize:     0,  
  43.         ShardTaskList: make([]*ShardTask, 0),  
  44.     }  
  45. }  
  46.   
  47. // 发送HEAD请求获取待下载文件的大小  
  48. func (task *ParallelGetTask) getLength() error {  
  49.     // 发送请求  
  50.     response, e := http.Head(task.Url)  
  51.     if e != nil {  
  52.         color.Red("发送HEAD请求出错!")  
  53.         return e  
  54.     }  
  55.     // 读取并设定长度  
  56.     task.TotalSize = response.ContentLength  
  57.     return nil  
  58. }  
  59.   
  60. // 根据待下载文件的大小和设定的并发数,创建每个分片任务对象  
  61. func (task *ParallelGetTask) allocateTask() {  
  62.     // 如果并发数大于总大小,则进行调整  
  63.     if int64(task.Concurrent) > task.TotalSize {  
  64.         task.Concurrent = int(task.TotalSize)  
  65.     }  
  66.     // 开始计算每个分片的下载范围  
  67.     eachSize := task.TotalSize / int64(task.Concurrent)  
  68.     // 创建任务对象  
  69.     for i := 0; i < task.Concurrent; i++ {  
  70.         task.ShardTaskList = append(task.ShardTaskList, NewShardTask(task.Url, i+1, filepath.Join(task.TempFolder, strconv.Itoa(i+1)), int64(i)*eachSize, int64(i+1)*eachSize-1))  
  71.     }  
  72.     // 处理末尾部分  
  73.     if task.TotalSize%int64(task.Concurrent) != 0 {  
  74.         task.ShardTaskList[task.Concurrent-1].RangeEnd = task.TotalSize - 1  
  75.     }  
  76. }  
  77.   
  78. // 根据任务列表进行多线程分片下载操作  
  79. func (task *ParallelGetTask) downloadShard() {  
  80.     // 创建线程组  
  81.     waitGroup := &sync.WaitGroup{}  
  82.     // 开始执行全部分片下载线程  
  83.     for _, task := range task.ShardTaskList {  
  84.         go task.DoShardGet(waitGroup)  
  85.         waitGroup.Add(1)  
  86.     }  
  87.     // 等待全部下载完成  
  88.     waitGroup.Wait()  
  89. }  
  90.   
  91. // 下载完成后,合并分片文件  
  92. func (task *ParallelGetTask) mergeFile() error {  
  93.     // 创建目的文件  
  94.     targetFile, e := os.OpenFile(task.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755)  
  95.     if e != nil {  
  96.         color.Red("创建目标文件出错!")  
  97.         return e  
  98.     }  
  99.     // 创建写入器  
  100.     writer := bufio.NewWriter(targetFile)  
  101.     // 准备读取每个分片文件  
  102.     for _, shard := range task.ShardTaskList {  
  103.         shardFile, e := os.OpenFile(shard.ShardFilePath, os.O_RDONLY, 0755)  
  104.         if e != nil {  
  105.             color.Red("读取分片文件出错!")  
  106.             return e  
  107.         }  
  108.         reader := bufio.NewReader(shardFile)  
  109.         readBuffer := make([]byte, 1024*1024)  
  110.         for {  
  111.             // 读取每个分片文件,一次读取1KB  
  112.             readSize, readError := reader.Read(readBuffer)  
  113.             // 处理结束或错误  
  114.             if readError != nil {  
  115.                 if readError == io.EOF {  
  116.                     break  
  117.                 } else {  
  118.                     color.Red("读取分片文件出错!")  
  119.                     return readError  
  120.                 }  
  121.             }  
  122.             // 写入到最终合并的文件  
  123.             _, writeError := writer.Write(readBuffer[0:readSize])  
  124.             if writeError != nil {  
  125.                 color.Red("写入合并文件出错!")  
  126.                 return writeError  
  127.             }  
  128.             _ = writer.Flush()  
  129.         }  
  130.         // 关闭分片文件资源  
  131.         _ = shardFile.Close()  
  132.     }  
  133.     // 关闭目的文件资源  
  134.     _ = targetFile.Close()  
  135.     return nil  
  136. }  
  137.   
  138. // 删除分片临时文件  
  139. func (task *ParallelGetTask) cleanShard() error {  
  140.     for _, shard := range task.ShardTaskList {  
  141.         e := os.Remove(shard.ShardFilePath)  
  142.         if e != nil {  
  143.             color.Red("删除分片临时文件%s出错!", shard.ShardFilePath)  
  144.             return e  
  145.         }  
  146.     }  
  147.     return nil  
  148. }  
  149.   
  150. // 在一个新线程中,实时输出每个分片的下载进度和总进度  
  151. func (task *ParallelGetTask) printTotalProcess() {  
  152.     go func() {  
  153.         // 上一次统计时的已下载大小,用于计算速度  
  154.         var lastDownloadSize int64 = 0  
  155.         for {  
  156.             // 如果全部任务完成则结束输出,并统计并发数  
  157.             allDone := true  
  158.             // 当前并发数  
  159.             currentTaskCount := 0  
  160.             for _, shardTask := range task.ShardTaskList {  
  161.                 if !shardTask.TaskDone {  
  162.                     allDone = false  
  163.                     currentTaskCount += 1  
  164.                 }  
  165.             }  
  166.             if allDone {  
  167.                 break  
  168.             }  
  169.             // 统计所有分片已下载大小之和  
  170.             var totalDownloadSize int64 = 0  
  171.             for _, shardTask := range task.ShardTaskList {  
  172.                 totalDownloadSize += shardTask.DownloadSize  
  173.             }  
  174.             // 计算速度  
  175.             currentDownload := totalDownloadSize - lastDownloadSize  
  176.             lastDownloadSize = totalDownloadSize  
  177.             speedString := util.ComputeSpeed(currentDownload, 300)  
  178.             // 输出到控制台  
  179.             fmt.Printf("\r当前并发数:%3d 速度:%s 总进度:%3.2f%%", currentTaskCount, speedString, float32(totalDownloadSize)/float32(task.TotalSize)*100)  
  180.             // 等待300ms  
  181.             time.Sleep(300 * time.Millisecond)  
  182.         }  
  183.     }()  
  184. }  
  185.   
  186. // Run 开始执行整个分片多线程下载任务  
  187. func (task *ParallelGetTask) Run() error {  
  188.     // 获取文件大小  
  189.     e := task.getLength()  
  190.     if e != nil {  
  191.         color.Red("%s", e)  
  192.         return e  
  193.     }  
  194.     color.HiYellow("已获取到下载文件大小:%d字节", task.TotalSize)  
  195.     // 分配任务  
  196.     task.allocateTask()  
  197.     color.HiYellow("已完成分片任务分配,共计%d个任务", len(task.ShardTaskList))  
  198.     // 开启进度输出  
  199.     task.printTotalProcess()  
  200.     // 开始下载分片  
  201.     task.downloadShard()  
  202.     color.HiYellow("\n所有分片已下载完成!")  
  203.     // 开始合并文件  
  204.     e = task.mergeFile()  
  205.     if e != nil {  
  206.         color.Red("%s", e)  
  207.         return e  
  208.     }  
  209.     color.HiYellow("合并分片完成!")  
  210.     // 清理临时分片文件  
  211.     e = task.cleanShard()  
  212.     if e != nil {  
  213.         color.Red("%s", e)  
  214.         return e  
  215.     }  
  216.     color.HiYellow("清理分片临时文件完成!")  
  217.     color.Green("分片下载任务完成!")  
  218.     return nil  
  219. }  

上述printTotalProcess函数中,util.ComputeSpeed函数用于计算下载速度并自动转换为可读单位,代码如下:

C#代码
  1. package util  
  2.   
  3. import (  
  4.     "fmt"  
  5.     "math"  
  6. )  
  7.   
  8. // 关于单位的实用工具函数  
  9.   
  10. // ComputeSpeed 计算网络速度  
  11. // size 一段时间内下载的数据大小,单位字节  
  12. // timeElapsed 经过的时间长度,单位毫秒  
  13. // 返回计算得到的网速,会自动换算单位  
  14. func ComputeSpeed(size int64, timeElapsed intstring {  
  15.     bytePerSecond := size / int64(timeElapsed) * 1000  
  16.     if 0 <= bytePerSecond && bytePerSecond <= 1024 {  
  17.         return fmt.Sprintf("%4d Byte/s", bytePerSecond)  
  18.     }  
  19.     if bytePerSecond > 1024 && bytePerSecond <= int64(math.Pow(1024, 2)) {  
  20.         return fmt.Sprintf("%6.2f KB/s", float64(bytePerSecond)/1024)  
  21.     }  
  22.     if bytePerSecond > 1024*1024 && bytePerSecond <= int64(math.Pow(1024, 3)) {  
  23.         return fmt.Sprintf("%6.2f MB/s", float64(bytePerSecond)/math.Pow(1024, 2))  
  24.     }  
  25.     return fmt.Sprintf("%6.2f GB/s", float64(bytePerSecond)/math.Pow(1024, 3))  
  26. }  

 

可见通过构造函数NewParallelGetTask完成参数传递和状态量设定后,其它每个私有函数都对应我们多线程下载中的一个步骤,最后由公开函数Run统筹组织起所有的步骤,完成整个多线程下载任务。

3,实现效果

现在我们在main函数中创建一个ParallelGetTask对象,设定好参数后调用其Run方法即可开始多线程下载文件的任务:

C#代码
  1. package main  
  2.   
  3. import (  
  4.     "gitee.com/swsk33/shard-download-demo/model"  
  5. )  
  6.   
  7. func main() {  
  8.     // 创建任务  
  9.     task := model.NewParallelGetTask(  
  10.         "https://github.com/jgraph/drawio-desktop/releases/download/v24.7.17/draw.io-24.7.17-windows-installer.exe",  
  11.         "C:\\Users\\swsk33\\Downloads\\draw.io.exe",  
  12.         64,  
  13.         "C:\\Users\\swsk33\\Downloads\\temp",  
  14.     )  
  15.     // 执行任务  
  16.     _ = task.Run()  
  17. }  
 

原生JS获取公网IP地址

[不指定 2024/10/11 16:39 | by 刘新修 ]
JavaScript代码
  1. function getUserIP(callback) {  
  2.     var ip_dups = {};  
  3.     var RTCPeerConnection = window.RTCPeerConnection  
  4.         || window.mozRTCPeerConnection  
  5.         || window.webkitRTCPeerConnection;  
  6.     var useWebKit = !!window.webkitRTCPeerConnection;  
  7.     var mediaConstraints = {  
  8.         optional: [{RtpDataChannels: true}]  
  9.     };  
  10.     var servers = {  
  11.         iceServers: [  
  12.             {urls: "stun:stun.services.mozilla.com"},  
  13.             {urls: "stun:stun.l.google.com:19302"},  
  14.         ]  
  15.     };  
  16.     var pc = new RTCPeerConnection(servers, mediaConstraints);  
  17.     function handleCandidate(candidate){  
  18.         var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/  
  19.         var hasIp = ip_regex.exec(candidate)  
  20.         if (hasIp) {  
  21.             var ip_addr = ip_regex.exec(candidate)[1];  
  22.             if(ip_dups[ip_addr] === undefined)  
  23.                 callback(ip_addr);  
  24.             ip_dups[ip_addr] = true;  
  25.         }  
  26.     }  
  27.     pc.onicecandidate = function(ice){  
  28.         if(ice.candidate) {  
  29.             handleCandidate(ice.candidate.candidate);  
  30.         }    
  31.     };  
  32.     pc.createDataChannel("");  
  33.     pc.createOffer(function(result){  
  34.       pc.setLocalDescription(result, function(){}, function(){});  
  35.     }, function(){});  
  36.     setTimeout(function(){  
  37.         var lines = pc.localDescription.sdp.split('\n');  
  38.         lines.forEach(function(line){  
  39.             if(line.indexOf('a=candidate:') === 0)  
  40.                 handleCandidate(line);  
  41.         });  
  42.     }, 1000);  
  43. }  
  44.   
  45.   
  46. getUserIP((ip) => {  
  47.   console.log("ipppp === ",ip)  
  48.   //ipppp ===  121.90.11.160  
  49. })  

SHELL 脚本删除文件或目录

[不指定 2024/09/27 17:28 | by 刘新修 ]

使用SHELL脚本,删除指定文件或目录,

使用方法:bash /sh/delfiles.sh 11.txt 22.txt

C#代码
  1. #!/bin/bash  
  2. # 检查是否有参数传入  
  3. if [ "$#" -eq 0 ]; then  
  4.     echo "[ERROR] No files to delete! At least one parameter is required! "  
  5.     exit 1  
  6. fi  
  7. # 打印脚本名称  
  8. echo "Script Name: $0"  
  9. # 打印脚本所在的目录  
  10. echo "Script Directory: $(dirname $0)"  
  11. # 打印当前工作目录  
  12. echo "Working Directory: $PWD"  
  13. # 进入当前工作目录  
  14. cd $PWD  
  15. echo "=================================================="  
  16. # 循环遍历所有参数并删除文件  
  17. for file in "$@"; do  
  18.     if [ -f "$file" ]; then  
  19.         rm "$file"  
  20.         echo "Deleted file ==> ./$file"  
  21.     elif [ -d "$file" ]; then  
  22.         rm -rf "$file"  
  23.         echo "Deleted directory ==> ./$file"  
  24.     else  
  25.         echo "File or directory does not exist: $file"  
  26.     fi  
  27. done  
  28.   
  29. echo "=================================================="  

 

JavaScript代码
  1. proxy: {  
  2.     '/dev-api': {  
  3.         target: 'http://1x.xx.x.xxx:xxxx', //  
  4.         changeOrigin: true,  
  5.         pathRewrite: {  
  6.           '^/dev-api': ''  
  7.         },  
  8.         //第一种方法  
  9.         onProxyReq: function(proxyReq, req, res, options) {  
  10.           if (req.body) {  
  11.             const bodyData = JSON.stringify(req.body)  
  12.             // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json  
  13.             proxyReq.setHeader('Content-Type', 'application/json')  
  14.             proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData))  
  15.             // stream the content  
  16.             proxyReq.write(bodyData)  
  17.           }  
  18.        }  
  19.    }  
  20. },  
  21. //第二种方法,注释这行  
  22. before: require('./mock/mock-server.js')  

Blob是什么

在JavaScript中,Blob(二进制大对象)对象是一种用于表示原始数据的类文件对象,它存储了任意类型的数据,通常是二进制数据,但也可以包含文本数据或其他格式的数据。Blob对象通常用于在客户端浏览器中处理二进制数据,比如文件上传、数据传输等场景。(Blob对象可以将任意类型的数据转换为二进制形式进行存储和处理。这使得Blob对象成为处理各种数据类型的有效工具,包括文本、图像、音频、视频等。在JavaScript中,Blob对象提供了一种统一的接口来处理这些数据,无论是从用户上传的文件中获取数据,还是从其他来源获取数据,都可以使用Blob对象来进行二进制处理。)

Blob的用途

Blob对象常见的使用情况包括:

  • 文件上传:当用户通过网页上传文件时,浏览器会创建一个Blob对象来表示上传的文件数据,然后可以将该Blob对象发送到服务器端进行处理。

  • 数据传输:在客户端与服务器端进行数据交互时,可以使用Blob对象来存储和传输二进制数据,例如通过XMLHttpRequest或Fetch API发送Blob对象。

  • 图像处理:在客户端对图像进行处理时,可以将图像数据存储在Blob对象中,并通过Canvas API等技术进行操作和显示。

  • 媒体处理:在处理音频或视频等媒体数据时,可以使用Blob对象来存储和处理媒体数据。

和file文件流的关系

以下是Blob对象和File对象之间的联系和区别:

  • Blob对象:
    Blob对象是二进制数据的容器,可以包含任意类型的数据,如文本、图像、音频或视频等。
    可以通过构造函数 new Blob(data, options) 创建 Blob 对象,其中 data 是包含数据的数组或数组缓冲区,options 是一个可选对象,用于指定 Blob 的 MIME 类型和结束符等信息。

  • File对象:
    File对象是Blob对象的一种扩展,通常用于表示用户系统中的文件,具有文件名和最后修改日期等属性。
    可以通过 new File(data, name, options) 构造函数创建 File 对象,其中 data 是包含数据的数组或数组缓冲区,name 是文件名,options 是一个可选对象,用于指定文件的 MIME 类型和最后修改日期等信息。

联系与区别:
File对象是Blob对象的子类,因此所有可以对Blob对象执行的操作也同样适用于File对象。
File对象在表示文件时具有额外的元数据,如文件名和最后修改日期等。
在实际使用中,Blob对象通常用于处理二进制数据,而File对象则用于处理用户系统中的文件,如文件上传和操作。

因此,可以说File对象是Blob对象的一种特殊情况,用于在JavaScript中表示用户系统中的文件,并提供了额外的文件相关的属性。

Blob和File文件流,base64的转化

Blob对象转文件: 可以使用File构造函数将Blob对象转换为文件对象。File构造函数接受一个Blob对象和一个文件名作为参数,然后返回一个文件对象。

JavaScript代码
  1. var blob = new Blob(["blob data"], { type: "text/plain" });  
  2. var file = new File([blob], "filename.txt");  

Blob对象转Base64: 可以使用FileReader对象将Blob对象转换为Base64编码的字符串。FileReader对象可以读取Blob对象中的数据,并以指定的编码格式输出。

JavaScript代码
  1. var blob = new Blob(["blob data"], { type: "text/plain" });  
  2. var reader = new FileReader();  
  3. reader.readAsDataURL(blob);  
  4. reader.onload = function () {  
  5.   var base64String = reader.result;  
  6. };  

Base64转Blob对象: 可以将Base64编码的字符串转换回Blob对象。这可以通过创建一个新的Blob对象并指定Base64字符串的数据类型完成。

JavaScript代码
  1. var base64String = "base64 data";  
  2. var byteCharacters = atob(base64String);  
  3. var byteNumbers = new Array(byteCharacters.length);  
  4. for (var i = 0; i < byteCharacters.length; i++) {  
  5.   byteNumbers[i] = byteCharacters.charCodeAt(i);  
  6. }  
  7. var byteArray = new Uint8Array(byteNumbers);  
  8. var blob = new Blob([byteArray], { type: "image/jpeg" });  
第一页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]