基于linux系统的文件传输系统
描述:之前做的一个课程结课设计,主要就是实现Linux系统下客户端和服务器端的文件传输,现在整理出来,希望和大家一起进步!
完整资源:https://download.csdn.net/download/shwliyi/20667236
具体要求:
服务器端,有以下功能 :
有客户端链接时,创建一个进程处理客户端的链接,该子进程完成如下任务:
(1)从客户端传入的用户名和密码到 MySQL 数据库比对客户端登录用户的合法性;
(2)用户名密码不匹配的用户给出提示,并关闭链接;
(3)对于合法用户,服务器列出指定目录下的文件列表;
(例如/home/shwliyi/serverfile/),供用户选择(提供序号);
(4)用户做出选择后(序号),服务器通过 socket 把文件传输到客户端,完成文件下载功能;
(5)记录用户名,下载文件名,下载时间到一个日志文本文件;
(6)该子进程退出 。
客户端,有以下功能 :
(1)客户端链接指定 IP 和端口的服务器,输入用户名和密码发送到服务器进行登录;
(2)接收服务器从返回的可供下载的文件列表;
(3)输入需要下载的文件序号,把序号发送给服务器;
(4)下载文件到指定目录;(例如:/home/shwliyi/download/) ;
(5)退出 。
功能分析:
服务器端:
(1)与客户端建立连接,建立连接后,能接受客户端发送过来的用户名和密码并能与数据库中的数据进行比较;
(2)将比较结果发送回客户端;
(3)接受客户端文件列表申请,获取当前目录下的文件列表,依次发送至客户端;
(4)服务器端接收客户端的命令,并根据命令要求发送对应文件给客户端。
客户端:
(1)与服务器端建立连接,用户输入用户名和密码,发送至服务器端;
(2)接受服务器端发送回的匹配结果,若匹配成功则申请文件列表;
(3)接收服务器端发送过来的文件列表,给发送过来的文件编号;
(4)客户端获取用户输入的命令并将命令发送至服务器端;
(5)客户端接收服务器端发送过来的文件,存入到当前目录中。
实现原理:
为了实现上述功能,程序中我主要使用套接字通信,先谈一下套接字相关的基础知识吧。
socket通信原理和socket接口函数:
(1)socket通信原理:
套接字(socket) 是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。Linux所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(如用于远程登录的rlogin和用于文件传输的ftp)通常都是通过套接字来进行通信的。
套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分开来。套接字机制可以实现将多个客户连接到一个服务器。
套接字的特性由3个属性确定,它们分别是:域、类型和协议。
套接字的域指定套接字通信中使用的网络介质,最常见的套接字域是AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。另一个域AF_UNIX表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。
因特网提供了两种通信机制:流(stream)和数据报(datagram),因而套接字的类型也就分为流套接字和数据报套接字。我所采用的是流套接字。
流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。
对于socket通信原理,我们需要了解TCP/IP和UDP,TCP/IP即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网设计的。关于UDP,UDP是用户数据报协议,是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
socket与TCP/IP协议的位置关系如下图:
socket是应用层与TCP/IP协议族通信的中间软件抽象层,作为一组接口使用。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。
关于socket的工作方式,先从服务器端说起,服务器端先初始化socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个socket,然后连接服务器(connect)如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接受请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,然后关闭连接,一次交互结束。
socket工作原理如下图:
(2)socket接口函数:
1)socket()函数:
格式:int socket(int domain, int type, int protocol);
socket()函数用于根据指定的地址族,数据类型和协议来分配一个套接字的描述字及其所用的资源,其三个参数分别为:
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。AF_ INET套接字可以用于通过包括因特网在内的TCP/IP网络进行通信的程序。
type: 指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。程序采用的是SOCK_ STREAM,SOCK_ STREAM是一个有序、可靠、面向连接的双向字节流。对AF_ INET域套接字来说,它默认是通过一个TCP连接来提供这一特性的,TCP连接在两个流套接字端点之间建立。数据可以通过套接字连接进行双向传递。TCP协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据。
protocol:即指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。程序中我将该参数值设置为0表示使用默认协议。
2)bind()函数:
格式:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
其三个参数分别为:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
3)listen()函数:监听指定的socket()地址
格式:int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。listen函数提供的这种机制允许当服务器程序正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。
4)connect()函数:客户端发出连接请求
格式:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
5)accept()函数:接收请求
格式:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为:监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常仅仅只创建一个监听socket描述字,它在服务器的生命周期内一直存在。内核为每个服务器进程接收的客户连接创建一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的连接socket描述字就被关闭。
6)send()函数:
格式:int send(int sockfd, const void *buf, size_t len, int flags);
不论客户端还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户端一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
第一个参数是指定发送端套接字描述字,第二个参数是指明一个存放应用程序要发送数据的缓冲区,第三个参数指明要发送的数据的字节数,第四个参数一般置为0。
send函数把buf中的数据成功复制到sockfd的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。(每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR);
Send函数的返回值有三类:
返回值=0:
返回值<0:发送失败,错误原因存于全局变量errno中
返回值>0:表示发送的字节数(实际上是拷贝到发送缓冲中的字节数)
7)recv()函数:
格式:int recv(int sockfd, void *buf, size_t len, int flags);
不论是客户端还是服务器端应用程序都用recv函数从TCP连接的另一端接收数据。
第一个参数指定接收端套接字描述字,第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据,第三个参数指明buf的长度,第四个参数一般设置为0。
recv函数的返回值类型:
成功执行时,返回接收到的字节数;
若另一端已关闭连接则返回0,这种关闭是对方主动且正常的关闭;
失败返回-1。
8)close()函数:
格式:int close(int fd);
关闭套接字,并释放分配给该套接字的资源;如果涉及一个打开的TCP连接,则该连接被释放。
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
下面是我自己写的一些函数:
(1)服务器端判断用户名和密码是否正确的函数:
Socket连接建立之后,接收客户端发送过来的用户名和密码,然后建立与本地数据库的连接,将用户名和密码写入sql语句当中,执行查询操作,根据查询结果判断,若查询结果中存在数据,则说明用户名和密码正确,若查询结果没有数据,则说明用户名和密码不正确,最后返回正确或不正确的信号量。
int check(){
MYSQL *conn;
MYSQL_RES *res_ptr;//指向查询结果的指针
int rows,filds,i,j;
int res=1;
int sign1 =0;
char buf2[SIZE];
char sign[SIZE]={0};
char sql[100]={0};
recv( client_sockfd,username, SIZE, 0);//接受用户名和密码
//输出用户名和密码,判断获取的信息对不对,调试正常后可以注释掉
printf("name is : %s\n",username);
recv( client_sockfd, buf2, SIZE, 0);
printf("password is : %s\n",buf2);
conn = mysql_init(NULL);
if (!conn)
{
printf("mysql_init failed\n");
return EXIT_FAILURE;
}
//建立数据库链接 参数含义: 本地连接 数据库账号 密码 数据库名 端口号
conn =
mysql_real_connect(conn,"localhost","root","991619","yh_db",3306,NULL,0);
if (conn)
{
printf("Connection success!\n");
sprintf(sql,"select * from yh_tb where name='%s' and password = '%s'",username,buf2);
//printf("sql is:%s\n",sql);
//mysql_set_character_set(conn,"utf8");
res = mysql_query(conn,sql);
if(res==0)
{
//把查询结果给res_ptr
res_ptr=mysql_store_result(conn);
rows=mysql_num_rows(res_ptr); //行
//printf("%d\n", rows);
//将账号和密码放进数据库进行查询,有结果的话,结果行数为1,表示账号和密码正确
if (rows == 1)
{
printf("用户存在!\n");
sign1 =1;
sign[0] = 'Y';
}
else{
printf("账号或密码错误!!\n");
sign1 = 0;
close(server_sockfd);
}
}
else{
printf("账号或密码错误!!\n");
sign1 = 0;
mysql_close(conn);
close(server_sockfd);
return EXIT_SUCCESS;
}
}
else{
printf("Connection failed!\n");
return EXIT_FAILURE;
}
//printf("sign is :%s\n",sign);
send(client_sockfd, sign, SIZE, 0);
//printf("check sign is :%d\n",sign1 );
return sign1;
}
(2)客户端获取文件列表的函数:
首先建立socket,连接服务器,当接收到登陆成功的信号量后向缓冲区里写入L字符,将该字符发送至服务器端,服务器端判断该去执行哪个函数。在连接成功后,开始接受服务器端传送回来的文件列表,由于题目要求用户做选择的是文件的序号,所以在此函数中传入filename[][50]参数,即一个二维字符型数组,用来保存序号和对应的文件名,然后通过一个循环,将缓冲区中的文件名依次存储到数组里,返回遍历数组,将序号和文件名输出供用户选择。
//获取文件列表
void getfilelist(struct sockaddr_in server_addr,char filename[][50])
{
char buf[SIZE];
int i=1;
int n;
if(( sockfd = socket( AF_INET, SOCK_STREAM, 0)) < 0){
printf("fail to list\n");
return;
}
//连接服务器
if( connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(server_addr)) < 0){
printf("fail to connect server\n");
close(sockfd);
}
//发生字符L,告诉服务器我要获取文件列表
strcpy(buf,"L");
send(sockfd, buf, SIZE, 0);
//接收文件列表,并给其加上序号
while(( n = recv( sockfd, buf,SIZE,0)) != 0){
printf("%d:%s\n",i,buf);
strcpy(filename[i],buf);
i++;
}
}
(3)服务器端传送文件列表的函数:
当服务器端接收到客户端的命令为L时,开始准备当前目录下的文件名,首先利用opendir()函数打开当前目录,然后结合readdir()函数遍历目录中的内容,并依次拷贝到缓冲区中,利用send函数逐个发送至客户端,直至发送完毕。
//获取文件列表
void getfilelist(int client_sockfd)
{
DIR *mydir;
struct dirent *myitem;
char buf[SIZE];
mydir = opendir(".");//打开一个目录
while((myitem = readdir(mydir)) != NULL){
if((strcmp(myitem->d_name, ".") == 0) || (strcmp(myitem->d_name, "..") == 0))
continue;
strcpy(buf, myitem->d_name);
send(client_sockfd,buf, SIZE, 0);
}
close(client_sockfd);
return;
}
(4)客户端下载文件函数:
用户输入要下载的文件序号后,将此序号和filename[][50]作为参数传送进该函数中,然后通过遍历二维数组找到对应的文件名,将文件名取出后在其前面加入F字符后发送到服务器端,然后用recv函数接收服务器端发送过来的文件数据,下载到当前目录下,关闭连接。
//获取文件
void getfile(struct sockaddr_in server_addr,char sign1[],char filename[][50]){
int i,n,fd;
char buf[SIZE];
//printf("getfile\n");
//printf("sign1 is %s\n",sign1 );
if(( sockfd = socket( AF_INET, SOCK_STREAM, 0)) < 0){
fprintf(stderr, "fail to socket: %s\n", strerror(errno));
exit(-1);
}
if( connect(sockfd, (struct sockaddr *)&(server_addr),sizeof(server_addr)) < 0){
printf("fail to connect server\n");
close(sockfd);
}
for (int i = 1; i < 50; ++i)
{
if (i == atoi(sign1))
{
//printf("i is %d\n",i );
sprintf(buf,"F%s",filename[i]);
//printf("%s\n",filename[i]);
if(( fd = open(filename[i], O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0){
printf("fail to create local file %s\n",filename[i]);
}
break;
}
}
send(sockfd, buf, SIZE, 0);
recv(sockfd, buf, SIZE, 0);
//printf("%s\n",buf );
if(buf[0] == 'N'){
printf("服务器中没有这个文件,请重新选择!\n");
}
while(( n = recv(sockfd, buf, SIZE, 0)) > 0){
write(fd, buf, n);
}
close(fd);
printf("文件下载成功!\n");
}
(5)服务器端发送文件函数:
服务器端接收到F字符开头的命令后进入该函数,该函数获取到传进来的文件名,之后开始打开并读取相关文件名对应的文件数据,利用循环和send函数向客户端发送文件数据,发送结束后将文件名、用户名以及下载时间写入日志。
//发送文件
void putfile(int client_sockfd,char buf[]){
int fd,n;
char filename[50];
strcpy(filename,buf+1);//获得F后面的文件名
if(( fd = open(buf+1, O_RDONLY)) < 0){
fprintf(stderr, "fail to open %s: %s\n",buf+1,strerror(errno));
buf[0] = 'N';
send(client_sockfd, buf, SIZE, 0);
}
buf[0] = 'Y';
send(client_sockfd, buf, SIZE, 0);
while(( n = read( fd, buf, SIZE)) > 0){
send(client_sockfd, buf, n, 0);
}
close(fd);
printf("文件发送完成!\n");
writelog(filename,username);
}
(6)日志记录函数:
服务器端在发送完文件后将文件名和用户名传入该函数,此时函数先调用time函数和localtime函数获取当前时间并解析,然后打开日志文件,将文件名、用户名和下载时间写入日志文件。
//日志记录
//百度上一搜一大把,可以自己去搜了借鉴一下
void writelog(char filename[],char username[]){
time_t t = 0;
struct tm* tm = NULL;
char file_name[SIZE];
char user_name[SIZE];
strcpy(user_name, username);
strcpy(file_name, filename);
printf("%s\n", username);
printf("%s\n", filename);
//写入日志
FILE* fp = fopen("log.txt", "a+");
if (NULL == fp)
{
printf("写入日志失败!\n");
}
t = time(NULL);
if (t == -1)
{
printf("日志获取时间失败!\n");
}
tm = localtime(&t);
if (tm == NULL)
{
printf("日志格式化时间失败!\n");
}
//输出标准输出所获取的日期和时间
//printf("%d年 %02d月 %02d日 %02d点 %02d分 %02d秒\n",tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec);
fprintf(fp, "用户名:%s 文件:%s 时间:%d年 %02d月 %02d日 %02d点 %02d分 %02d秒\n", user_name, file_name,
tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday,tm->tm_hour, tm->tm_min, tm->tm_sec);//写入日志
fclose(fp);//关闭日志文件
}
程序流程:
(1)服务器端:
(2)客户端:
数据库设计:
因为需要进行对客户端发送过来的用户名和密码进行验证,所以需要先创建数据库,在数据库中存储用户名和密码:
首先创建数据库yh_db:
创建用户表 yh_tb:
初始化表格数据:
导入数据:
注:数据库名和表名按照你的想法来取就行,初始的数据改一下就可以,这里改了程序中涉及到的部分也需要做出更改!
程序运行结果:
服务器端运行效果:
客户端运行效果:
下载完后查看客户端保存下载文件的目录:
查看下载日志:
总结一下:
其实在套接字通信中,首先就是建立链接,链接建立之后,后面基本就是靠两个关键操作:send函数和recv函数,发送和接收,然后就是在每个操作之前都判断一下链接是否还在,有没有断开什么的,主要就是这些了。如果看到这里还是不懂的话,这里有完整代码,拿去吧你!!!