文件斷點續傳原理與實現

在網絡狀況不好的情況下,對於文件的傳輸,我們希望能夠支持可以每次傳部分數據。首先從文件傳輸協議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好,屬於兩者的一個平衡的選擇,感興的可以深入研究一下。

發佈了56 篇原創文章 · 獲贊 401 · 訪問量 40萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章