一 摘要

  JWebFileTrans是一款基于socket的网络文件传输小程序,目前支持从HTTP站点下载文件,后续会增加ftp站点下载、断点续传、多线程下载等功能。其代码已开源到github上面,下载网址是JWebFileTrans的github链接 。

注:转载请注明博客原始链接,最近发现有人盗用我的博客却设置为他的原创。

二 下载功能演示截图

    笔者分别用3个链接做了下载测试,分别是apache tomcat镜像、apache hbase 华中科大镜像以及著名下载工具快车的官网下载链接,链接如下:

  1. http://www-us.apache.org/dist/tomcat/tomcat-8/v8.5.11/bin/apache-tomcat-8.5.11-fulldocs.tar.gz
  2. http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.4-bin.tar.gz
  3. http://mirrors.hust.edu.cn/apache/hbase/stable/hbase-1.2.4-src.tar.gz
  4. 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

 int Http_link_parse(char *link, char **url, char **port, char **path, char **file_name){

     /**
** check argument
*/
if(NULL==link){
printf("Http_link_parse: argument error, please provide correct link\n");
exit();
} char *url_begin=NULL;
char *url_end=NULL; url_begin=strstr(link,"http://");
if(NULL==url_begin){
printf("Http_link_parse: not valid http link\n");
exit();
} url_begin=url_begin+; int link_length=strlen(link); int i=;
for(i=link_length;i>=;i--){
if('/'!=link[i]){
continue;
}
else{
break;
}
} int j=;
for(j=;j<link_length;j++){
if('/'!=link[j]){
continue;
}else{
break;
}
} if(j>=link_length){
printf("Http_link_parse: Http link path not intact\n");
exit();
} if(i<){
printf("Http_link_parse: Http link path not intact\n");
exit();
}
char *path_begin=&(link[j]);
int path_length=link_length-j; char *colon=strstr(url_begin,":");
char *port_begin=NULL;
int url_length=;
int port_length=;
if(NULL==colon){ *port="";//default http port
url_end=&(link[j]);
url_length=url_end-url_begin; }else{ port_length=&(link[i])-colon-;
port_begin=colon+; url_length=colon-url_begin; } char *file_name_tmp=&(link[i])+;
int file_length=(link_length-)-i; *url=(char *)malloc(sizeof(char)*(url_length+));
if(port_length!=){
*port=(char *)malloc(sizeof(char)*(port_length+));
if(NULL==*port){
printf("Http_link_parsed: malloc failed\n");
exit();
}
memcpy(*port,port_begin,port_length);
(*port)[port_length]='\0';
} *path=(char *)malloc(sizeof(char)*(path_length+));
*file_name=(char *)malloc(sizeof(char)*(file_length+)); if(NULL==*url || NULL==*path ||NULL==*file_name){
printf("Http_link_parsed: malloc failed\n");
exit();
} memcpy(*url,url_begin,url_length);
(*url)[url_length]='\0'; memcpy(*path,path_begin,path_length);
(*path)[path_length]='\0'; memcpy(*file_name, file_name_tmp, file_length);
(*file_name)[file_length]='\0'; return ; }

2. 获得字符串形式的URL(域名)对应的IP地址。前文中提到了mirrors.hust.edu.cn,我们需要获取它对应的ip地址,因为ip地址才是网络实际链接的载体,字符串形式的域名是为了方便热门阅读才有的。这个可以使用《UNIX环境高级编程》中的getaddrinfo()函数,gethostbyname()也可以,这个接口虽然可以用但是实际上已经被废弃了,最好使用getaddrinfo()接口。

 int Http_get_ip_str_from_url(char *url, char **ip_str){

     /**
** check argument
*/
if(NULL==url){
printf("Http_get_ip_str_from_url: argument error\n");
exit();
} struct addrinfo *addrinfo_result=NULL;
struct addrinfo *addrinfo_cur=NULL;
struct addrinfo hint;
memset(&hint,,sizeof(struct addrinfo)); int res=getaddrinfo(url,"",&hint,&addrinfo_result);
if(res!=){
printf("Http_get_ip_str_from_url: getaddrinfo failed\n");
exit();
} addrinfo_cur=addrinfo_result;
struct sockaddr_in *sockin=NULL;
char ip_addr_str[INET_ADDRSTRLEN+]; if(NULL!=addrinfo_cur){
sockin=(struct sockaddr_in *)addrinfo_cur->ai_addr;
const char *ret=inet_ntop(AF_INET,&(sockin->sin_addr),ip_addr_str,INET_ADDRSTRLEN);
int ip_addr_str_len=strlen(ip_addr_str);
*ip_str=(char *)malloc(sizeof(char)*(ip_addr_str_len+));
if(NULL==ret){
printf("Http_get_ip_str_from_url: ip_str malloc failed\n");
exit();
}
memcpy(*ip_str,ip_addr_str,ip_addr_str_len);
(*ip_str)[ip_addr_str_len]='\0';
} freeaddrinfo(addrinfo_result); return ; }

3. 上文我们解析出了ip地址和端口号(实际上每一种协议都有默认的端口号,一般网址中很少会出现端口号)。下一步我们就要向服务器发起链接了。这里需要注意的是“主机字节序”和“网络字节序”可能是不一样的。主机字节序指的是cpu的字节序,一般情况下我们绝大部分的情况下主机字节序是小端字节序,而网络字节序是大端字节序。因此在编程的时候遇到这种情况要做好转换工作。当然有现成的函数可以拿来做这个转换工作。

 int Http_connect_to_server(char *ip, int port, int *socket_fd){

     /**
** check argument error
*/
if(ip==NULL || socket_fd==NULL){
printf("Http_connect_to_server: argument error\n");
exit();
} *socket_fd=socket(AF_INET,SOCK_STREAM,);
if(*socket_fd<){
perror("Http_connect_to_server"); exit();
} struct sockaddr_in sock_address;
sock_address.sin_family=AF_INET;
int ret_0 =inet_pton(AF_INET,ip,&(sock_address.sin_addr.s_addr));
if(ret_0!=){
printf("Http_connect_to_server: inet_pton failed\n");
exit();
}
sock_address.sin_port=htons(port); int ret_1=connect(*socket_fd, (struct sockaddr*)&sock_address, sizeof(struct sockaddr_in));
if(ret_1!=){
printf("Http_connect_to_server: invoke connect failed\n");
exit();
} return ;
}

4.  在上一步我们连接了服务器,于是我们就可以给服务器发送HEAD消息来查询要下载的文件的大小了。

 int Http_query_file_size(char *path, char *host_ip, char *port, int socket_fd,long long *file_size){
/**
** check argument error
*/
if(NULL==path || NULL==host_ip || NULL==port){ printf("Http_query_file_size: argument error\n");
exit();
} char send_buffer[];
sprintf(send_buffer,"HEAD %s",path);
strcat(send_buffer," HTTP/1.1\r\n");
strcat(send_buffer,"host: ");
strcat(send_buffer,host_ip);
strcat(send_buffer," : ");
strcat(send_buffer,port);
strcat(send_buffer,"\r\nConnection: Keep-Alive\r\n\r\n"); int ret=send(socket_fd, send_buffer, strlen(send_buffer),);
if(ret<){
printf("Http_query_file_size: send failed\n");
exit();
} char recv_buffer[];
int ret_recv=recv(socket_fd,recv_buffer,,);
if(ret_recv<){
printf("Http_query_file_size: recv failed\n");
exit();
} if(recv_buffer[]!='O' || recv_buffer[]!='K'){
printf("Http_query_file_size: server response message status not ok\n");
} char *ptr=strstr(recv_buffer,"Content-Length"); if(NULL==ptr){ printf("Http_query_file_size: recv message seems wrong\n");
exit(); } ptr=ptr+strlen("Content-Length")+;
*file_size=atoll(ptr); return ; }

5. 在本地创建即将要下载的文件,在没下载完成之前后缀名加上.part0,完成后再改成原名称。

 int Http_create_download_file(char *file_name, FILE **fp_download_file,int part){
/**
** check argument error
*/
if(file_name==NULL || fp_download_file==NULL || part<){ printf("Http_create_download_file: argument error\n");
exit();
} char buffer_for_part[max_download_thread+];
sprintf(buffer_for_part,"%d",part);
int part_str_length=strlen(buffer_for_part);
char *download_file_name=(char *)malloc((strlen(file_name)++part_str_length+)*sizeof(char));
if(NULL==download_file_name){ printf("Http_create_download_file: malloc failed\n");
exit(); }
strcpy(download_file_name,file_name);
strcat(download_file_name,".part");
strcat(download_file_name,buffer_for_part); if(access(download_file_name,F_OK)==){
int ret=remove(download_file_name);
if(ret!=){
printf("Http_create_download_file: remove file failed\n");
exit();
}
} *fp_download_file=fopen(download_file_name,"w+");
if(NULL==*fp_download_file){
printf("Http_create_download_file: fopen failed\n");
exit();
} if(download_file_name!=NULL){
free(download_file_name);
} return ;
}

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函数来定位文件有效数据。注意,对于同一个请求的接下来服务端发来的数据都是有效文件数据,并不包含消息头。
 int Http_recv_file(int socket_fd, long long range_begin, long long range_end, unsigned char *buffer, long buffer_size,
char *path, char *host_ip, char *port){
/**
** check argument
*/
if(range_begin< || range_end< || range_end<range_begin || NULL==buffer || buffer_size<){
printf("Http_recv_file: rename failed\n");
exit();
} char send_buffer[];
char buffer_range[];
sprintf(buffer_range, "\r\nRange: bytes=%lld-%lld",range_begin,range_end); sprintf(send_buffer,"GET %s",path);
strcat(send_buffer," HTTP/1.1\r\n");
strcat(send_buffer,"host: ");
strcat(send_buffer,host_ip);
strcat(send_buffer," : ");
strcat(send_buffer,port);
strcat(send_buffer,buffer_range);
strcat(send_buffer, "\r\nKeep-Alive: 200");
strcat(send_buffer,"\r\nConnection: Keep-Alive\r\n\r\n"); int download_size=range_end-range_begin+; int port_num=atoi(port);
int ret0=send(socket_fd,send_buffer,strlen(send_buffer),);
if(ret0!=strlen(send_buffer)){
printf("send failed, retry\n");
perror("Http_recv_file");
exit();
}
int recv_size=;
int length_of_http_head=;
while(){ long ret=recv(socket_fd,buffer+recv_size+length_of_http_head,buffer_size,);
if(ret<=){ recv_size=;
length_of_http_head=;
memset(buffer,,buffer_size); int ret=close(socket_fd);
if(ret!=){
perror("Http_recv_file");
exit();
} //seems not need to sleep Http_connect_to_server(host_ip,port_num,&socket_fd);
int ret0=send(socket_fd,send_buffer,strlen(send_buffer),);
if(ret0!=strlen(send_buffer)){
printf("send failed, retry\n");
perror("Http_recv_file");
exit();
} continue; } if(recv_size==){
char *ptr=strstr(buffer,"Content-Length");
if(ptr==NULL){
printf("Http_recv_file: recv buffer error\n");
exit();
}
int size=atoll(ptr+strlen("Content-Length")+);
if(size!=download_size){
printf("Http_recv_file: send recv not match\n");
exit();
} char *ptr2=strstr(buffer,buffer_range+);
if(NULL==ptr2){
printf("Http_recv_file: expected range do not match recv range, %s\n%s\n",buffer,buffer_range+);
exit();
} char *ptr1=strstr(buffer,"\r\n\r\n");
if(ptr1==NULL){
printf("Http_recv_file: http header not correct\n");
exit();
} length_of_http_head=ptr1-(char*)buffer+;
recv_size=recv_size+ret-length_of_http_head; }else{
recv_size+=ret;
} if(recv_size==download_size){
break;
} } return ;
}

8. 保存文件到磁盘

 int Save_download_part_of_file(FILE *fp, unsigned char *buffer, long buffer_size, long long file_offset){
/**
** check argument error
*/
if(NULL==fp || NULL==buffer || buffer_size< || file_offset<){
printf("Save_download_part_of_file: argument error\n");
exit();
} fseek(fp,file_offset,SEEK_SET);
int ret=fwrite(buffer,buffer_size,,fp);
if(ret!=){
printf("Save_download_part_of_file: fwrite failed\n");
exit();
}
return ; }

9. 下载文件主体部分,这一部分主要就是执行网址连接的解析、服务器的连接、分段下载等。代码也很简洁。

 int JHttp_download_whole_file(char *link){
/**
** check argument errorint Http_link_parse(char *link, char **url, char **port, char **path, char **file_name);
*/
if(NULL==link){
printf("JHttp_download_whole_file: argument error\n");
exit();
} char *url=NULL;
char *port=NULL;
char *path=NULL;
char *file_name=NULL; Http_link_parse(link,&url,&port,&path,&file_name); char *ip_str=NULL;
Http_get_ip_str_from_url(url,&ip_str); int socket_fd=-;
int port_int=atoi(port);
Http_connect_to_server(ip_str,port_int,&socket_fd); long long file_size=;
Http_query_file_size(path,ip_str,port,socket_fd,&file_size); FILE *fp_breakpoint=NULL;
int piece_num=file_size/(download_one_piece_size);
int size_of_last_piece=file_size%(download_one_piece_size); Http_create_breakpoint_file(file_name,&fp_breakpoint,(download_one_piece_size),file_size,,size_of_last_piece,link); FILE *fp_download_file=NULL;
Http_create_download_file(file_name,&fp_download_file,); long buffer_size=(download_one_piece_size)+;//besides file content, server will also send http header
unsigned char *buffer=(unsigned char *)malloc(sizeof(unsigned char)*buffer_size); if(NULL==buffer){
printf("JHttp_download_whole_file: malloc failed\n");
exit();
} for(int i=;i<=piece_num;i++){ memset(buffer,,buffer_size); long range_begin=(i-)*(download_one_piece_size);
long range_end=i*(download_one_piece_size)-; Http_recv_file(socket_fd,range_begin,range_end,buffer,buffer_size,path,ip_str,port); long long offset=(i-)*(download_one_piece_size); char *ptr=strstr(buffer,"\r\n\r\n");
if(NULL==ptr){
printf("JHttp_download_whole_file:recv file seems not correct\n");
exit();
} ptr+=;//pass \r\n\r\n Save_download_part_of_file(fp_download_file,ptr,(download_one_piece_size),offset); } if(size_of_last_piece>){ memset(buffer,,buffer_size); long range_begin=piece_num*(download_one_piece_size);
long range_end=range_begin+size_of_last_piece-; Http_recv_file(socket_fd,range_begin,range_end,buffer,buffer_size,path,ip_str,port); long long offset=piece_num*(download_one_piece_size); char *ptr=strstr(buffer,"\r\n\r\n");
if(NULL==ptr){
printf("JHttp_download_whole_file:recv file seems not correct\n");
exit();
} ptr+=; Save_download_part_of_file(fp_download_file,ptr,(range_end-range_begin+),offset);
} if(fclose(fp_download_file)!=){
printf("JHttp_download_whole_file:fclose file failed\n");
} char *download_file_name_part=(char *)malloc(sizeof(char)*(strlen(file_name)++));
strcpy(download_file_name_part,file_name);
strcat(download_file_name_part,".part0"); Http_restore_orignal_file_name(download_file_name_part); return ;
}

10. 在下载过程中,文件名被加上了后缀.part0,下载完毕后要做一个文件名的恢复

 int Http_restore_orignal_file_name(char *download_part_file_name){
/**
** check argument
*/
if(download_part_file_name==NULL){
printf("Http_restore_orignal_file_name: argument error\n");
exit();
}
char *new_name=(char *)malloc(sizeof(char)*(strlen(download_part_file_name)+));
if(NULL==new_name){
printf("Http_restore_orignal_file_name: malloc failed\n");
exit();
} strcpy(new_name,download_part_file_name); int file_length=strlen(new_name);
for(int i=file_length;i>;i--){
if(new_name[i]=='.'){
new_name[i]='\0';
break;
}
} int ret=rename(download_part_file_name,new_name);
if(ret!=){
printf("Http_restore_orignal_file_name: rename failed\n");
exit();
} return ; }

五:结束语

  至此关于JWebFileTrans的实现就分析完毕了,更完整的代码请读者到笔者的github上面下载,下载链接在本文的开头处。后续会增加断点续传、多线程下载、ftp下载等功能。

JWebFileTrans: 一款可以从网络上下载文件的小程序(一)的更多相关文章

  1. JWebFileTrans(JDownload): 一款可以从网络上下载文件的小程序(二)

    一  前言 本文是上一篇博客JWebFileTrans:一款可以从网络上下载文件的小程序(一)的续集.此篇博客主要在上一篇的基础上加入了断点续传的功能,用户在下载中途停止下载后,下次可以读取断点文件, ...

  2. JWebFileTrans(JDownload): 一款可以从网络上下载文件的小程序(三),多线程断点下载

    一 前言 本篇博客是<JWebFileTrans(JDownload):一款可以从网络上下载文件的小程序>系列博客的第三篇,本篇博客的内容主要是在前两篇的基础上增加多线程的功能.简言之,本 ...

  3. JDownload: 一款可以从网络上下载文件的小程序第四篇(整体架构描述)

    一 前言 时间过得真快,距离本系列博客第一篇的发布已经过去9个月了,本文是该系列的第四篇博客,将对JDownload做一个整体的描述与介绍.恩,先让笔者把记忆拉回到2017年年初,那会笔者在看Unix ...

  4. C# 中从网络上下载文件保存到本地文件

    下面是C#中常用的从Internet上下载文件保存到本地的一些方法,没有太多的技巧. 1.通过  WebClient  类下载文件 WebClient webClient = new WebClien ...

  5. 从网络上下载文件到sd卡上

    String SDPATH = Environment.getExternalStorageDirectory() + "/"; String path = SDPATH + &q ...

  6. Android开发 ---从互联网上下载文件,回调函数,图片压缩、倒转

     Android开发 ---从互联网上下载文件,回调函数,图片压缩.倒转 效果图: 描述: 当点击“下载网络图像”按钮时,系统会将图二中的照片在互联网上找到,并显示在图像框中 注意:这个例子并没有将图 ...

  7. 通过cmd命令到ftp上下载文件

    通过cmd命令到ftp上下载文件 点击"开始"菜单.然后输入"cmd"点"enter"键,出现cmd命令执行框 2 输入"ftp& ...

  8. 【转】精选十二款餐饮、快递、票务行业微信小程序源码demo推荐

    微信小程序的初衷是为了线下实体业服务的,必须有实体相结合才能显示小程序的魅力.个人认为微信小程序对于餐饮业和快递业这样业务比较单一的行业比较有市场,故整理推荐12款餐饮业和快递业微信小程序源码demo ...

  9. (SSM框架)实现小程序图片上传(配小程序源码)

    阅读本文约"2分钟" 又是一个开源小组件啦! 因为刚好做到这个小功能,所以就整理了一下,针对微信小程序的图片(文件)上传! 原业务是针对用户反馈的图片上传.(没错,本次还提供小程序 ...

随机推荐

  1. Android Service生命周期 Service里面的onStartCommand()方法详解

    在Demo上,Start一个Service之后,执行顺序:onCreate - > onStartCommand 然后关闭应用,会重新执行上面两步. 但是把代码拷贝到游戏工程发现,关闭游戏后,只 ...

  2. jquery为某div下的所有textbox的赋值

    html代码 <input type="button" value="变量div_Alltext中的变量" onclick="Do_DivAll ...

  3. 机器人局部避障的动态窗口法(dynamic window approach) (转)

    源:机器人局部避障的动态窗口法(dynamic window approach) 首先在V_m∩V_d的范围内采样速度: allowable_v = generateWindow(robotV, ro ...

  4. R语言实战(五)方差分析与功效分析

    本文对应<R语言实战>第9章:方差分析:第10章:功效分析 ================================================================ ...

  5. 在ubuntu上安装k-vim

    在ubuntu 上安装k-vim 早就想好好改造一下自己使用的vim了!可惜各种配置都十分复杂,特别是涉及到C语言的语义补全,YouCompleteMe,总是出各种安装问题.今天有人推荐我使用k-vi ...

  6. MySQL-教学系统数据库设计

    根据大学教学系统的原型,我构建出如下ER关系图,来学习搭建数据库: 上面共有五个实体,分别是学生,教师,课程,院系,行政班级: 1.其中学生和课程的关系是多对多,即一个学生可以选择多门课程,而一个课程 ...

  7. Mybatis学习(6)动态加载、一二级缓存

    一.动态加载: resultMap可以实现高级映射(使用association.collection实现一对一及一对多映射),association.collection具备延迟加载功能. 需求: 如 ...

  8. Python3基础 使用id() 查询变量的存储位置

    镇场诗: 诚听如来语,顿舍世间名与利.愿做地藏徒,广演是经阎浮提. 愿尽吾所学,成就一良心博客.愿诸后来人,重现智慧清净体.-------------------------------------- ...

  9. html 转义

    function escapeHTML(n) { var t = document.createElement("div"), i = document.createTextNod ...

  10. CSS文字大小单位PX、EM、PT

    老是被人问到px.pt和em的区别,自己有时候也会纠结到底该用什么单位,今天特意查了一些文章,下面这篇虽然很久远了,但解释的比较全面,转载收藏之.点击查看原文 这里引用的是Jorux的"95 ...