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