【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
文件断点续传之TUS
TUS是github上一个开源的断点续传的框架,服务端是用go写的,客户端也支持很多种,支持js的,andorid,java的,IOS也支持。本文将对android端做一些分析。
先来介绍框架最核心的部分,即如何做到续传。说到续传就是接着上一次上传的位置继续上传,而不用从头开始传。TUS的做法是,客户端在创建链接的时候上传必要的参数,服务端根据客户端传的参数寻找对应的文件,并返回上次上传的位置给客户端。客户端根据服务的返回的偏移量再进行上传。
那客户端要传递什么参数呢?客户端传什么参数才能保证服务器找到的文件就是对应的文件呢?这里使用了文件名和大小作为文件的唯一性。如果文件名相同,文件大小也相同,那么服务端就认为是同一个文件。
try {
SharedPreferences pref = getSharedPreferences("tus", 0);
client = new TusClient();
client.setUploadCreationURL(new URL("http://192.168.160.1:1080/files/"));
client.enableResuming(new TusPreferencesURLStore(pref));
} catch(Exception e) {
showError(e);
}
首先创建一个client对象,传入要上传的地址,设置上传信息的preference。
public void resumeUpload() {
try {
TusUpload upload = new TusAndroidUpload(fileUri, this);
uploadTask = new UploadTask(this, client, upload);
uploadTask.execute(new Void[0]);
} catch (Exception e) {
showError(e);
}
}
这段代码创建了上传的对象,并将上传的任务放到线程里面执行。这个里面传入了文件的信息。
protected URL doInBackground(Void... params) {
try {
TusUploader uploader = client.resumeOrCreateUpload(upload);
long totalBytes = upload.getSize();
long uploadedBytes = uploader.getOffset();
// Upload file in 1MiB chunks
uploader.setChunkSize(1024 * 1024);
Log.i("upload","upload_tus start");
while(!isCancelled() && uploader.uploadChunk() > 0) {
uploadedBytes = uploader.getOffset();
Log.i("upload","upload_tus byte = "+uploadedBytes);
publishProgress(uploadedBytes, totalBytes);
}
Log.i("upload","upload_tus finish");
uploader.finish();
return uploader.getUploadURL();
} catch(Exception e) {
exception = e;
cancel(true);
}
return null;
}
这个是上传的过程,先获取一个uploader对象,在这个uploader对象里面已经完成了和服务器的交互,并获取了偏移量。uploader.setChunkSize
这个方法设置了一次读取文件的大小。设置越大一次读取的字节越多。uploader.uploadChunk()
这个才是真正上传的地方。这里使用一个while循环来不断的读取文件并上传。publishProgress
是为了向用户展示进度条。 下面看看uploader.uploadChunk()
的实现。
public int uploadChunk() throws IOException, ProtocolException {
openConnection();
int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest);
int bytesRead = input.read(buffer, bytesToRead);
if(bytesRead == -1) {
// No bytes were read since the input stream is empty
return -1;
}
// Do not write the entire buffer to the stream since the array will
// be filled up with 0x00s if the number of read bytes is lower then
// the chunk's size.
output.write(buffer, 0, bytesRead);
output.flush();
offset += bytesRead;
bytesRemainingForRequest -= bytesRead;
if(bytesRemainingForRequest <= 0) {
finishConnection();
}
return bytesRead;
}
这里首先去打开链接,如果链接已经打开那么就返回。然后再去从文件里面读数据,然后再写入outputstream里面并刷新缓冲区。再将偏移量累加。后面这几段代码就不是很理解了。bytesRemainingForRequest
这个是一次请求发送的大小,如果一次链接发送的请求大于这个数则将请求断开,再次循环的时候再进行重新链接。大家一定会这个很纳闷,为什么要重新链接呢。这个其实和客户端使用的inputstream类型是有关系的。如果使用bufferedinputstream,这种类型的inputstream为了减少对文件的操作,会将读取的文件字节进行缓存,读取的字节越多那么缓存越大,这样就会势必造成申请大内存。申请的内存大到一定的程度就可能出现oom。所以这里为了解决这个问题使用了bytesRemainingForRequest
来控制bufferedinputstream缓存的大小。这种方式固然缩小了对文件的访问,但是经过我实际的测试,这种方式并不可取,首先每次断开链接再进行重新链接的时候,在传输过程中可能造成tcp reset的异常或者IO错误。特别是在上传大文件的时候,要经过几次的断开链接和链接的过程。为此我在这里舍弃了bufferedinputstream的使用。修改如下的代码:
public TusInputStream(InputStream stream) {
/* if(!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}*/
this.stream = stream;
}
上面屏蔽的代码既是让其不使用bufferedinputstream。并将断开链接的条件删除:
public int uploadChunk() throws IOException, ProtocolException {
openConnection();
int bytesToRead = Math.min(getChunkSize(), bytesRemainingForRequest);
int bytesRead = input.read(buffer, bytesToRead);
if(bytesRead == -1) {
// No bytes were read since the input stream is empty
return -1;
}
// Do not write the entire buffer to the stream since the array will
// be filled up with 0x00s if the number of read bytes is lower then
// the chunk's size.
output.write(buffer, 0, bytesRead);
output.flush();
offset += bytesRead;
/*
bytesRemainingForRequest -= bytesRead;
if(bytesRemainingForRequest <= 0) {
finishConnection();
}
*/
return bytesRead;
}
bytesRemainingForRequest
将不再作为关闭链接的条件。
来源:oschina
链接:https://my.oschina.net/u/1013544/blog/3151172