javaer学rust(四)

2022-10-11  本文已影响0人  yiang飞扬

之前实现了文件上传下载的功能,这次我们利用go-fastdfs实现一个分片上传的功能,按照惯例,先贴代码

use std::fs::File;
use std::io::Read;
use std::thread;
use std::thread::spawn;
use reqwest::blocking::Response;

fn main() {
    let file_path="D:/uploadBigTest.txt";
    let mut file=std::fs::File::open(file_path).unwrap();
    let file_size=file.metadata().unwrap().len();

    let mut buf=Vec::new();
    file.read_to_end(&mut buf);

    let part_size=10*1024*1024;
    let part_num=if file_size%part_size==0{
        file_size/part_size
    }else{
        file_size/part_size+1
    };
    let client=reqwest::blocking::Client::new();
    let response=client.post("http://localhost:8234/group2/big/upload/")
        .header("Upload-Length",file_size)
        .header("Tus-Resumable","1.0.0")
        .send()
        .unwrap();
    let file_location=response.headers().get("Location").unwrap().to_str().unwrap().to_string();
    println!("upload url:{}",file_location);

    for i in 0..part_num {
        let start:usize= (i * part_size) as usize;
        let mut end=((i+1)*part_size) as usize;
        if end>(file_size as usize){
            end=file_size as usize;
        }
        let upload_url=file_location.clone();
        let body=(&buf[start..end]).to_vec();
        thread::spawn(move ||{
            let client=reqwest::blocking::Client::new();
            let response=client.patch(upload_url.as_str())
                .header("Content-Type","application/offset+octet-stream")
                .header("Tus-Resumable","1.0.0")
                .header("Upload-Offset",start)
                .body(body).send().unwrap();
            println!("part {} upload success",i);
        }).join();
    }
}

这段代码的大体流程如下:
1、先读取文件数据到数组,rust里面数组类型是Vec
2、根据文件大小和分片大小计算分片数量
3、按照go-fastdfs的接口,先调用big/upload接口获取分片上传的地址
4、开启多个线程进行分片上传

虽然代码能够运行,但还是有要继续优化的地方,我们先看一下用到的一些知识点和遇到问题的地方,后面有针对性的进行优化

1、为了使用多线程,这里我们使用reqwest的同步模式(使用异步模式在线程中有问题,而且异步模式其实也没必要在使用多线程了,异步和多线程就是解决阻塞,提高并发的的两种方式),可以看到main方法上已经没有async修饰符了,另外因为用了同步模式,别忘了把blocking特性加到Cargo.toml的依赖特性里面,创建client的时候需要使用blocking模块

let client=reqwest::blocking::Client::new();

2、mut修饰符 ,rust使用let定义的变量默认是不能修改的,类似java的final,如果定义的变量需要修改,则需要通过let mut进行定义,如果某个方法内部要对变量进行修改,则传参的时候也要带着mut修改符,如:

let mut buf=Vec::new();
file.read_to_end(&mut buf);

3、在java中,除了基本类型,对象都传递的是引用,但在rust中,传递引用需要用&符号明确指明,否则都是传递的指针。

4、三目运算符,rust中是没有java中的 <expression> ? <result1> : <result2>这种三目运算符的,但我们可以使用if{}else{}来替代,见代码:

let part_num=if file_size%part_size==0{
        file_size/part_size
    }else{
        file_size/part_size+1
    };

这里需要注意的是,代码不要写成下面这样

let part_num=if file_size%part_size==0{
        file_size/part_size;
}else{
        file_size/part_size+1;
};

两者的区别主要是在分号上,不加分号在rust中表示表达式,加上分号就变成了语句,表达式是可以作为值返回的,而语句返回的是()

5、rust开启线程的方式会比java方便一些, 通过thread::spawn()既可以开启一个线程,spawn方法里面是rust闭包,这里我们可以简单理解成java的匿名内部类类(其实还是有比较大的差异的),写法如下:

|param:type|->type{} 

代码中是不需要传参和返回值简化了的写法,相当于 ||{},即下面代码:

||{
     let client=reqwest::blocking::Client::new();
     let response=client.patch(upload_url.as_str())
            .header("Content-Type","application/offset+octet-stream")
           .header("Tus-Resumable","1.0.0")
           .header("Upload-Offset",start)
           .body(body).send().unwrap();
            println!("part {} upload success",i);
 }       

当然也可以把{}部分抽成具体的方法
另外我们在这块代码部分还看到一个move关键字,其实是将线程中用到的变量的所有权转移到闭包中来的意思,具体看下面内容

6、生命周期和所有权,生命周期我们都理解,任何变量都有一个从创建到销毁的过程。所有权是rust特有的概念,规定了变量使用的范围,用来进行内存管理,是rust不需要进行GC的基础。由所有权会引申出借用和引用的概念,当我们定义一个变量符赋值后,变量值就已经属于了这个变量,别的变量要用的话则需要借用,一旦别的变量借用了,原有的变量就不能再使用了,因为值的所有权已经变了,这其实和物理世界很像,就像你有一个东西借给了你的好朋友,那在朋友换回来之前,你就不能再使用这个物品了。举个例子:

  let s1=String::from("test ownership");
  let s2=s1;
  println!("{}",s1);

如果执行上面的代码是编译不过去的,编译器会提示下面信息

error[E0382]: borrow of moved value: `s1`
  --> src\main.rs:17:19
   |
15 |     let s1=String::from("test ownership");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
16 |     let s2=s1;
   |            -- value moved here
17 |     println!("{}",s1);
   |                   ^^ value borrowed here after move

错误信息提示的很明白,就是s1的所有权已经发生了转移,再去使用s1的值是不能用的。那如果想继续使用s1的值怎么办呢,这就需要在把值付给s1,可以理解成你朋友把东西还给你了(不过很可能你的朋友借了就不换了:) )。

let mut s1=String::from("test ownership");
let s2 = s1;
s1=s2;
println!("{}",s1);

另外就是一个变量的值同时只能被借用一次,这和我们的现实世界也很像,你一个东西是不能同时借给两个朋友使用的,如下代码是有问题的:

let mut s1=String::from("test ownership");
let s2 = s1;
let s3=s1;
println!("{}",s3);

但是在处理业务过程中,一个变量被同时处理的场景是不可避免的,在现实世界其实也一样,假如你有一本绝版的书,你两个朋友都想在同段时间内借阅,那怎么办呢?这就要用到引用的概念了,还是拿借书比喻,如果两个朋友都想同时借阅,我们是不是可以把他们邀请到家里一起阅读,这样是不是就解决问题了。我们把上面的错误代码改下,在变量上加个&符号就没问题了

    let s1=String::from("test ownership");
    let s2 = &s1;
    let s3=&s1;
    println!("{},{}",s1,s3);

继续用来对照现实,如果两个朋友在读书的过程中,你允许某个人可以翻到自己喜欢的章节,那是不是就容易造成冲突。在rust中,如果存在可变引用,即引用方可以修改变量值,那么和借用一样,只能同时存在一个引用。例如,下面的代码是不行的

 let mut s1=String::from("test ownership");
 let s2 = &mut s1;
 s2.push_str("12345");
 let s3=&s1;
 println!("{},{},{}",s1,s2,s3);

说了这么多,其实可以用一句话概括,即‘共享不可变,可变不共享‘

除了借用和引用,其实还有一种情况就是转让,也就是上面提到的move关键字,就是我把东西送给你了,以后它的生死存活就和我没关系了。我们上面讲所有权其实都是默认在变量的生命周期内,那如果借用或引用的范围已经超出了变量本身的生命周期了呢,就像你有一个宠物交给朋友养,但你移民火星再也不回来了,最好的选择就是把抚养权交给朋友。

再次回到代码中来,为什么线程部分需要加上move关键字,因为在闭包中我们用到了两个变量upload_url和body,因为这两个变量的生命周期是在for循环体内,但线程里面需要的变量生命周期比for循环体时间长,所以需要通过move关键字把所有权转移到线程的闭包里面去。另外我们可发现upload_url其实是对file_location的值的拷贝,为什么在闭包中不直接用file_location呢,我们不是可以通过move把file_location的所有权移到闭包中吗?因为我们线程在for循环里面,会存在多个线程,而move只能把所有权交给一个线程,因此每个线程我们都需要做个拷贝。虽然解决了问题,但这肯定不是好的办法,后面继续优化下,看看有没有好的办法。

上一篇下一篇

猜你喜欢

热点阅读