一 前言
本文是上一篇博客JWebFileTrans:一款可以從網絡上下載文件的小程序(一)的續集。此篇博客主要在上一篇的基礎上加入了斷點續傳的功能,用戶在下載中途停止下載后,下次可以讀取斷點文件,接着上次已經下載的部分文件繼續下載。另外將程序名從JWebFileTrans更改為JDownload,並從github的utility repository中獨立出來專門維護,后續會添加多線程、ftp下載等功能。JDownload的github鏈接請點擊JDownload源代碼 。
另外時隔三個月后,我按照上一篇博客里面的四個下載鏈接再次測試的時候發現失敗了,原因在於其中有的http鏈接已經發生變化,比如stable目錄下的hbase從1.2.4升級成1.2.5了。然后我把其中有個鏈接中的1.2.4改成1.2.5結果也無法下載,后來發現是改成1.2.5后的鏈接是對的,但是服務器會把這個鏈接重定向到真正的下載鏈接。由於JDownload在設計過程中並沒有考慮到可能出現的重定向問題,所以對於此類鏈接暫時無法下載,但是在未來可能會考慮增加此類功能。所以大家在測試的時候選擇好正確的http鏈接,確保此鏈接當前存在,並且是真正的下載鏈接而不是重定向的鏈接,這樣才會測試成功。
PS: 本文是github用戶junhuster,以及微博用戶http://weibo.com/junhuster 的原創作品,轉載請注明原作者和博客出處,謝謝。
二 斷點續傳功能展示
測試鏈接 http://www.flashget.com/apps/flashget3.7.0.1222cn.exe ,這是快車下載軟件的官網下載鏈接,注意,如果讀者也用快車鏈接來做實驗的話,請先到快車官網檢查下最新的下載鏈接,因為諸如軟件升級改版本號等問題,就會導致本文給出的鏈接失效。在實驗的過程中,作者在虛擬機Ubuntu linux的終端shell里面先提交一個腳本從快車官網下載,等下載到1M左右的時候,終端輸入crtl-z中斷程序的執行。然后再次運行另外一個腳本,這個腳本會告訴JDownload 去讀取斷點文件,然后接着上次未完成的下載點繼續下載。下載成功后,把快車拷貝到windows 10操作系統里,然后點擊測試證明下載的文件可以正確執行。實驗過程的截圖如下:
如上圖,從第一張圖中的終端可以看出,中途筆者執行了crtl-z中斷了執行,圖中后綴.jbp .jbp0是斷點文件,斷點續傳依賴這些斷點數據來繼續下載,.part0是未下載完成的快車軟件。在第二張圖中讀者可以看到作者在中斷了上一次的執行后,再一次執行另外一個腳本shell1.sh,(這兩個腳本請參加作者的github JDownload的test目錄),開始繼續上次的下載,直到完成。最后一張圖是把在ubuntu中下載的快車軟件復制的windows里面執行的畫面,以確保下載的軟件沒有出現錯誤。顯然從圖中可以看出安裝程序可以正常執行。
三 基本思路
關於下載的功能設計部分請讀者參考作者的上一篇博客,或者直接參考github代碼,鏈接在前言中。本文主要描述如何在上一篇博客的基礎上加入斷點續傳的功能。
其實斷點續傳的功能挺簡單的,無非就是記錄一下上次下載的位置,然后再次下載的時候從那個位置開始就行了。從上一篇博客可以了解到,在下載一個文件的時候,我們會按照一定的規則把這個文件划分為N等分,每一次向服務器請求1/N的數據。所以顯然為了支持斷點續傳,我們需要記錄該文件被等分的數目N, 因為是續傳我們還要記錄上一次已經下載了多少個等分。中斷下載后再一次鏈接時需要知道服務器的鏈接(或者ip),因此這些信息也是需要記錄的。因此在單線程下載的時候只需要用一個文件記錄這些信息就行了。然而未來要假如多線程的支持,那個時候一個文件會同時由M個線程同時下載,每一個線程下載這個文件的第i到第j個等分,對於每一個線程來說,都有可能下載中斷重新續傳的可能性,所以每一個線程所下載的那部分文件都需要記錄一份斷點信息。而且應該還有一份單獨的信息記錄總共文件被分成了幾份來下載(例如4個線程就相當於把文件分成了四份來下載),這樣下載中斷后,下載就可以挨個讀取所有的斷點文件來續傳。
由以上描述我們可以設計出如下的數據結構:
下面這個描述的是總體斷點的信息情況
1 typedef struct break_point{ 2 3 long file_size; 4 long num_of_part_file; 5 long size_of_one_piece; 6 long total_num_of_piece_of_whole_file; 7 char server_ip[128+1]; 8 int server_port; 9 10 }break_point;
各個字段的含義是:
- file_size: 文件大小,以字節為單位
- num_of_part_file: 文件被分為多少份來下載,比如四個線程來下載就是四份
- size_of_one_piece: 前文說過,一個大文件被分為很多小的等分,這個字段就表示每一個等分的大小,也是以字節為單位
- total_num_of_piece_of_whole_file: 文件總共有多少等分
- server_ip: 服務器ip地址
- server_port: 服務器端口號
下面這個是描述的是每一個線程下載的那部分文件對應的斷點信息,當前實際上只有一個線程,未來會加入多線程的支持
1 typedef struct break_point_part 2 { 3 long start_num_of_piece_of_this_part_file; 4 long end_num_of_piece_of_this_part_file; 5 long size_of_last_incomplete_piece; 6 long alread_download_num_of_piece; 7 }break_point_of_part;
各個字段的含義是:
- start_num_of_piece_of_this_part_file: 該部分文件對應的起始等分數,也即前文提到的第i個等分
- end_num_of_piece_of_this_part_file: 分到的結束等分數,也即前文提到的第j個等分
- size_of_last_incomplete_piece: 文件不一定能夠完全均分,最后一份取余數
- alread_download_num_of_piece:已經下載了的等分數目
假如,我們一開始啟用了四個線程來下載,那么就會生成一份break_point信息,四份break_point_of_part信息。
在一開始的時候我們向服務器查詢要下載的文件的大小,然后根據自己代碼中每一次下載分片的大小,將文件記為N等分,這N等分將分給M個線程,每一個線程下載其中的N/M個等分。根據這些信息就可以創建出相應的斷點文件。然后每一個線程在每一次下載成功后就更新一下對應的斷點文件,主要是更新已經下載的等分數目。在中斷下載后,再一次下載的時候,程序首先會讀取break_point斷點文件,得到總共有多少個break_point_of_part斷點文件,然后挨個讀取break_point_of_part斷點文件,解析之,然后繼續上次未完成的地方繼續下載。
由上所述,我們可以設計出如下幾個函數:
1 int Http_create_breakpoint_file(char *file_name, FILE **fp_breakpoint, long file_size, long num_of_part_file, long size_of_one_piece, 2 long total_num_of_piece_of_whole_file, 3 char *server_ip, int server_port); 4 int Http_create_breakpoint_part_file(char *file_name, FILE **fp_breakpoint_part, int part_num, long start_num_of_piece_of_this_part_file, 5 long end_num_of_piece_of_this_part_file, 6 long size_of_last_incompelet_piece, 7 long alread_download_num_of_piece); 8 int Update_breakpoint_part_file(FILE *fp_breakpoint_part, int num_of_piece_tobe_added); 9 int Delete_breakpoint_file(char *file_name, FILE *fp);
四 代碼實現
下載部分的代碼請參考上一篇博客或者github源代碼。這里主要描述斷點相關函數。
首先是Http_create_breakpoint_file:
1 int Http_create_breakpoint_file(char *file_name, FILE **fp_breakpoint, long file_size, long num_of_part_file, long size_of_one_piece, 2 long total_num_of_piece_of_whole_file, 3 char *server_ip, int server_port){ 4 /** 5 ** check argument error 6 */ 7 if(file_name==NULL || fp_breakpoint==NULL){ 8 9 printf("Http_create_breakpoint_file: argument error\n"); 10 exit(0); 11 } 12 13 char *break_point_file_name=(char *)malloc((strlen(file_name)+4+1)*sizeof(char)); 14 if(NULL==break_point_file_name){ 15 16 printf("Http_create_breakpoint_file: malloc failed\n"); 17 exit(0); 18 19 } 20 strcpy(break_point_file_name,file_name); 21 strcat(break_point_file_name,".jbp"); 22 23 if(access(break_point_file_name,F_OK)==0){ 24 int ret=remove(break_point_file_name); 25 if(ret!=0){ 26 perror("Http_create_breakpoint_file,remove,\n"); 27 exit(0); 28 } 29 } 30 31 *fp_breakpoint=fopen(break_point_file_name,"w+"); 32 if(NULL==*fp_breakpoint){ 33 printf("Http_create_breakpoint_file: fopen failed\n"); 34 exit(0); 35 } 36 37 unsigned char *break_point_buffer=(unsigned char *)malloc(sizeof(break_point)+1000); 38 if(NULL==break_point_buffer){ 39 printf("Http_create_breakpoint_file: malloc failed\n"); 40 exit(0); 41 } 42 43 ((break_point *)break_point_buffer)->file_size=file_size; 44 ((break_point *)break_point_buffer)->num_of_part_file=number_of_part_file; 45 ((break_point *)break_point_buffer)->size_of_one_piece=size_of_one_piece; 46 ((break_point *)break_point_buffer)->total_num_of_piece_of_whole_file=total_num_of_piece_of_whole_file; 47 ((break_point *)break_point_buffer)->server_port=server_port; 48 49 memcpy(((break_point *)break_point_buffer)->server_ip, server_ip, strlen(server_ip)); 50 ((break_point *)break_point_buffer)->server_ip[strlen(server_ip)]='\0'; 51 52 53 int ret_fwrite=fwrite(break_point_buffer,sizeof(break_point),1,*fp_breakpoint); 54 fflush(*fp_breakpoint); 55 56 if(ret_fwrite!=1){ 57 printf("Http_create_breakpoint_file: fwrite failed \n"); 58 exit(0); 59 } 60 61 if(break_point_file_name!=NULL){ 62 free(break_point_file_name); 63 } 64 65 return 1; 66 }
然后是Http_create_breakpoint_part_file:
1 int Http_create_breakpoint_part_file(char *file_name, FILE **fp_breakpoint_part, int part_num, long start_num_of_piece_of_this_part_file, 2 long end_num_of_piece_of_this_part_file, 3 long size_of_last_incomplete_piece, 4 long alread_download_num_of_piece){ 5 if(file_name==NULL || fp_breakpoint_part==NULL || part_num<0){ 6 printf("Http_create_breakpoint_part_file, argument error\n"); 7 exit(0); 8 } 9 10 char buffer_for_part_num[6]; 11 sprintf(buffer_for_part_num, "%d",part_num); 12 int part_num_str_len=strlen(buffer_for_part_num); 13 char *break_point_part_file_name=(char *)malloc((strlen(file_name)+4+part_num_str_len+1)*sizeof(char)); 14 if(break_point_part_file_name==NULL){ 15 printf("Http_create_breakpoint_part_file,malloc failed\n"); 16 exit(0); 17 } 18 19 strcpy(break_point_part_file_name,file_name); 20 strcat(break_point_part_file_name,".jbp"); 21 strcat(break_point_part_file_name,buffer_for_part_num); 22 23 if(access(break_point_part_file_name,F_OK)==0){ 24 int ret=remove(break_point_part_file_name); 25 if(ret!=0){ 26 perror("Http_create_breakpoint_part_file,remove"); 27 exit(0); 28 } 29 } 30 31 *fp_breakpoint_part=fopen(break_point_part_file_name, "w+"); 32 if(*fp_breakpoint_part==NULL){ 33 printf("Http_create_breakpoint_part_file,fopen failed\n"); 34 exit(0); 35 } 36 37 break_point_of_part bpt; 38 bpt.start_num_of_piece_of_this_part_file=start_num_of_piece_of_this_part_file; 39 bpt.end_num_of_piece_of_this_part_file=end_num_of_piece_of_this_part_file; 40 bpt.size_of_last_incomplete_piece=size_of_last_incomplete_piece; 41 bpt.alread_download_num_of_piece=alread_download_num_of_piece; 42 43 44 int ret=fwrite(&bpt, sizeof(break_point_of_part), 1, *fp_breakpoint_part); 45 if(ret!=1){ 46 printf("Http_create_breakpoint_part_file,fwrite, break_point_of_part,error\n"); 47 exit(0); 48 } 49 50 51 fflush(*fp_breakpoint_part); 52 53 54 if(break_point_part_file_name!=NULL){ 55 free(break_point_part_file_name); 56 } 57 58 return 0; 59 60 }
接下來是int Update_breakpoint_part_file:
1 int Update_breakpoint_part_file(FILE *fp_breakpoint_part, int num_of_piece_tobe_added){ 2 3 if(fp_breakpoint_part==NULL || num_of_piece_tobe_added<1){ 4 printf("Update_breakpoint_part_file,argument error\n"); 5 exit(0); 6 } 7 8 break_point_of_part *bpt=(break_point_of_part *)malloc(sizeof(break_point_of_part)); 9 if(bpt==NULL){ 10 printf("Update_breakpoint_part_file,malloc failed\n"); 11 exit(0); 12 } 13 fseek(fp_breakpoint_part, 0, SEEK_SET); 14 int ret_fread=fread(bpt, sizeof(break_point_of_part), 1, fp_breakpoint_part); 15 16 int start_num=bpt->start_num_of_piece_of_this_part_file; 17 int end_num=bpt->end_num_of_piece_of_this_part_file; 18 bpt->alread_download_num_of_piece=bpt->alread_download_num_of_piece+num_of_piece_tobe_added; 19 if((bpt->alread_download_num_of_piece)<=(bpt->end_num_of_piece_of_this_part_file-bpt->start_num_of_piece_of_this_part_file+1+1)){ 20 fseek(fp_breakpoint_part, 0, SEEK_SET); 21 int ret=fwrite(bpt, sizeof(break_point_of_part), 1, fp_breakpoint_part); 22 if(ret!=1){ 23 printf("Update_breakpoint_part_file,fwrite failed\n"); 24 exit(0); 25 } 26 fflush(fp_breakpoint_part); 27 }else{ 28 printf("Update_breakpoint_part_file, num_of_piece_tobe_added not correct\n"); 29 exit(0); 30 } 31 32 }
最后是Delete_breakpoint_file,文件下載成功后,這些斷點文件應該刪除
1 int Delete_breakpoint_file(char *file_name,FILE *fp){ 2 3 if(file_name==NULL || fp==NULL){ 4 printf("Delete_breakpoint_file, argument error\n"); 5 exit(0); 6 } 7 8 break_point *bp=(break_point *)malloc(sizeof(break_point)); 9 fseek(fp, 0, SEEK_SET); 10 int ret=fread(bp, sizeof(break_point), 1, fp); 11 if(ret!=1){ 12 printf("Delete_breakpoint_file,fread failed\n"); 13 exit(0); 14 } 15 int num=bp->num_of_part_file; 16 17 char *buffer=(char *)malloc((strlen(file_name)+4+100)); 18 for(int i=0;i<num;i++){ 19 20 char buffer_part[6]; 21 sprintf(buffer_part, "%d",i); 22 strcpy(buffer,file_name); 23 strcat(buffer,".jbp"); 24 strcat(buffer,buffer_part); 25 26 if(access(buffer, F_OK)==0){ 27 if(remove(buffer)!=0){ 28 perror("Delete_breakpoint_file,remove .jbp_num"); 29 exit(0); 30 } 31 } 32 33 } 34 35 36 37 fclose(fp); 38 strcpy(buffer, file_name); 39 strcat(buffer, ".jbp"); 40 41 if(access(buffer, F_OK)==0){ 42 if(remove(buffer)!=0){ 43 perror("Delete_breakpoint_file,remove .jbp"); 44 exit(0); 45 } 46 } 47 48 if(buffer!=NULL){ 49 free(buffer); 50 } 51 52 return 0; 53 }
注意,每一次重新續傳讀取斷點文件的時候模式不要設錯了,筆者之前每次讀取斷點文件以“w+”模式打開,結果每次下載完后,文件都是錯誤的,原因是w+模式打開文件,會把文件清零,這樣自然就出錯了。
五 結束語
自此整篇文章就結束了,更詳細的信息請訪問筆者的github鏈接。
聯系方式:https://github.com/junhuster/
http://weibo.com/junhuster/