【Golang】多线程下载器的实现

news2024/11/25 7:04:45

〇、前言

多线程下载,顾名思义就是对一个文件进行切片访问,等待所有的文件下载完成后在本地进行拼接成一个整体文件的过程。
因此可以利用 golang 的多协程对每个分片同步下载,之后再合并且进行md5校验或者总长度校验。

一、请求资源

下载文件的本质就是从服务器获取数据,更笼统地说就是向服务器发送 GET请求。

1.1 http1.1协议

HTTP1.1 协议(RFC2616)开始支持获取文件的部分内容,这为并行下载以及断点续传提供了技术支持:Range\Content-Range。Range参数是本地发往服务器的http头参数;Content-Range是远程服务器发往本地http头参数。

1.2 Range\Content-Range

range: (unit=first byte pos)-[last byte pos] : 指定第一个字节位置和最后一个字节位置。

例子说明:

  • range: bytes=0-1300 : 表示第0-1300字节范围的内容发往远程服务器。

  • range: bytes=1301-23041: 表示第1201-23041字节范围的内容发往远程服务器。

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]

例子说明:

  • content-Range: bytes 0-797/1024000 : 表示0-797字节范围内容从服务器响应到客户端,1024000是文件总大小。

完成http响应后,http状态码返回:206 表示使用断掉续传方式,而一般200表示不使用断掉续传方式。
比如:

(base) luliang@shenjian ~ % curl --location --head ‘https://download.jetbrains.com/go/goland-2020.2.2.exe’
HTTP/2 302
date: Sat, 06 May 2023 11:52:42 GMT
content-type: text/html
content-length: 138
location: https://download.jetbrains.com.cn/go/goland-2020.2.2.exe
server: nginx
strict-transport-security: max-age=31536000; includeSubdomains;
x-frame-options: DENY
x-content-type-options: nosniff
x-xss-protection: 1; mode=block;
x-geocountry: China
x-geocode: CN
x-geocity: Taiyigong
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:51:35 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 f7c361bc042484d244950f166c4f320c.cloudfront.net (CloudFront)
x-amz-cf-pop: PVG52-E1
x-amz-cf-id: xkbWvLoSgdyhCV-gXgANy7pq_P4ndAHEBCznYtxiOIAuvEm5ew9Qlw==
age: 72

如果在响应的Header中存在Accept-Ranges首部(并且它的值不为 “none”),那么表示该服务器支持范围请求(支持断点续传)。
可以使用 curl 发送一个 HEADER 请求来进行检测:

(base) luliang@shenjian ~ % curl -I https://download.jetbrains.com.cn/go/goland-2020.2.2.exe
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:55:58 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 cf7a8587fc03d8367e313c3f45e5b454.cloudfront.net (CloudFront)
x-amz-cf-pop: BJS9-E1
x-amz-cf-id: UDJvsOsiddSrXUF9CzkUKucO9ClpNrFrj2m-M9S4LYJADs34pMn8wA==

在上面的响应中, Accept-Ranges: bytes 表示界定范围的单位是 bytes,这里 Content-Length 也是很有用的信息,因为它提供了要检索的图片的完整大小!

如果站点返回的Header中不包括Accept-Ranges,那么它有可能不支持范围请求。一些站点会明确将其值设置为 “none”,以此来表明不支持。在这种情况下,某些应用的下载管理器可能会将暂停按钮禁用!

1.3 Last-Modified\If-Modified-Since

利用HTTP协议头Last-Modified\If-Modified-Since参数存储文件最后修改日期,每次通信文件要判断与上一次文件最后修改日期是否相同,如果不同就从0开始重新接收文件,相同则继续。Last-Modified 是由服务器往客户端发送的 HTTP 头,而If-Modified-Since 则是由客户端往服务器发送的头。
例如:

  • Last-Modified: Fri, 22 Feb 2023 03:45:06 GMT : 服务器端返回客户端HTTP头信息。

  • If-Modified-Since: Fri, 22 Feb 2013 03:45:02 GMT : 客户端通过 If-Modified-Since HTTP头将上一次服务器端发过来的 Last-Modified 时间戳发送回服务器端进行比较验证。

1.4 NewRequest()

该NewRequest()函数的定义为:

func NewRequest(method string, url string, body io.Reader) (*Request, error)

返回一个*Request,该结构体定义为:

type Request struct {
	Method           string
	URL              *url.URL
	Proto            string // "HTTP/1.0"
	ProtoMajor       int    // 1
	ProtoMinor       int    // 0
	Header           Header
	Body             io.ReadCloser
	GetBody          func() (io.ReadCloser, error)
	ContentLength    int64
	TransferEncoding []string
	Close            bool
	Host             string
	Form             url.Values
	PostForm         url.Values
	MultipartForm    *multipart.Form
	Trailer          Header
	RemoteAddr       string
	RequestURI       string
	TLS              *tls.ConnectionState
	Cancel           <-chan struct{}
	Response         *Response
	ctx              context.Context
}

1.5 http.DefaultClient.Do()

该函数定义为:

func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}

而函数 do()也返回一个 *Response,Response的结构体定义如下:

type Response struct {
	Status           string // e.g. "200 OK"
	StatusCode       int    // e.g. 200
	Proto            string // e.g. "HTTP/1.0"
	ProtoMajor       int    // e.g. 1
	ProtoMinor       int    // e.g. 0
	Header           Header
	Body             io.ReadCloser
	ContentLength    int64
	TransferEncoding []string
	Close            bool
	Uncompressed     bool
	Trailer          Header
	Request          *Request
	TLS              *tls.ConnectionState
}

可以看到,Response 中有StatusCode 、Header 、Body等我们想要的信息。
因此可以打一套组合拳将Response得到:
在这里插入图片描述
用函数实现就是:

func (d *FileDownloader) getHeaderInfo() (int, error) {
	headers := map[string]string{
		"User_Agent": userAgent,
	}
	req, err := getNewRequest(d.url, "HEADER", headers) // 得到一个 request
	resp, err := http.DefaultClient.Do(req)             // 利用 req 发送请求,获得一个请求
	if err != nil {
		return 0, err
	}
	fmt.Println(req)
	fmt.Println(resp)
	fmt.Println(resp.StatusCode)
	//                        对响应做出相应的处理
	//信息响应 (100–199)
	//成功响应 (200–299)
	//重定向消息 (300–399)
	//客户端错误响应 (400–499)
	//服务端错误响应 (500–599)
	if resp.StatusCode > 299 {
		// 如果出错就直接返回
		return 0, errors.New(fmt.Sprintf("Can't process, response is %v", resp.StatusCode))
	}

	// 检查是否支持断点续传
	if resp.Header.Get("Accept-Ranges") != "bytes" {
		return 0, errors.New("服务器不支持文件断点续传")
	}
	// 					支持断点传送时,获取相应的信息
	//获取文件名
	outputFileName, err := parseFileInfo(resp)
	if err != nil {
		return 0, errors.New(fmt.Sprintf("get file info err: %v", err))
	}
	// 返回文件名
	if d.outputFileName == "" {
		d.outputFileName = outputFileName

	}
	// 返回文件的长度
	return strconv.Atoi(resp.Header.Get("Content-Length"))

}
// 返回一个 Request
func getNewRequest(url, method string, headers map[string]string) (*http.Request, error) {
	r, err := http.NewRequest(
		method,
		url,
		nil,
	)
	if err != nil {
		return nil, err
	}
	// 设置头部信息,即 UserAgent 信息
	for k, v := range headers {
		r.Header.Set(k, v)
	}
	return r, err
}

1.6 获取文件名

我们先看看 Hear 上定义的方法:

A Header represents the key-value pairs in an HTTP header.
The keys should be in canonical form, as returned by CanonicalHeaderKey.
Methods on (Header):
Add(key string, value string)
Set(key string, value string)
Get(key string) string
Values(key string) []string
get(key string) string
has(key string) bool
Del(key string)
Write(w io.Writer) error
write(w io.Writer, trace *httptrace.ClientTrace) error
Clone() http.Header
sortedKeyValues(exclude map[string]bool) (kvs []http.keyValues, hs *http.headerSorter)
WriteSubset(w io.Writer, exclude map[string]bool) error
writeSubset(w io.Writer, exclude map[string]bool, trace *httptrace.ClientTrace) error
`Header` on pkg.go.dev 

里面有一个 get方法,它传入一个 key,返回一个值。我们可以传入一个想要的键从而得到想要的信息。
如果我们可以传入一个"Content-Disposition",得到 fileName。Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。

// 或得 filename
func parseFileInfo(resp *http.Response) (string, error) {
	contentDisposition := resp.Header.Get("Content-Disposition")
	if contentDisposition != "" {
		_, params, err := mime.ParseMediaType(contentDisposition)
		if err != nil {
			return "", err
		}
		return params["filename"], nil
	}
	filename := filepath.Base(resp.Request.URL.Path)
	return filename, nil
}

二、下载文件

两个重要的结构体:

// FileDownloader 定义下载器
type FileDownloader struct {
	// 待下载文件大小
	fileSize int
	// 目标源连接
	url string
	// 下载文件存储名
	outputFileName string
	// 文件切片的总数
	totalPart int
	// 文件存储目录
	outputDir string
	// 已完成文件切片
	doneFilePart []filePart
	// 文件校验
	md5 string
}
// 文件分片
type filePart struct {
	// 文件分片序号
	Index int
	// 开始下载 byte 起点
	From int
	// 结束byte
	To int
	// 下载得到的内容
	Data []byte
}

其中一个是定义的下载器,这个下载器定义了源地址、总文件大小、文件名、文件存储地址、md5 校验等;另一个定义了一个分片,这个分片定义了分片的身份(编号),文件开始点、结束点以及一个存储数据的Data。
接下来就可以初始化下载器了,填充一些基本的信息:

// NewFileDownloader 创建下载器(初始化)
func NewFileDownloader(url, outputFileName, outputDir string, totalPart int, md5 string) *FileDownloader {
	if outputDir == "" {
		// 如果为空,就获取当前目录
		wd, err := os.Getwd()
		if err != nil {
			log.Println(err)
		}
		outputDir = wd
	}
	return &FileDownloader{
		fileSize:       0,
		url:            url,
		outputFileName: outputFileName,
		totalPart:      totalPart,
		doneFilePart:   make([]filePart, totalPart),
		md5:            md5,
		outputDir:      outputDir,
	}
}

1.1 下载分片

func (d *FileDownloader) downloadPart(c filePart) error {
	headers := map[string]string{
		"User-Agent": userAgent,
		"Range":      fmt.Sprintf("bytes=%v-%v", c.From, c.To),
	}
	// 或得一个 request
	r, err := getNewRequest(d.url, "GET", headers)
	if err != nil {
		return err
	}
	// 打印要下载的分片信息
	log.Printf("开始[%d]下载from:%d to:%d\n", c.Index, c.From, c.To)
	resp, err := http.DefaultClient.Do(r)

	if resp.StatusCode > 299 {
		return errors.New(fmt.Sprintf("服务器错误状态码: %v", resp.StatusCode))
	}
	// 最后关闭文件
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
		}
	}(resp.Body)

	// 读取 Body 的响应数据
	bs, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if len(bs) != (c.To - c.From + 1) {
		return errors.New("下载文件分片长度错误")
	}
	c.Data = bs
	// c完成了后就加入到下载器中
	d.doneFilePart[c.Index] = c
	return nil
}

这个思路就是就把 Body 存储起来,那就是有效数据。之后就可以把所有的 数据合成成一个完整文件。

2.2 合成文件

// 合并要下载的文件
func (d *FileDownloader) mergeFileParts() error {
	path := filepath.Join(d.outputDir, d.outputFileName)
	log.Println("开始合并文件")
	// 创建文件
	mergedFile, err := os.Create(path)
	if err != nil {
		return err
	}
	// 最后关闭文件
	defer func(mergedFile *os.File) {
		err := mergedFile.Close()
		if err != nil {

		}
	}(mergedFile)

	// sha256是一种密码散列函数,说白了它就是一个哈希函数。
	//对于任意长度的消息,SHA256都会产生一个256bit长度的散列值,
	//称为消息摘要,可以用一个长度为64的十六进制字符串表示。
	fileMd5 := sha256.New()
	totalSize := 0

	// 合并的工作
	for _, s := range d.doneFilePart {
		_, err := mergedFile.Write(s.Data)
		if err != nil {
			fmt.Printf("error when merge file: %v\n", err)
		}
		fileMd5.Write(s.Data)    // 更新哈希值
		totalSize += len(s.Data) // 更新长度
	}
	// 校验文件完整性
	if totalSize != d.fileSize {
		return errors.New("文件不完整")
	}
	// 检验 MD5
	if d.md5 == "" {
		// 将整个文件进行了 Sum 运算, 该函数返回一个 16 进制串,转成字符串之后,
		// 和 d.md5比较,起到了一个校验的效果
		if hex.EncodeToString(fileMd5.Sum(nil)) != d.md5 {
			return errors.New("文件损坏")
		} else {
			log.Println("文件SHA-256校验成功")
		}
	}
	return nil
}

该函数合成了新文件还对文件完整性、MD5 做了校验。

三、多线程下载

func (d *FileDownloader) Run() error {
	// 获取文件大小
	fileTotalSize, err := d.getHeaderInfo()
	if err != nil {
		fmt.Printf("hello!!")
		return err
	}
	d.fileSize = fileTotalSize
	jobs := make([]filePart, d.totalPart)
	// 这里进行均分
	eachSize := fileTotalSize / d.totalPart

	for i := range jobs {
		jobs[i].Index = i
		// 计算 form
		if i == 0 {
			jobs[i].From = 0
		} else {
			jobs[i].From = jobs[i-1].To + 1
		}
		// 计算 to
		if i < d.totalPart-1 {
			jobs[i].To = jobs[i].From + eachSize
		} else {
			// 最后一个filePart
			jobs[i].To = fileTotalSize - 1
		}
	}

	// 多线程下载
	var wg sync.WaitGroup
	for _, j := range jobs {
		wg.Add(1)
		go func(job filePart) {
			defer wg.Done()
			err := d.downloadPart(job)
			if err != nil {
				log.Println("下载文件失败:", err, job)
			}
		}(j)
	}
	wg.Wait()

	return d.mergeFileParts()
}

该函数将文件总长度信息获取之后,进行了等分的分片,然后开启协程进行并发请求。

之后,我们在 main()函数中填上目标链接以及 md5值就可以下载了。

func main() {
	startTime := time.Now()
	url := "https://speed.hetzner.de/100MB.bin"
	md5 := "2f282b84e7e608d5852449ed940bfc51"
	downloader := NewFileDownloader(url, "", "", 8, md5)
	if err := downloader.Run(); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("\n 文件下载完成耗时: %f second\n", time.Now().Sub(startTime).Seconds())
}

运行效果:

2023/05/07 19:56:48 开始[7]下载from:365989316 to:418273495
2023/05/07 19:56:48 开始[0]下载from:0 to:52284187
2023/05/07 19:56:48 开始[5]下载from:261420940 to:313705127
2023/05/07 19:56:48 开始[4]下载from:209136752 to:261420939
2023/05/07 19:56:48 开始[3]下载from:156852564 to:209136751
2023/05/07 19:56:48 开始[1]下载from:52284188 to:104568375
2023/05/07 19:56:48 开始[6]下载from:313705128 to:365989315
2023/05/07 19:56:48 开始[2]下载from:104568376 to:156852563
…………

四、总结

该程序的流程简单,和爬虫相比,更简单,毕竟不用使用各种选择器+正则表达式来获取特定元素。本质上来说,就是在获取 GET 请求,只是绕的弯比较多。
另外这里有一个获取某个文件 md5 值的方法:

func getFileMd5(filename string) string {
	// 文件全路径名
	path := fmt.Sprintf("./%s", filename)
	pFile, err := os.Open(path)
	if err != nil {

		log.Println("打开文件失败!")
		return ""
	}
	defer func(pFile *os.File) {
		err := pFile.Close()
		if err != nil {
		}
	}(pFile)
	md5h := md5.New()
	io.Copy(md5h, pFile)

	return hex.EncodeToString(md5h.Sum(nil))
}
func main() {
	// 当前目录的csv配置文件为例
	fileName1 := "Tasks/Downloader/100MB.bin"
	fileName2 := "goland-2020.2.2.dmg"
	md5Val := getFileMd5(fileName2)
	md5Val1 := getFileMd5(fileName1)
	fmt.Println("配置文件的md5值:", md5Val, md5Val1)
	// 配置文件的md5值: 8c2e8bcad8f0612fb62c8d5bd21efb8f 2f282b84e7e608d5852449ed940bfc51
}

全文完,感谢阅读。

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

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

相关文章

5.7学习周报

文章目录 前言文献阅读摘要简介数据方法论预测结果结论 时间序列预测总结 前言 本周阅读文献《Water quality forecasting based on data decomposition, fuzzy clustering and deep learning neural network》&#xff0c;文献主要结合数据分解、模糊C均值聚类和双向门控循环…

设计模式 - 工厂方法模式

设计模式 - 工厂方法模式 1、关于工厂方法模式2、工厂方法模式小试牛刀2.1、类图2.2、代码清单3、工厂方法模式的扩展3.1、简单工厂模式&#xff08;静态工厂模式&#xff09; 1、关于工厂方法模式 工厂方法模式&#xff0c;就是定义一个用于创建对象的接口&#xff0c;让子类决…

Ubuntu20升级nodejs版本

执行 grunt build的时候提示node版本过低 当前版本为10.19.0&#xff0c;不满足要求 安装 n&#xff0c;用于更新 node 版本的 sudo npm install n -g用n 下载 nodejs 的最新稳定版本 sudo n stable3. 安装完毕&#xff0c;node -r检查当前版本

【算法与数据结构】链表

链表 链表&#xff1a;结构定义 链表是由一串节点串联在一起的&#xff0c;链表的每个节点存储两个信息&#xff1a;数据下一个节点的地址 分清楚两个概念&#xff1a;什么是内存内部&#xff0c;什么是程序内部 内存内部&#xff1a; 信息存储在内存空间里的 程序内部&#…

复习笔记1

考纲(张友生版本软件架构 考试题型&#xff1a; 10*3单选 5*3简答题 5*3设计图&#xff08;含画图&#xff09; 10*2 论述题 10*2综合题 复习以课件为主&#xff0c;书为辅 第一章 (软件危机) &#xff1f; &#xff1f; 构造模型与实现 掌握软件结构体系核心模型 第二章 软件体…

阿里云Alibaba Cloud Linux镜像系统介绍及常见问题解答FAQ

阿里云服务器操作系统Alibaba Cloud Linux镜像怎么样&#xff1f;可以代替CentOS吗&#xff1f;Alibaba Cloud Linux兼容性如何&#xff1f;有人维护吗&#xff1f;漏洞可以修复吗&#xff1f;Alibaba Cloud Linux完全兼容CentOS&#xff0c;并由阿里云官方免费提供长期维护。 …

Java并发,夺命 60 问

基础 1.并行跟并发有什么区别&#xff1f; 从操作系统的角度来看&#xff0c;线程是CPU分配的最小单位。 并行就是同一时刻&#xff0c;两个线程都在执行。这就要求有两个CPU去分别执行两个线程。 并发就是同一时刻&#xff0c;只有一个执行&#xff0c;但是一个时间段内&am…

linux系统中rpm与Yum软件仓库

rpm的作用&#xff1a; 在没有rpm软件管理之前我们在安装、升级、卸载服务程序时要考虑到其他程序、库的依赖关系&#xff0c;所以在进行安装、校验、卸载、升级等操作时的难度就非常之大。 rpm机制则为就是为了解决这些问题而设计的&#xff0c;rpm有点像windows系统中的控制…

xawtv涉及的vivid系统调用分析

xawtv涉及的vivid系统调用分析 文章目录 xawtv涉及的vivid系统调用分析调用过程分析摄像头驱动程序必需的11个ioctl非必须必须 分析数据的获取过程1.请求分配缓冲区: ioctl(4, VIDIOC_REQBUFS // 请求系统分配缓冲区2.查询映射缓冲区:3.把缓冲区放入队列:4.启动摄像头5.用selec…

Docker-Flannel网络讲解

目录 什么是Flannel Flannel原理 部署 环境准备 安装etcd 安装fannel 在server2上装flannel 测试 什么是Flannel flannel是 CoreOS 团队针对 Kubernetes 设计的一个覆盖网络&#xff08;Overlay Network&#xff09;工具&#xff0c;其目的在于帮助每一个使用 Kuberent…

文心一言 VS chatgpt (15)-- 算法导论3.2 4~5题

四、函数 ⌈ lg ⁡ n ⌉ ! \lceil \lg n \rceil ! ⌈lgn⌉! 多项式有界吗&#xff1f;函数 ⌈ lg ⁡ lg ⁡ n ⌉ ! \lceil \lg \lg n \rceil ! ⌈lglgn⌉! 多项式有界吗&#xff1f; 文心一言&#xff1a; chatgpt&#xff1a; 对于第一个问题&#xff0c;函数 ⌈ lg ⁡ n…

C++类和对象(6)

类和对象 1.在谈构造函数1.1. 构造函数体赋值1.2. 初始化列表1.3. explicit关键字 2. static成员2.1. 概念2.2. 特性 3.友元函数3.2.友元类 4. 内部类5.匿名对象6.拷贝对象时的一些编译器优化7.再次理解类和对象 1.在谈构造函数 1.1. 构造函数体赋值 在创建对象时&#xff0c…

【SPSS】回归分析详细操作教程(附案例实战)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

数据结构入门-顺序表链表

线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。线性表是一种实际中广泛使用多个数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线。…

前后端图片交互的简易方式

前后端图片交互的简易方式 一、交互方式说明二、前后端具体代码实现前端具体代码实现后端具体代码实现效果 测试结果 一、交互方式说明 在项目的实际开发中&#xff0c;难免会遇到前端需要渲染数据库中保存的图片&#xff0c;那咱知道图片也属于一种文件&#xff0c;不好保存到…

maven依赖jar包时版本冲突的解决

1、第一声明优先原则 在pom.xml配置文件中&#xff0c;如果有两个名称相同版本不同的依赖声明&#xff0c;那么先写的会生效。 所以&#xff0c;先声明自己要用的版本的jar包即可。 所以&#xff0c;添加新依赖时要放在最后边&#xff0c;以防止新依赖替换原有依赖造成版本冲…

Mybatis基础操作XML映射文件

Mybatis基本操作 一、环境准备 数据库表和springboot工程在课程资料中都有提供 注意&#xff0c;entrydate是用的jdk1.8中的LocalDate类型&#xff0c;createTime用的是jdk1.8中的LocalDateTime类型 二、删除 Mybatis中提供了一种参数占位符 #{id} empMapper.java package…

Leetcode461. 汉明距离

Every day a leetcode 题目来源&#xff1a;461. 汉明距离 解法1&#xff1a;模拟 先将x和y转为为对应二进制的字符串&#xff0c;再遍历两个字符串进行比较。 代码&#xff1a; /** lc appleetcode.cn id461 langcpp** [461] 汉明距离*/// lc codestart class Solution {…

HTML的两个实战项目

文章目录 HTML的两个实战项目1. 个人简历1. 1 快速生成模板1.2 根据简历格式进行预设计1.3 开始创作吧1.3.1 基本信息1.3.2 教育背景1.3.3 专业技能1.3.4 我的项目1.3.5 自我评价 2. 输入简历信息页面设计2.1 设计大概框架2.2 开始创作吧2.2.1 输入姓名2.2.2 上传照片2.2.3 输入…

Axure教程——用中继器制作动态柱状图

今天作者就教大家在Axure里面如何用中继器做一个可以动态的柱状图。 制作完成之应具备以下交互效果&#xff1a; 1.在中继器表格中填写具体数据和坐标轴后&#xff0c;自动生成对应的柱状图 2.鼠标移动到每项&#xff0c;显示其数据 预览地址&#xff1a;https://tj4v11.axshar…