一 摘要
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. 下载文件主体部分,这一部分主要就是执行网址连接的解析、服务器的连接、分段下载等。代码也很简洁。
1 int JHttp_download_whole_file(char *link){ 2 /** 3 ** check argument errorint Http_link_parse(char *link, char **url, char **port, char **path, char **file_name); 4 */ 5 if(NULL==link){ 6 printf("JHttp_download_whole_file: argument error\n"); 7 exit(0); 8 } 9 10 char *url=NULL; 11 char *port=NULL; 12 char *path=NULL; 13 char *file_name=NULL; 14 15 Http_link_parse(link,&url,&port,&path,&file_name); 16 17 char *ip_str=NULL; 18 Http_get_ip_str_from_url(url,&ip_str); 19 20 int socket_fd=-1; 21 int port_int=atoi(port); 22 Http_connect_to_server(ip_str,port_int,&socket_fd); 23 24 long long file_size=0; 25 Http_query_file_size(path,ip_str,port,socket_fd,&file_size); 26 27 FILE *fp_breakpoint=NULL; 28 int piece_num=file_size/(download_one_piece_size); 29 int size_of_last_piece=file_size%(download_one_piece_size); 30 31 32 Http_create_breakpoint_file(file_name,&fp_breakpoint,(download_one_piece_size),file_size,0,size_of_last_piece,link); 33 34 FILE *fp_download_file=NULL; 35 Http_create_download_file(file_name,&fp_download_file,0); 36 37 long buffer_size=(download_one_piece_size)+1000;//besides file content, server will also send http header 38 unsigned char *buffer=(unsigned char *)malloc(sizeof(unsigned char)*buffer_size); 39 40 if(NULL==buffer){ 41 printf("JHttp_download_whole_file: malloc failed\n"); 42 exit(0); 43 } 44 45 for(int i=1;i<=piece_num;i++){ 46 47 memset(buffer,0,buffer_size); 48 49 long range_begin=(i-1)*(download_one_piece_size); 50 long range_end=i*(download_one_piece_size)-1; 51 52 Http_recv_file(socket_fd,range_begin,range_end,buffer,buffer_size,path,ip_str,port); 53 54 long long offset=(i-1)*(download_one_piece_size); 55 56 char *ptr=strstr(buffer,"\r\n\r\n"); 57 if(NULL==ptr){ 58 printf("JHttp_download_whole_file:recv file seems not correct\n"); 59 exit(0); 60 } 61 62 ptr+=4;//pass \r\n\r\n 63 64 Save_download_part_of_file(fp_download_file,ptr,(download_one_piece_size),offset); 65 66 } 67 68 if(size_of_last_piece>0){ 69 70 memset(buffer,0,buffer_size); 71 72 long range_begin=piece_num*(download_one_piece_size); 73 long range_end=range_begin+size_of_last_piece-1; 74 75 Http_recv_file(socket_fd,range_begin,range_end,buffer,buffer_size,path,ip_str,port); 76 77 long long offset=piece_num*(download_one_piece_size); 78 79 char *ptr=strstr(buffer,"\r\n\r\n"); 80 if(NULL==ptr){ 81 printf("JHttp_download_whole_file:recv file seems not correct\n"); 82 exit(0); 83 } 84 85 ptr+=4; 86 87 Save_download_part_of_file(fp_download_file,ptr,(range_end-range_begin+1),offset); 88 } 89 90 if(fclose(fp_download_file)!=0){ 91 printf("JHttp_download_whole_file:fclose file failed\n"); 92 } 93 94 char *download_file_name_part=(char *)malloc(sizeof(char)*(strlen(file_name)+6+1)); 95 strcpy(download_file_name_part,file_name); 96 strcat(download_file_name_part,".part0"); 97 98 Http_restore_orignal_file_name(download_file_name_part); 99 100 return 1; 101 }
10. 在下载过程中,文件名被加上了后缀.part0,下载完毕后要做一个文件名的恢复
1 int Http_restore_orignal_file_name(char *download_part_file_name){ 2 /** 3 ** check argument 4 */ 5 if(download_part_file_name==NULL){ 6 printf("Http_restore_orignal_file_name: argument error\n"); 7 exit(0); 8 } 9 char *new_name=(char *)malloc(sizeof(char)*(strlen(download_part_file_name)+1)); 10 if(NULL==new_name){ 11 printf("Http_restore_orignal_file_name: malloc failed\n"); 12 exit(0); 13 } 14 15 strcpy(new_name,download_part_file_name); 16 17 int file_length=strlen(new_name); 18 for(int i=file_length;i>0;i--){ 19 if(new_name[i]=='.'){ 20 new_name[i]='\0'; 21 break; 22 } 23 } 24 25 int ret=rename(download_part_file_name,new_name); 26 if(ret!=0){ 27 printf("Http_restore_orignal_file_name: rename failed\n"); 28 exit(0); 29 } 30 31 return 1; 32 33 34 }
五:结束语
至此关于JWebFileTrans的实现就分析完毕了,更完整的代码请读者到笔者的github上面下载,下载链接在本文的开头处。后续会增加断点续传、多线程下载、ftp下载等功能。