Dioxus由DioxusLabs开发的全栈应用框架 旨在简化跨平台应用的开发
Dioxus是一个现代的、轻量级的、用于构建跨平台UI的库,灵感来源于React。它以其高性能、简洁的API和丰富的生态系统,为开发者提供了一种高效开发原生应用的新方式。
chrome 公网地址页面,无法访问内网地址图片及接口解决方法
- 地址栏输入:chrome://flags/,回车。搜索找到Block insecure private network requests,设置禁用(Disabled),然后重启浏览器即可
fetch获取wasm模块实例
fetch获取wasm模块实例
- /*** 完整的实例 ***/
- fetch("/pkg/wasm-lib/wasm_lib_bg.wasm")
- .then((response) => response.arrayBuffer())
- .then(bytes => {
- var module = new WebAssembly.Module(bytes);
- var imports = {
- "__wbindgen_init_externref_table": ()=> {}
- };
- var instance = new WebAssembly
- .Instance(module, { wbg: imports});
- console.log(instance.exports.add(1,2));
- })
axios获取wasm模块实例
- /*** 请求.wasm文件流 ***/
- this.$store.dispatch("test/getWasmStream",{}).then(async (res) => {
- WebAssembly.compile(res.data).then(module => {
- // 使用编译好的模块
- // 打印静态属性
- var imports = WebAssembly.Module.imports(module);
- var exports = WebAssembly.Module.exports(module);
- var strJSON = JSON.stringify({"imports":imports, "exports":exports}, null, 2)
- console.log('strJSON', strJSON)
- var imports = {
- "__wbindgen_init_externref_table": ()=> {}
- };
- /*** 使用WASM模块(运行实例) ***/
- var instance = new WebAssembly.Instance(module, {wbg: imports})
- //console.log(instance.exports.add(3,6))
- this.exports = instance.exports;
- }).catch(error => {
- console.error(error);
- });
- }).catch((error)=> {
- console.log('Action failed', error);
- });
loadWebAssemblyModule.js 封装
- /*
- * @Description: loadWebAssemblyModule.js
- * @Version: 1.0
- * @Author: Jesse Liu
- * @Date: 2022-11-14 10:32:06
- * @LastEditors: Jesse Liu
- * @LastEditTime: 2024-11-14 10:55:25
- */
- /*** AJAX加载WASM模块的函数 ***/
- const getWebAssemblyModule = (url) => {
- /*** 返回一个Promise ***/
- return new Promise(function(resolve, reject) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'arraybuffer';
- xhr.onload = function() {
- if (xhr.status === 200) {
- // 加载成功,编译WASM模块
- var arrayBuffer = xhr.response;
- WebAssembly.compile(arrayBuffer).then(function(module) {
- resolve(module);
- }).catch(function(error) {
- reject(error);
- });
- } else {
- // 出错了
- reject(new Error('Error loading WASM module: ' + xhr.statusText));
- }
- };
- xhr.send();
- });
- }
- /*** fetch加载WASM模块的函数 ***/
- const fetchWebAssemblyModule = (url) => {
- /*** 返回一个Promise ***/
- return new Promise(function(resolve, reject) {
- fetch(url)
- .then((response) => response.arrayBuffer())
- .then(bytes => {
- var module = new WebAssembly.Module(bytes);
- resolve(module);
- })
- .catch(error => {
- console.error('Fetch 错误:', error)
- reject(error);
- });
- });
- }
- export { fetchWebAssemblyModule, getWebAssemblyModule };
centos 安装 Emscripten
centos 安装 Emscripten
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 编程语言及其工具链。
输出解释
- info: downloading installer
- Welcome to Rust!
- This will download and install the official compiler for the Rust
- programming language, and its package manager, Cargo.
- Rustup metadata and toolchains will be installed into the Rustup
- home directory, located at:
- /home/jjmczd/.rustup
- This can be modified with the RUSTUP_HOME environment variable.
- The Cargo home directory is located at:
- /home/jjmczd/.cargo
- This can be modified with the CARGO_HOME environment variable.
- The cargo, rustc, rustup and other commands will be added to
- Cargo's bin directory, located at:
- /home/jjmczd/.cargo/bin
- This path will then be added to your PATH environment variable by
- modifying the profile files located at:
- /home/jjmczd/.profile
- /home/jjmczd/.bashrc
- You can uninstall at any time with rustup self uninstall and
- these changes will be reverted.
- Current installation options:
- default host triple: x86_64-unknown-linux-gnu
- default toolchain: stable (default)
- profile: default
- modify PATH variable: yes
- 1) Proceed with standard installation (default - just press enter)
- 2) Customize installation
- 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 的命令。
安装选项菜单:
- 1) Proceed with standard installation (default - just press enter)
- 2) Customize installation
- 3) Cancel installation
- rustc --version
- cargo --version
- rustup update
- [root@localhost brotli-compress-vue-component]# wasm-pack build --target web --out-name compress --out-dir pkg
新版chrome无法请求内网接口的解决方法
开发本地项目时,访问接口如果遇到类似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),然后重启浏览器即可
Go实现多线程分片下载文件
我们在下载大文件时,通常会使用多线程下载的方式来加快下载速度。例如常用的多线程下载工具(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
请求头的讲解,来学习一下:
- 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
这个分片文件的保存路径RangeStart
和RangeEnd
下载的文件起始范围和结束范围,用于设定Range
请求头
此外,还有作为下载状态的属性:
DownloadSize
下载任务进行时,这个线程已下载的文件部分大小TaskDone
这个线程的下载任务是否完成
该类的成员方法如下:
DoShardGet
执行分片下载任务,在其中会根据RangeStart
和RangeEnd
设定对应的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
- 一个线程的下载任务
- package model
- import (
- "bufio"
- "fmt"
- "github.com/fatih/color"
- "io"
- "net/http"
- "os"
- "sync"
- )
- // 全局HTTP客户端
- var httpClient = http.Client{
- Transport: &http.Transport{
- // 关闭keep-alive确保一个线程就使用一个TCP连接
- DisableKeepAlives: true,
- },
- }
- // ShardTask 单个分片下载任务的任务参数和状态量
- type ShardTask struct {
- // 下载链接
- Url string
- // 分片序号,从1开始
- Order int
- // 这个分片文件的路径
- ShardFilePath string
- // 分片的起始范围(字节,包含)
- RangeStart int64
- // 分片的结束范围(字节,包含)
- RangeEnd int64
- // 已下载的部分(字节)
- DownloadSize int64
- // 该任务是否完成
- TaskDone bool
- }
- // NewShardTask 构造函数
- func NewShardTask(url string, order int, shardFilePath string, rangeStart int64, rangeEnd int64) *ShardTask {
- return &ShardTask{
- // 设定任务参数
- Url: url,
- Order: order,
- ShardFilePath: shardFilePath,
- RangeStart: rangeStart,
- RangeEnd: rangeEnd,
- // 初始化状态量
- DownloadSize: 0,
- TaskDone: false,
- }
- }
- // DoShardGet 开始下载这个分片(该方法在goroutine中执行)
- func (task *ShardTask) DoShardGet(waitGroup *sync.WaitGroup) {
- // 创建文件
- file, e := os.OpenFile(task.ShardFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
- if e != nil {
- color.Red("任务%d创建文件失败!", task.Order)
- color.HiRed("%s", e)
- return
- }
- // 准备请求
- request, e := http.NewRequest("GET", task.Url, nil)
- if e != nil {
- color.Red("任务%d创建请求出错!", task.Order)
- color.HiRed("%s", e)
- return
- }
- // 设定请求头
- request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.RangeStart, task.RangeEnd))
- // 发送请求
- response, e := httpClient.Do(request)
- if e != nil {
- color.Red("任务%d发送下载请求出错!", task.Order)
- color.HiRed("%s", e)
- return
- }
- // 读取请求体
- body := response.Body
- // 读取缓冲区
- buffer := make([]byte, 8092)
- // 准备写入文件
- writer := bufio.NewWriter(file)
- for {
- // 读取一次内容至缓冲区
- readSize, readError := body.Read(buffer)
- if readError != nil {
- // 如果读取完毕则退出循环
- if readError == io.EOF {
- break
- } else {
- color.Red("任务%d读取响应错误!", task.Order)
- color.HiRed("%s", readError)
- return
- }
- }
- // 把缓冲区内容追加至文件
- _, writeError := writer.Write(buffer[0:readSize])
- if writeError != nil {
- color.Red("任务%d写入文件时出现错误!", task.Order)
- color.HiRed("%s", writeError)
- return
- }
- _ = writer.Flush()
- // 记录下载进度
- task.DownloadSize += int64(readSize)
- }
- // 关闭全部资源
- _ = body.Close()
- _ = file.Close()
- // 标记任务完成
- task.TaskDone = true
- // 使线程组中计数器-1
- waitGroup.Done()
- }
构造函数NewShardTask
负责完成ShardTask
的参数传入和状态量初始化,而DoShardGet
方法实现了下载一个文件分片的完整步骤,从创建文件准备写入,到设定请求头,发出请求,最后读取响应体保存到文件。
此外,可见这里的http.Client
对象中,我们将其DisableKeepAlives
设为了true
即关闭keep-alive
,这是因为默认情况下Go语言的HTTP客户端会复用TCP连接,即使你多个线程发起请求,也会使用一个TCP连接进行。
而多线程下载需要每个线程持有一个单独的TCP连接来达到突破cwnd
的限制,因此这里关闭keep-alive
实现每个线程发起请求时,使用单独的TCP连接。
(2) ParallelGetTask
- 一整个多线程下载任务
- package model
- import (
- "bufio"
- "fmt"
- "gitee.com/swsk33/shard-download-demo/util"
- "github.com/fatih/color"
- "io"
- "net/http"
- "os"
- "path/filepath"
- "strconv"
- "sync"
- "time"
- )
- // ParallelGetTask 多线程下载任务类,存放一个多线程下载任务的参数和状态量
- type ParallelGetTask struct {
- // 文件的下载链接
- Url string
- // 文件的最终保存位置
- FilePath string
- // 下载并发数
- Concurrent int
- // 下载的分片临时文件保存文件夹
- TempFolder string
- // 下载文件的总大小
- TotalSize int64
- // 全部的下载分片任务参数列表
- ShardTaskList []*ShardTask
- }
- // NewParallelGetTask 构造函数
- func NewParallelGetTask(url string, filePath string, concurrent int, tempFolder string) *ParallelGetTask {
- return &ParallelGetTask{
- // 参数赋值
- Url: url,
- FilePath: filePath,
- Concurrent: concurrent,
- TempFolder: tempFolder,
- // 初始化状态量
- TotalSize: 0,
- ShardTaskList: make([]*ShardTask, 0),
- }
- }
- // 发送HEAD请求获取待下载文件的大小
- func (task *ParallelGetTask) getLength() error {
- // 发送请求
- response, e := http.Head(task.Url)
- if e != nil {
- color.Red("发送HEAD请求出错!")
- return e
- }
- // 读取并设定长度
- task.TotalSize = response.ContentLength
- return nil
- }
- // 根据待下载文件的大小和设定的并发数,创建每个分片任务对象
- func (task *ParallelGetTask) allocateTask() {
- // 如果并发数大于总大小,则进行调整
- if int64(task.Concurrent) > task.TotalSize {
- task.Concurrent = int(task.TotalSize)
- }
- // 开始计算每个分片的下载范围
- eachSize := task.TotalSize / int64(task.Concurrent)
- // 创建任务对象
- for i := 0; i < task.Concurrent; i++ {
- 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))
- }
- // 处理末尾部分
- if task.TotalSize%int64(task.Concurrent) != 0 {
- task.ShardTaskList[task.Concurrent-1].RangeEnd = task.TotalSize - 1
- }
- }
- // 根据任务列表进行多线程分片下载操作
- func (task *ParallelGetTask) downloadShard() {
- // 创建线程组
- waitGroup := &sync.WaitGroup{}
- // 开始执行全部分片下载线程
- for _, task := range task.ShardTaskList {
- go task.DoShardGet(waitGroup)
- waitGroup.Add(1)
- }
- // 等待全部下载完成
- waitGroup.Wait()
- }
- // 下载完成后,合并分片文件
- func (task *ParallelGetTask) mergeFile() error {
- // 创建目的文件
- targetFile, e := os.OpenFile(task.FilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0755)
- if e != nil {
- color.Red("创建目标文件出错!")
- return e
- }
- // 创建写入器
- writer := bufio.NewWriter(targetFile)
- // 准备读取每个分片文件
- for _, shard := range task.ShardTaskList {
- shardFile, e := os.OpenFile(shard.ShardFilePath, os.O_RDONLY, 0755)
- if e != nil {
- color.Red("读取分片文件出错!")
- return e
- }
- reader := bufio.NewReader(shardFile)
- readBuffer := make([]byte, 1024*1024)
- for {
- // 读取每个分片文件,一次读取1KB
- readSize, readError := reader.Read(readBuffer)
- // 处理结束或错误
- if readError != nil {
- if readError == io.EOF {
- break
- } else {
- color.Red("读取分片文件出错!")
- return readError
- }
- }
- // 写入到最终合并的文件
- _, writeError := writer.Write(readBuffer[0:readSize])
- if writeError != nil {
- color.Red("写入合并文件出错!")
- return writeError
- }
- _ = writer.Flush()
- }
- // 关闭分片文件资源
- _ = shardFile.Close()
- }
- // 关闭目的文件资源
- _ = targetFile.Close()
- return nil
- }
- // 删除分片临时文件
- func (task *ParallelGetTask) cleanShard() error {
- for _, shard := range task.ShardTaskList {
- e := os.Remove(shard.ShardFilePath)
- if e != nil {
- color.Red("删除分片临时文件%s出错!", shard.ShardFilePath)
- return e
- }
- }
- return nil
- }
- // 在一个新线程中,实时输出每个分片的下载进度和总进度
- func (task *ParallelGetTask) printTotalProcess() {
- go func() {
- // 上一次统计时的已下载大小,用于计算速度
- var lastDownloadSize int64 = 0
- for {
- // 如果全部任务完成则结束输出,并统计并发数
- allDone := true
- // 当前并发数
- currentTaskCount := 0
- for _, shardTask := range task.ShardTaskList {
- if !shardTask.TaskDone {
- allDone = false
- currentTaskCount += 1
- }
- }
- if allDone {
- break
- }
- // 统计所有分片已下载大小之和
- var totalDownloadSize int64 = 0
- for _, shardTask := range task.ShardTaskList {
- totalDownloadSize += shardTask.DownloadSize
- }
- // 计算速度
- currentDownload := totalDownloadSize - lastDownloadSize
- lastDownloadSize = totalDownloadSize
- speedString := util.ComputeSpeed(currentDownload, 300)
- // 输出到控制台
- fmt.Printf("\r当前并发数:%3d 速度:%s 总进度:%3.2f%%", currentTaskCount, speedString, float32(totalDownloadSize)/float32(task.TotalSize)*100)
- // 等待300ms
- time.Sleep(300 * time.Millisecond)
- }
- }()
- }
- // Run 开始执行整个分片多线程下载任务
- func (task *ParallelGetTask) Run() error {
- // 获取文件大小
- e := task.getLength()
- if e != nil {
- color.Red("%s", e)
- return e
- }
- color.HiYellow("已获取到下载文件大小:%d字节", task.TotalSize)
- // 分配任务
- task.allocateTask()
- color.HiYellow("已完成分片任务分配,共计%d个任务", len(task.ShardTaskList))
- // 开启进度输出
- task.printTotalProcess()
- // 开始下载分片
- task.downloadShard()
- color.HiYellow("\n所有分片已下载完成!")
- // 开始合并文件
- e = task.mergeFile()
- if e != nil {
- color.Red("%s", e)
- return e
- }
- color.HiYellow("合并分片完成!")
- // 清理临时分片文件
- e = task.cleanShard()
- if e != nil {
- color.Red("%s", e)
- return e
- }
- color.HiYellow("清理分片临时文件完成!")
- color.Green("分片下载任务完成!")
- return nil
- }
上述printTotalProcess
函数中,util.ComputeSpeed
函数用于计算下载速度并自动转换为可读单位,代码如下:
- package util
- import (
- "fmt"
- "math"
- )
- // 关于单位的实用工具函数
- // ComputeSpeed 计算网络速度
- // size 一段时间内下载的数据大小,单位字节
- // timeElapsed 经过的时间长度,单位毫秒
- // 返回计算得到的网速,会自动换算单位
- func ComputeSpeed(size int64, timeElapsed int) string {
- bytePerSecond := size / int64(timeElapsed) * 1000
- if 0 <= bytePerSecond && bytePerSecond <= 1024 {
- return fmt.Sprintf("%4d Byte/s", bytePerSecond)
- }
- if bytePerSecond > 1024 && bytePerSecond <= int64(math.Pow(1024, 2)) {
- return fmt.Sprintf("%6.2f KB/s", float64(bytePerSecond)/1024)
- }
- if bytePerSecond > 1024*1024 && bytePerSecond <= int64(math.Pow(1024, 3)) {
- return fmt.Sprintf("%6.2f MB/s", float64(bytePerSecond)/math.Pow(1024, 2))
- }
- return fmt.Sprintf("%6.2f GB/s", float64(bytePerSecond)/math.Pow(1024, 3))
- }
可见通过构造函数NewParallelGetTask
完成参数传递和状态量设定后,其它每个私有函数都对应我们多线程下载中的一个步骤,最后由公开函数Run
统筹组织起所有的步骤,完成整个多线程下载任务。
3,实现效果
现在我们在main
函数中创建一个ParallelGetTask
对象,设定好参数后调用其Run
方法即可开始多线程下载文件的任务:
- package main
- import (
- "gitee.com/swsk33/shard-download-demo/model"
- )
- func main() {
- // 创建任务
- task := model.NewParallelGetTask(
- "https://github.com/jgraph/drawio-desktop/releases/download/v24.7.17/draw.io-24.7.17-windows-installer.exe",
- "C:\\Users\\swsk33\\Downloads\\draw.io.exe",
- 64,
- "C:\\Users\\swsk33\\Downloads\\temp",
- )
- // 执行任务
- _ = task.Run()
- }
原生JS获取公网IP地址
- function getUserIP(callback) {
- var ip_dups = {};
- var RTCPeerConnection = window.RTCPeerConnection
- || window.mozRTCPeerConnection
- || window.webkitRTCPeerConnection;
- var useWebKit = !!window.webkitRTCPeerConnection;
- var mediaConstraints = {
- optional: [{RtpDataChannels: true}]
- };
- var servers = {
- iceServers: [
- {urls: "stun:stun.services.mozilla.com"},
- {urls: "stun:stun.l.google.com:19302"},
- ]
- };
- var pc = new RTCPeerConnection(servers, mediaConstraints);
- function handleCandidate(candidate){
- var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/
- var hasIp = ip_regex.exec(candidate)
- if (hasIp) {
- var ip_addr = ip_regex.exec(candidate)[1];
- if(ip_dups[ip_addr] === undefined)
- callback(ip_addr);
- ip_dups[ip_addr] = true;
- }
- }
- pc.onicecandidate = function(ice){
- if(ice.candidate) {
- handleCandidate(ice.candidate.candidate);
- }
- };
- pc.createDataChannel("");
- pc.createOffer(function(result){
- pc.setLocalDescription(result, function(){}, function(){});
- }, function(){});
- setTimeout(function(){
- var lines = pc.localDescription.sdp.split('\n');
- lines.forEach(function(line){
- if(line.indexOf('a=candidate:') === 0)
- handleCandidate(line);
- });
- }, 1000);
- }
- getUserIP((ip) => {
- console.log("ipppp === ",ip)
- //ipppp === 121.90.11.160
- })
SHELL 脚本删除文件或目录
使用SHELL脚本,删除指定文件或目录,
使用方法:bash /sh/delfiles.sh 11.txt 22.txt
- #!/bin/bash
- # 检查是否有参数传入
- if [ "$#" -eq 0 ]; then
- echo "[ERROR] No files to delete! At least one parameter is required! "
- exit 1
- fi
- # 打印脚本名称
- echo "Script Name: $0"
- # 打印脚本所在的目录
- echo "Script Directory: $(dirname $0)"
- # 打印当前工作目录
- echo "Working Directory: $PWD"
- # 进入当前工作目录
- cd $PWD
- echo "=================================================="
- # 循环遍历所有参数并删除文件
- for file in "$@"; do
- if [ -f "$file" ]; then
- rm "$file"
- echo "Deleted file ==> ./$file"
- elif [ -d "$file" ]; then
- rm -rf "$file"
- echo "Deleted directory ==> ./$file"
- else
- echo "File or directory does not exist: $file"
- fi
- done
- echo "=================================================="
Vue 设置proxy后post请求失效,pending挂起
- proxy: {
- '/dev-api': {
- target: 'http://1x.xx.x.xxx:xxxx', //
- changeOrigin: true,
- pathRewrite: {
- '^/dev-api': ''
- },
- //第一种方法
- onProxyReq: function(proxyReq, req, res, options) {
- if (req.body) {
- const bodyData = JSON.stringify(req.body)
- // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
- proxyReq.setHeader('Content-Type', 'application/json')
- proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData))
- // stream the content
- proxyReq.write(bodyData)
- }
- }
- }
- },
- //第二种方法,注释这行
- before: require('./mock/mock-server.js')