JWebFileTrans- 一款可以從網路上下載檔案的小程式(一)
一 摘要
JWebFileTrans是一款基於socket的網路檔案傳輸小程式,目前支援從HTTP站點下載檔案,後續會增加ftp站點下載、斷點續傳、多執行緒下載等功能。其程式碼已開源到github上面,下載網址是JWebFileTrans的github連結 。
二 下載功能演示截圖
筆者分別用3個連結做了下載測試,分別是apache tomcat映象、apache hbase 華中科大映象以及著名下載工具快車的官網下載連結,連結如下:
- http://www-us.apache.org/dist/tomcat/tomcat-8/v8.5.11/bin/apache-tomcat-8.5.11-fulldocs.tar.gz
- http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.4-bin.tar.gz
- http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.4-src.tar.gz
- http://www.flashget.com/apps/flashget3.7.0.1222cn.exe
下面幾個截圖分別是下載完後的檔案、解壓後的檔案、瀏覽檔案、執行快車安裝程式的截圖
在下文筆者會列出原始碼,需要注意的是,筆者在原始碼裡面定義了斷點續傳的資料結構,以及處理斷點續傳檔案的函式,但是實際上當前JWebFileTrans並不支援斷點續傳,這個功能會在後續的更新中提供。
三:基本思路
本文所涉及到的主要技術點分別是:Http協議、TCP傳輸協議、socket程式設計技術。雖然涉及到HTTP/TCP協議,但是本文並不需要了解這些協議的具體細節,我們只需要知道其中主要的幾個特性,以及幾個socket程式設計介面便可以實現一個網路檔案下載程式。
在HTTP協議中,有幾個頗為耳熟能詳的命令:Head、GET、POST、DELETE等等。本文所涉及到的主要是HEAD和GET. 假設HTTP服務端儲存了一些檔案供客戶端下載,那麼使用者傳送一個HEAD命令給服務端,服務端便會返回一個響應訊息給客服端。響應訊息裡面會包含一系列欄位,其中一個比較重要的欄位就是‘感興趣’的那個檔案的大小,這個大小是以位元組為單位的。HEAD命令以及相應的服務端發來的響應訊息都是有一定的格式的。網上有大量的介紹文章,此處筆者就不再贅述。HEAD命令得到的響應訊息只包含訊息頭,不包含‘感興趣’的那個檔案的具體內容資料。
GET命令與HEAD命令的區別在於,服務端除了傳送訊息的頭部外,緊跟著頭部還會發送‘感興趣’的那個檔案的內容。但是檔案的尺寸有可能非常大,比如好幾個G,這樣的話,如果用GET命令來請求服務端傳輸這個檔案的資料,顯然是非常‘不優雅’的。很難想象服務端一個響應訊息一下傳輸幾個G的資料。大家不用擔心,GET命令可以用於設定告訴服務端‘我’期望獲得檔案的某一小段內容,比如:第1000個位元組到第2000個位元組。
於是我們可以先用HEAD命令來獲得檔案的尺寸,假設為file_size, 然後我們設定每次下載檔案的一小段,假設這一小段的位元組數是one_piece,那麼我們向服務端請求file_size/one_piece次就可以獲得檔案的全部內容了。當然file_size並不一定是one_piece的整數倍,此是後話,下文原始碼部分會處理這種情況。
前文說的HEAD命令,GET命令,那麼怎麼使用呢?沒錯socket系列介面函式就可以解決這個問題。socket是一套網路程式設計介面,面向應用層它支援IPV4、IPV6協議族,面向傳輸層它支援TCP、UDP等傳輸協議。socket使用socket描述符來表示客服端-服務端的連結,使用connect()介面來與服務端建立連線,使用send()等介面向服務端傳送訊息,使用recv()等接收服務端發來的響應訊息。如果讀者對這些概念不是太熟悉的話,可以去網上搜索一下,筆者在寫JWebFileTrans的時候也是在網上搜索資料來學習的。
有了HEAD、GET命令以及socket系列介面函式後,相信讀者腦海中已經有了一個大致的下載程式框架了。那麼就讓我們一起來看看這些技術點如何通過程式碼的方式來轉換為一個迷你下載工具的。
四:JWebFileTrans程式碼實現
1. 下載連結解析,前文中我們做測試的有幾個連結,比如:http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.4-src.tar.gz",我們需要從這個連結裡面解析出四個元素,url部分:mirrors.hust.edu.cn(抱歉筆者一直分不清楚應該是URL還是URI);port埠部分:這個連結省略了埠號,預設http埠號是80,完整的網址應該在edu.cn後加:80;路徑部分:/apache/hbase/stable/;檔名:hbase-1.2.4-src.tar.gz
1 int Http_link_parse(char *link, char **url, char **port, char **path, char **file_name){ 2 3 /** 4 ** check argument 5 */ 6 if(NULL==link){ 7 printf("Http_link_parse: argument error, please provide correct link\n"); 8 exit(0); 9 } 10 11 char *url_begin=NULL; 12 char *url_end=NULL; 13 14 url_begin=strstr(link,"http://"); 15 if(NULL==url_begin){ 16 printf("Http_link_parse: not valid http link\n"); 17 exit(0); 18 } 19 20 url_begin=url_begin+7; 21 22 int link_length=strlen(link); 23 24 int i=0; 25 for(i=link_length;i>=7;i--){ 26 if('/'!=link[i]){ 27 continue; 28 } 29 else{ 30 break; 31 } 32 } 33 34 int j=0; 35 for(j=7;j<link_length;j++){ 36 if('/'!=link[j]){ 37 continue; 38 }else{ 39 break; 40 } 41 } 42 43 if(j>=link_length){ 44 printf("Http_link_parse: Http link path not intact\n"); 45 exit(0); 46 } 47 48 if(i<7){ 49 printf("Http_link_parse: Http link path not intact\n"); 50 exit(0); 51 } 52 char *path_begin=&(link[j]); 53 int path_length=link_length-j; 54 55 char *colon=strstr(url_begin,":"); 56 char *port_begin=NULL; 57 int url_length=0; 58 int port_length=0; 59 if(NULL==colon){ 60 61 *port="80";//default http port 62 url_end=&(link[j]); 63 url_length=url_end-url_begin; 64 65 }else{ 66 67 port_length=&(link[i])-colon-1; 68 port_begin=colon+1; 69 70 url_length=colon-url_begin; 71 72 } 73 74 char *file_name_tmp=&(link[i])+1; 75 int file_length=(link_length-1)-i; 76 77 *url=(char *)malloc(sizeof(char)*(url_length+1)); 78 if(port_length!=0){ 79 *port=(char *)malloc(sizeof(char)*(port_length+1)); 80 if(NULL==*port){ 81 printf("Http_link_parsed: malloc failed\n"); 82 exit(0); 83 } 84 memcpy(*port,port_begin,port_length); 85 (*port)[port_length]='\0'; 86 } 87 88 *path=(char *)malloc(sizeof(char)*(path_length+1)); 89 *file_name=(char *)malloc(sizeof(char)*(file_length+1)); 90 91 if(NULL==*url || NULL==*path ||NULL==*file_name){ 92 printf("Http_link_parsed: malloc failed\n"); 93 exit(0); 94 } 95 96 memcpy(*url,url_begin,url_length); 97 (*url)[url_length]='\0'; 98 99 memcpy(*path,path_begin,path_length); 100 (*path)[path_length]='\0'; 101 102 memcpy(*file_name, file_name_tmp, file_length); 103 (*file_name)[file_length]='\0'; 104 105 return 1; 106 107 }
2. 獲得字串形式的URL(域名)對應的IP地址。前文中提到了mirrors.hust.edu.cn,我們需要獲取它對應的ip地址,因為ip地址才是網路實際連結的載體,字串形式的域名是為了方便熱門閱讀才有的。這個可以使用《UNIX環境高階程式設計》中的getaddrinfo()函式,gethostbyname()也可以,這個介面雖然可以用但是實際上已經被廢棄了,最好使用getaddrinfo()介面。
1 int Http_get_ip_str_from_url(char *url, char **ip_str){ 2 3 /** 4 ** check argument 5 */ 6 if(NULL==url){ 7 printf("Http_get_ip_str_from_url: argument error\n"); 8 exit(0); 9 } 10 11 struct addrinfo *addrinfo_result=NULL; 12 struct addrinfo *addrinfo_cur=NULL; 13 struct addrinfo hint; 14 memset(&hint,0,sizeof(struct addrinfo)); 15 16 int res=getaddrinfo(url,"80",&hint,&addrinfo_result); 17 if(res!=0){ 18 printf("Http_get_ip_str_from_url: getaddrinfo failed\n"); 19 exit(0); 20 } 21 22 addrinfo_cur=addrinfo_result; 23 struct sockaddr_in *sockin=NULL; 24 char ip_addr_str[INET_ADDRSTRLEN+1]; 25 26 if(NULL!=addrinfo_cur){ 27 sockin=(struct sockaddr_in *)addrinfo_cur->ai_addr; 28 const char *ret=inet_ntop(AF_INET,&(sockin->sin_addr),ip_addr_str,INET_ADDRSTRLEN); 29 int ip_addr_str_len=strlen(ip_addr_str); 30 *ip_str=(char *)malloc(sizeof(char)*(ip_addr_str_len+1)); 31 if(NULL==ret){ 32 printf("Http_get_ip_str_from_url: ip_str malloc failed\n"); 33 exit(0); 34 } 35 memcpy(*ip_str,ip_addr_str,ip_addr_str_len); 36 (*ip_str)[ip_addr_str_len]='\0'; 37 } 38 39 freeaddrinfo(addrinfo_result); 40 41 return 1; 42 43 }
3. 上文我們解析出了ip地址和埠號(實際上每一種協議都有預設的埠號,一般網址中很少會出現埠號)。下一步我們就要向伺服器發起連結了。這裡需要注意的是“主機位元組序”和“網路位元組序”可能是不一樣的。主機位元組序指的是cpu的位元組序,一般情況下我們絕大部分的情況下主機位元組序是小端位元組序,而網路位元組序是大端位元組序。因此在程式設計的時候遇到這種情況要做好轉換工作。當然有現成的函式可以拿來做這個轉換工作。
1 int Http_connect_to_server(char *ip, int port, int *socket_fd){ 2 3 /** 4 ** check argument error 5 */ 6 if(ip==NULL || socket_fd==NULL){ 7 printf("Http_connect_to_server: argument error\n"); 8 exit(0); 9 } 10 11 *socket_fd=socket(AF_INET,SOCK_STREAM,0); 12 if(*socket_fd<0){ 13 perror("Http_connect_to_server"); 14 15 exit(0); 16 } 17 18 struct sockaddr_in sock_address; 19 sock_address.sin_family=AF_INET; 20 int ret_0 =inet_pton(AF_INET,ip,&(sock_address.sin_addr.s_addr)); 21 if(ret_0!=1){ 22 printf("Http_connect_to_server: inet_pton failed\n"); 23 exit(0); 24 } 25 sock_address.sin_port=htons(port); 26 27 int ret_1=connect(*socket_fd, (struct sockaddr*)&sock_address, sizeof(struct sockaddr_in)); 28 if(ret_1!=0){ 29 printf("Http_connect_to_server: invoke connect failed\n"); 30 exit(0); 31 } 32 33 return 1; 34 }
4. 在上一步我們連線了伺服器,於是我們就可以給伺服器傳送HEAD訊息來查詢要下載的檔案的大小了。
1 int Http_query_file_size(char *path, char *host_ip, char *port, int socket_fd,long long *file_size){ 2 /** 3 ** check argument error 4 */ 5 if(NULL==path || NULL==host_ip || NULL==port){ 6 7 printf("Http_query_file_size: argument error\n"); 8 exit(0); 9 } 10 11 char send_buffer[100]; 12 sprintf(send_buffer,"HEAD %s",path); 13 strcat(send_buffer," HTTP/1.1\r\n"); 14 strcat(send_buffer,"host: "); 15 strcat(send_buffer,host_ip); 16 strcat(send_buffer," : "); 17 strcat(send_buffer,port); 18 strcat(send_buffer,"\r\nConnection: Keep-Alive\r\n\r\n"); 19 20 int ret=send(socket_fd, send_buffer, strlen(send_buffer),0); 21 if(ret<1){ 22 printf("Http_query_file_size: send failed\n"); 23 exit(0); 24 } 25 26 char recv_buffer[500]; 27 int ret_recv=recv(socket_fd,recv_buffer,500,0); 28 if(ret_recv<1){ 29 printf("Http_query_file_size: recv failed\n"); 30 exit(0); 31 } 32 33 if(recv_buffer[13]!='O' || recv_buffer[14]!='K'){ 34 printf("Http_query_file_size: server response message status not ok\n"); 35 } 36 37 char *ptr=strstr(recv_buffer,"Content-Length"); 38 39 if(NULL==ptr){ 40 41 printf("Http_query_file_size: recv message seems wrong\n"); 42 exit(0); 43 44 } 45 46 ptr=ptr+strlen("Content-Length")+2; 47 *file_size=atoll(ptr); 48 49 return 1; 50 51 }
5. 在本地建立即將要下載的檔案,在沒下載完成之前後綴名加上.part0,完成後再改成原名稱。
1 int Http_create_download_file(char *file_name, FILE **fp_download_file,int part){ 2 /** 3 ** check argument error 4 */ 5 if(file_name==NULL || fp_download_file==NULL || part<0){ 6 7 printf("Http_create_download_file: argument error\n"); 8 exit(0); 9 } 10 11 char buffer_for_part[max_download_thread+1]; 12 sprintf(buffer_for_part,"%d",part); 13 int part_str_length=strlen(buffer_for_part); 14 char *download_file_name=(char *)malloc((strlen(file_name)+5+part_str_length+1)*sizeof(char)); 15 if(NULL==download_file_name){ 16 17 printf("Http_create_download_file: malloc failed\n"); 18 exit(0); 19 20 } 21 strcpy(download_file_name,file_name); 22 strcat(download_file_name,".part"); 23 strcat(download_file_name,buffer_for_part); 24 25 if(access(download_file_name,F_OK)==0){ 26 int ret=remove(download_file_name); 27 if(ret!=0){ 28 printf("Http_create_download_file: remove file failed\n"); 29 exit(0); 30 } 31 } 32 33 *fp_download_file=fopen(download_file_name,"w+"); 34 if(NULL==*fp_download_file){ 35 printf("Http_create_download_file: fopen failed\n"); 36 exit(0); 37 } 38 39 40 if(download_file_name!=NULL){ 41 free(download_file_name); 42 } 43 44 return 1; 45 }
6. 建立斷點檔案,檢測斷點檔案合法性的函式在當前版本的JWebFileTrans中並沒有發揮作用,暫不介紹,在本系列部落格增加斷點續傳功能後在介紹之。
7. JWebFileTrans核心程式碼,從伺服器接收傳輸來的檔案的資料。此處有幾個重點需要指出:
- 首先我們向伺服器傳送訊息索取range_begin--range_end區間內的檔案內容,但是伺服器傳輸檔案到客服端的時候,可能一次無法傳輸完畢,需要傳輸多次。因此我們要用一個while迴圈來接收range_begin--range_end範圍內的資料,直到成功接收到資料大小=range_begin--range_end.結束本次接收。
- 其次我們已經建立的連線可能會由於種種原因斷開了,比如伺服器主動斷開等。所以我們一旦檢測到這種情況,就要關閉socket,重新建立連線。可以從recv()函式的返回值來判斷,如果==0說明伺服器斷開了連線,如果小於0,說明出現了其他網路錯誤,如果>0則代表接收到的資料的位元組數。不論是伺服器斷開連線還是出現了網路錯誤,我們都應該立刻關閉當前連線,重新建立連線,然後接著下載。
- 伺服器在每一次請求中,發來的第一段資料的開頭是訊息頭,這一部分並不是有效的檔案資訊,訊息頭與檔案有效資料之間用\r\n\r\n隔開了,我們可以用strstr函式來定位檔案有效資料。注意,對於同一個請求的接下來服務端發來的資料都是有效檔案資料,並不包含訊息頭。
1 int Http_recv_file(int socket_fd, long long range_begin, long long range_end, unsigned char *buffer, long buffer_size, 2 char *path, char *host_ip, char *port){ 3 /** 4 ** check argument 5 */ 6 if(range_begin<0 || range_end<0 || range_end<range_begin || NULL==buffer || buffer_size<1){ 7 printf("Http_recv_file: rename failed\n"); 8 exit(0); 9 } 10 11 char send_buffer[200]; 12 char buffer_range[100]; 13 sprintf(buffer_range, "\r\nRange: bytes=%lld-%lld",range_begin,range_end); 14 15 sprintf(send_buffer,"GET %s",path); 16 strcat(send_buffer," HTTP/1.1\r\n"); 17 strcat(send_buffer,"host: "); 18 strcat(send_buffer,host_ip); 19 strcat(send_buffer," : "); 20 strcat(send_buffer,port); 21 strcat(send_buffer,buffer_range); 22 strcat(send_buffer, "\r\nKeep-Alive: 200"); 23 strcat(send_buffer,"\r\nConnection: Keep-Alive\r\n\r\n"); 24 25 int download_size=range_end-range_begin+1; 26 27 int port_num=atoi(port); 28 int ret0=send(socket_fd,send_buffer,strlen(send_buffer),0); 29 if(ret0!=strlen(send_buffer)){ 30 printf("send failed, retry\n"); 31 perror("Http_recv_file"); 32 exit(0); 33 } 34 int recv_size=0; 35 int length_of_http_head=0; 36 while(1){ 37 38 long ret=recv(socket_fd,buffer+recv_size+length_of_http_head,buffer_size,0); 39 if(ret<=0){ 40 41 recv_size=0; 42 length_of_http_head=0; 43 memset(buffer,0,buffer_size); 44 45 int ret=close(socket_fd); 46 if(ret!=0){ 47 perror("Http_recv_file"); 48 exit(0); 49 } 50 51 //seems not need to sleep 52 53 Http_connect_to_server(host_ip,port_num,&socket_fd); 54 int ret0=send(socket_fd,send_buffer,strlen(send_buffer),0); 55 if(ret0!=strlen(send_buffer)){ 56 printf("send failed, retry\n"); 57 perror("Http_recv_file"); 58 exit(0); 59 } 60 61 continue; 62 63 } 64 65 if(recv_size==0){ 66 char *ptr=strstr(buffer,"Content-Length"); 67 if(ptr==NULL){ 68 printf("Http_recv_file: recv buffer error\n"); 69 exit(0); 70 } 71 int size=atoll(ptr+strlen("Content-Length")+2); 72 if(size!=download_size){ 73 printf("Http_recv_file: send recv not match\n"); 74 exit(0); 75 } 76 77 char *ptr2=strstr(buffer,buffer_range+15); 78 if(NULL==ptr2){ 79 printf("Http_recv_file: expected range do not match recv range, %s\n%s\n",buffer,buffer_range+15); 80 exit(0); 81 } 82 83 char *ptr1=strstr(buffer,"\r\n\r\n"); 84 if(ptr1==NULL){ 85 printf("Http_recv_file: http header not correct\n"); 86 exit(0); 87 } 88 89 length_of_http_head=ptr1-(char*)buffer+4; 90 recv_size=recv_size+ret-length_of_http_head; 91 92 }else{ 93 recv_size+=ret; 94 } 95 96 if(recv_size==download_size){ 97 break; 98 } 99 100 } 101 102 return 1; 103 }
8. 儲存檔案到磁碟
1 int Save_download_part_of_file(FILE *fp, unsigned char *buffer, long buffer_size, long long file_offset){ 2 /** 3 ** check argument error 4 */ 5 if(NULL==fp || NULL==buffer || buffer_size<1 || file_offset<0){ 6 printf("Save_download_part_of_file: argument error\n"); 7 exit(0); 8 } 9 10 11 fseek(fp,file_offset,SEEK_SET); 12 int ret=fwrite(buffer,buffer_size,1,fp); 13 if(ret!=1){ 14 printf("Save_download_part_of_file: fwrite failed\n"); 15 exit(0); 16 } 17 return 1; 18 19 }
9. 下載檔案主體部分,這一部分主要就是執行網址連線的解析、伺服器的連線、分段下載等。程式碼也很簡潔。