现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

文件断点续传原理与实现

2015-11-04 13:52 工业·编程 ⁄ 共 8274字 ⁄ 字号 暂无评论
文章目录

在网络状况不好的情况下,对于文件的传输,我们希望能够支持可以每次传部分数据。首先从文件传输协议FTP和TFTP开始分析,

FTP是基于TCP的,一般情况下建立两个连接,一个负责指令,一个负责数据;而TFTP是基于UDP的,由于UDP传输是不可靠的,虽然传输速度很快,但对于普通的文件像PDF这种,少了一个字节都不行。本次以IM中的文件下载场景为例,解析基于TCP的文件断点续传的原理,并用代码实现。

什么是断点续传?

断点续传其实正如字面意思,就是在下载的断开点继续开始传输,不用再从头开始。所以理解断点续传的核心后,发现其实和很简单,关键就在于对传输中断点的把握,我就自己的理解画了一个简单的示意图:

原理:

断点续传的关键是断点,所以在制定传输协议的时候要设计好,如上图,我自定义了一个交互协议,每次下载请求都会带上下载的起始点,这样就可以支持从断点下载了,其实HTTP里的断点续传也是这个原理,在HTTP的头里有个可选的字段RANGE,表示下载的范围,下面是我用JAVA语言实现的下载断点续传示例。

提供下载的服务端代码:

import java.io.File; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.RandomAccessFile; 
import java.io.StringWriter; 
import java.net.ServerSocket; 
import java.net.Socket; 
 
// 断点续传服务端 
public class FTPServer { 
 
    // 文件发送线程 
    class Sender extends Thread{ 
        // 网络输入流 
        private InputStream in; 
        // 网络输出流 
        private OutputStream out; 
        // 下载文件名 
        private String filename; 
 
        public Sender(String filename, Socket socket){ 
            try { 
                this.out = socket.getOutputStream(); 
                this.in = socket.getInputStream(); 
                this.filename = filename; 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
         
        @Override 
        public void run() { 
            try { 
                System.out.println("start to download file!"); 
                int temp = 0; 
                StringWriter sw = new StringWriter(); 
                while((temp = in.read()) != 0){ 
                    sw.write(temp); 
                    //sw.flush(); 
                } 
                // 获取命令 
                String cmds = sw.toString(); 
                System.out.println("cmd : " + cmds); 
                if("get".equals(cmds)){ 
                    // 初始化文件 
                    File file = new File(this.filename); 
                    RandomAccessFile access = new RandomAccessFile(file,"r"); 
                    // 
                    StringWriter sw1 = new StringWriter(); 
                    while((temp = in.read()) != 0){ 
                        sw1.write(temp); 
                        sw1.flush(); 
                    } 
                    System.out.println(sw1.toString()); 
                    // 获取断点位置 
                    int startIndex = 0; 
                    if(!sw1.toString().isEmpty()){ 
                        startIndex = Integer.parseInt(sw1.toString()); 
                    } 
                    long length = file.length(); 
                    byte[] filelength = String.valueOf(length).getBytes(); 
                    out.write(filelength); 
                    out.write(0); 
                    out.flush(); 
                    // 计划要读的文件长度 
                    //int length = (int) file.length();//Integer.parseInt(sw2.toString()); 
                    System.out.println("file length : " + length); 
                    // 缓冲区10KB 
                    byte[] buffer = new byte[1024*10]; 
                    // 剩余要读取的长度 
                    int tatol = (int) length; 
                    System.out.println("startIndex : " + startIndex); 
                    access.skipBytes(startIndex); 
                    while (true) { 
                        // 如果剩余长度为0则结束 
                        if(tatol == 0){ 
                            break; 
                        } 
                        // 本次要读取的长度假设为剩余长度 
                        int len = tatol - startIndex; 
                        // 如果本次要读取的长度大于缓冲区的容量 
                        if(len > buffer.length){ 
                            // 修改本次要读取的长度为缓冲区的容量 
                            len = buffer.length; 
                        } 
                        // 读取文件,返回真正读取的长度 
                        int rlength = access.read(buffer,0,len); 
                        // 将剩余要读取的长度减去本次已经读取的 
                        tatol -= rlength; 
                        // 如果本次读取个数不为0则写入输出流,否则结束 
                        if(rlength > 0){ 
                            // 将本次读取的写入输出流中 
                            out.write(buffer,0,rlength); 
                            out.flush(); 
                        } else { 
                            break; 
                        } 
                        // 输出读取进度 
                        //System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %"); 
                    } 
                    //System.out.println("receive file finished!"); 
                    // 关闭流 
                    out.close(); 
                    in.close(); 
                    access.close(); 
                } 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
            super.run(); 
        } 
    } 
     
    public void run(String filename, Socket socket){ 
        // 启动接收文件线程  
        new Sender(filename,socket).start(); 
    } 
     
    public static void main(String[] args) throws Exception { 
        // 创建服务器监听 
        ServerSocket server = new ServerSocket(8888); 
        // 接收文件的保存路径 
        String filename = "E:\\ceshi\\mm.pdf"; 
        for(;;){ 
            Socket socket = server.accept(); 
            new FTPServer().run(filename, socket); 
        } 
    } 
 
}
 

 

下载的客户端代码:

import java.io.File; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.RandomAccessFile; 
import java.io.StringWriter; 
import java.net.InetSocketAddress; 
import java.net.Socket; 
 
// 断点续传客户端 
public class FTPClient { 
 
    /**
     *  request:get0startIndex0
     *  response:fileLength0fileBinaryStream
     *  
     * @param filepath
     * @throws Exception
     */ 
    public void Get(String filepath) throws Exception { 
        Socket socket = new Socket(); 
        // 建立连接 
        socket.connect(new InetSocketAddress("127.0.0.1", 8888)); 
        // 获取网络流 
        OutputStream out = socket.getOutputStream(); 
        InputStream in = socket.getInputStream(); 
        // 文件传输协定命令 
        byte[] cmd = "get".getBytes(); 
        out.write(cmd); 
        out.write(0);// 分隔符 
        int startIndex = 0; 
        // 要发送的文件 
        File file = new File(filepath); 
        if(file.exists()){ 
            startIndex = (int) file.length(); 
        } 
        System.out.println("Client startIndex : " + startIndex); 
        // 文件写出流 
        RandomAccessFile access = new RandomAccessFile(file,"rw"); 
        // 断点 
        out.write(String.valueOf(startIndex).getBytes()); 
        out.write(0); 
        out.flush(); 
        // 文件长度 
        int temp = 0; 
        StringWriter sw = new StringWriter(); 
        while((temp = in.read()) != 0){ 
            sw.write(temp); 
            sw.flush(); 
        } 
        int length = Integer.parseInt(sw.toString()); 
        System.out.println("Client fileLength : " + length); 
        // 二进制文件缓冲区 
        byte[] buffer = new byte[1024*10]; 
        // 剩余要读取的长度 
        int tatol = length - startIndex; 
        // 
        access.skipBytes(startIndex); 
        while (true) { 
            // 如果剩余长度为0则结束 
            if (tatol == 0) { 
                break; 
            } 
            // 本次要读取的长度假设为剩余长度 
            int len = tatol; 
            // 如果本次要读取的长度大于缓冲区的容量 
            if (len > buffer.length) { 
                // 修改本次要读取的长度为缓冲区的容量 
                len = buffer.length; 
            } 
            // 读取文件,返回真正读取的长度 
            int rlength = in.read(buffer, 0, len); 
            // 将剩余要读取的长度减去本次已经读取的 
            tatol -= rlength; 
            // 如果本次读取个数不为0则写入输出流,否则结束 
            if (rlength > 0) { 
                // 将本次读取的写入输出流中 
                access.write(buffer, 0, rlength); 
            } else { 
                break; 
            } 
            System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %"); 
        } 
        System.out.println("finished!"); 
        // 关闭流 
        access.close(); 
        out.close(); 
        in.close(); 
    } 
 
    public static void main(String[] args) { 
        FTPClient client = new FTPClient(); 
        try { 
            client.Get("E:\\ceshi\\test\\mm.pdf"); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    } 

测试

原文件、下载中途断开的文件和从断点下载后的文件分别从左至右如下:

断点前的传输进度如下(中途省略):

Client fileLength : 51086228
finish : 0.020044541 %
finish : 0.040089082 %
finish : 0.060133625 %
finish : 0.07430574 %
finish : 0.080178164 %
...
finish : 60.41171 %
finish : 60.421593 %
finish : 60.428936 %
finish : 60.448982 %
finish : 60.454338 %

断开的点计算:30883840 / 51086228 = 0.604543361471119 * 100% = 60.45433614%

从断点后开始传的进度(中途省略):
Client startIndex : 30883840
Client fileLength : 51086228
finish : 60.474377 %
finish : 60.494423 %
finish : 60.51447 %
finish : 60.53451 %
finish : 60.554558 %
...
finish : 99.922035 %
finish : 99.942085 %
finish : 99.95677 %
finish : 99.96213 %
finish : 99.98217 %
finish : 100.0 %
finished!

断点处前后的百分比计算如下:

============================下面是从断点开始的进度==============================

本方案是基于TCP,在本方案设计之初,我还探索了一下介于TCP与UDP之间的一个协议:UDT(基于UDP的可靠传输协议)。

我基于Netty写了相关的测试代码,用Wireshark拆包发现的确是UDP的包,而且是要建立连接的,与UDP不同的是需要建立连接,所说UDT的传输性能比TCP好,传输的可靠性比UDP好,属于两者的一个平衡的选择,感兴的可以深入研究一下。

原作者未知。

给我留言

留言无头像?