Android FTP 多线程断点续传下载上传的实例
最近在给我的开源下载框架Aria增加FTP断点续传下载和上传功能,在此过程中,爬了FTP的不少坑,终于将功能实现了,在此把一些核心功能点记录下载。
FTP下载原理
FTP单线程断点续传
FTP和传统的HTTP协议有所不同,由于FTP没有所谓的头文件,因此我们不能像HTTP那样通过设置header向服务器指定下载区间。
但是FTP协议提供了一个更好用的命令REST用于从指定位置恢复任务,同时FTP协议也提供了一个命令SIZE用于获取下载的文件大小,有了这两个命令,FTP断点续传也就没有什么问题。
FTP断点续传的原理和HTTP的断点续传原理差不多,在暂停时记录文件的停止位置,再次下载时,先读取记录的位置,如果位置存在,则通过REST命令告诉服务器从指定区间进行下载。
FTP多线程断点续传
多线程下载的原理和HTTP多线程下载的原理差不多。先获取文件大小,然后根据线程数,对整个文件进行分段下载,在任务停止时,记录每一条线程的暂停位置,重新开始下载,每一条线程读取对应的下载记录,然后每一线程从指定位置开始下载。
分段下载
和HTTP所不同的是,FTP并没有提供文件区间的API,因此,FTP在分段下载中,只有起始位置而没有结束位置。 因此,你需要在指定位置手动停止线程。
功能实现
本文使用将采用apache commons-net实现FTP断点续传下载上传功能。<br
通过下文的几步操作,你就能很简单的实现FTP断点续传。
登录
FTP协议和HTTP协议有所不同,使用FTP进行下载时,你需要进行登录操作。
当然,如果你服务器没有登录功能,你可以忽略登录操作。
FTPClient client = new FTPClient();
client.connect(serverIp, port); //连接到FTP服务器
client.login(userName, passsword);
通过上面三行代码,就可以很简单的登录到FTP服务器上。
在进行登录后,还需要验证是否登录成功
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
Log.d(TAG, "无法连接到ftp服务器,错误码为:" + reply);
return;
}
由于FTP协议中,连接成功的状态有多个,因此需要通过FTPReply.isPositiveCompletion(reply)
用于验证是否成功连接到FTP服务器。
文件信息获取
在连接到FTP服务器后,就需要开始获取下载最重要的几个参数(文件长度、文件名)。
客户端可以通过client.listFiles(remotePath)
获取FTP服务器上该路径的文件列表。
- 如果路径是文件,只会返回一个长度为1的数组。
- 如果该路径为文件夹,则会返回该文件夹下对应的所有文件。
String remotePath = "/upload/qjnn.apk"; //FTP服务器上文件路径
FTPFile[] files = client.listFiles(remotePath);
FTPFile file = files[0]; //文件信息
long size = file.getSize();
String fileaName = file.getName();
如果你的文件为英文名,并且路径中没有中文,那么通过上述代码,便可以获取到正确的文件信息。
但如果FTP上的服务器上的文件名有中文或路径有中文,那么上述代码,你将获取不到正确的文件信息。
正确的写法
由于FTP服务器默认的编码是ISO-8859-1,因此,客户端在获取文件信息时
- 需要请求服务器使用UTF-8编码(如果服务器支持的话),如果服务器不支持开启UTF-8编码,那么客户端需要指定字符串编码格式
- 客户端在请求remotePath路径、获取文件名时,都需要对路径进行编码转换处理。
String remotePath = "/upload/qjnn.apk"; //FTP服务器上文件路径
String charSet = "UTF-8";
if (!FTPReply.isPositiveCompletion(client.sendCommand("OPTS UTF8", "ON"))) { //向服务器请求使用"UTF-8"编码
charSet = "GBK";
}
FTPFile[] files = client.listFiles(new String(remotePath.getBytes(charSet), "ISO-8859-1")); //对remotePath进行编码转换
FTPFile file = files[0]; //文件信息
long size = file.getSize();
String fileaName = new String(fileName.getBytes(), Charset.forName(charSet));
通过以上代码,便可以获取到正确的文件信息。
文件下载
配置每条线程的下载区间
long fileLength = mEntity.getFileSize();
Properties pro = CommonUtil.loadConfig(mConfigFile);
int blockSize = (int) (fileLength / mThreadNum);
int[] recordL = new int[mThreadNum];
for (int i = 0; i < mThreadNum; i++) {
recordL[i] = -1;
}
int rl = 0;
for (int i = 0; i < mThreadNum; i++) {
long startL = i * blockSize, endL = (i + 1) * blockSize;
Object state = pro.getProperty(mTempFile.getName() + "_state_" + i);
if (state != null && Integer.parseInt(state + "") == 1) { //该线程已经完成
if (resumeRecordLocation(i, startL, endL)) return;
continue;
}
//分配下载位置
Object record = pro.getProperty(fileName + "_record_" + i);
//如果有记录,则恢复下载
if (record != null && Long.parseLong(record + "") = 0) {
Long r = Long.parseLong(record + "");
mConstance.CURRENT_LOCATION += r - startL;
Log.d(TAG, "任务【" + mEntity.getFileName() + "】线程__" + i + "__恢复下载");
startL = r;
recordL[rl] = i;
rl++;
} else {
recordL[rl] = i;
rl++;
}
//最后一个线程的结束位置即为文件的总长度
if (i == (mThreadNum - 1)) endL = fileLength;
//创建分段线程
AbsThreadTask task = createSingThreadTask(i, startL, endL, fileLength);
if (task == null) return;
mTask.put(i, task);
}
startSingleTask(recordL);
在上面的代码中,主要做了两步操作:
- 在文件下载前,先从本地文件中读取当前下载的每一条线程的下载情况
- 如果下载记录存在,从记录位置开始下载,如果记录不存在,则重新开始下载
FTP 分段线程区间自动停止
由于FTP协议没有区间下载的原因,为了让线程只下载特定区间的内容,需要客户端在单条线程累计读的数据长度已经超过了所分配的区间长度的时候,停止该条线程。
client.enterLocalPassiveMode(); //设置被动模式
client.setFileType(FTP.BINARY_FILE_TYPE); //设置文件传输模式
client.setRestartOffset(mConfig.START_LOCATION); //设置恢复下载的位置
client.allocate(mBufSize);
is = client.retrieveFileStream(new String(remotePath.getBytes(charSet), SERVER_CHARSET));
//发送第二次指令时,还需要再做一次判断
reply = client.getReplyCode();
if (!FTPReply.isPositivePreliminary(reply)) {
client.disconnect();
fail(mChildCurrentLocation, "获取文件信息错误,错误码为:" + reply, null);
return;
}
file = new BufferedRandomAccessFile(mConfig.TEMP_FILE, "rwd", mBufSize);
file.seek(mConfig.START_LOCATION);
byte[] buffer = new byte[mBufSize];
int len;
while ((len = is.read(buffer)) != -1) {
//如果该条线程读取的数据长度大于所分配的区间长度,则只能读到区间的最大长度
if (mChildCurrentLocation + len = mConfig.END_LOCATION) {
len = (int) (mConfig.END_LOCATION - mChildCurrentLocation);
file.write(buffer, 0, len);
progress(len);
break;
} else {
file.write(buffer, 0, len);
progress(len);
}
}
这里还有几个坑需要处理一下:
- 对于FTP客户端来说,一般需要设置被动模式,被动模式和主动模式的区别
- 在获取文件流后,还需要使用FTPReply.isPositivePreliminary(reply)进行第二次命令判断
关于FTP文件上传
FTP 文件断点续传的方式原理和下载的都差不多:
- 都是在停止的时候记录停止位置,重新开始下载的时候从指定位置通过REST命令恢复断点。
- 都需要在任务执行前获取文件信息,比对服务器上的文件。
而和下载有区别的是:
- FTP上传时需要指定工作目录、在远程服务器上创建文件夹
- 需要服务器给用户打开删除和读入IO的权限,否则会出现550权限错误问题
- 上传文件需要storeFileStream获取outputStream流
最终效果
以上就是本文的全部内容,希望对大家的学习有所帮助。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Centos7 安装 jdk8 的正确姿势 实践笔记
- 初探雾效果!shader 源码分析与讲解! Cocos Creator 3D Shader Fog !
- js键盘键值大全
- mysql用户创建+密码修改+删除用户+角色分配 的正确姿势 实践笔记
- Centos7 安装 Nginx 的正确姿势 并设置开机自启 实践笔记
- js函数大全(2)
- HTML之打开/另存为/打印/刷新/查看原文件等按钮的代码
- Centos7 安装 Keepalived+Nginx 双机热备HA 的正确姿势 并开机自启 实践笔记
- 从 findbugs-maven-plugin 到 spotbugs-maven-plugin 帮你找到代码中的bug
- nmap常用命令检查主机在线与在线主机端口开放情况
- 想做测试工程师的可以看看
- nginx 实现Web应用程序的负载均衡
- 线上遇到nginx Cors跨域请求OPTIONS方法405 Method Not Allowed问题
- 常见Linux命令的正确打开姿势 实践笔记 更新中
- 用gogs轻松搭建个人的git服务器