模拟网盘(基于多线程TCP)
题目描述:
模拟一个网盘实现以下功能:
用户可以注册、登录、上传和下载文件
用户可以实现类似 ls的功能查询文件信息
多用户间文件可以共享
要求:
基于TCP协议
server端要用多线程实现
在linux下用C语言实现
需要实现的功能:
服务器端:
多线程的实现(注意多用户间的同步)
对于一个刚连接的用户,发送要求输入验证信息
录入用户的注册信息到文件
验证登录信息
处理用户上传的文件
处理用户查询自己现有文件的请求(ls)
对用户的下载请求做出响应
用户可以设置文件权限对他人共享
客户端:
上传
下载
操作服务器传来的菜单即可
程序逻辑:
客户端
流程图
准备阶段
客户端依次进行建立socket、connect
成功连接(connect)后,进入登陆阶段
程序发出提示信息(菜单一)0:退出 1:登陆 2:注册
然后读取请求(标志位),并发送给服务器(while(1))
- 读到0:直接发送0给服务器然后退出
- 读到1:发送1给服务器,然后录入用户名,并发送给服务器,等待服务器的反馈,服务器发回1表示登录成功否则是0表示登录失败(可能是不存在这个用户),登录成功程序需要创建一个本地文件夹(如果没有的话)来保存下载到本地的文件以及要上传的文件(调用函数
C_mkdir(username)
实现) - 读到2:发送2给服务器,然后录入用户名,并发送给服务器,等待服务器的反馈,服务器发回1表示注册成功然后返回再登录即可,否则是0表示注册失败(可能是已经有重名用户)
若登录成功,进入交互阶段
程序发出提示信息(菜单二)0:退出 3:查文件 4:文件共享 5:上传 6:下载
然后读取请求(标志位),并发送给服务器(while(1))
- 若是3:收取服务器发来的消息即可(注意菜单结束的标志是一个#)
- 若是4:提示发送要共享的文件名,发送文件名给服务器后,服务器会反馈一条消息,发回‘0’表示服务器端不存在此文件无法共享,发回1表示可以共享,然后在录入一个目标用户名发给服务器即可
- 若是5:录入并发送待发送文件的文件名。服务器会判断文件是否存在并给出回应,收到反馈0表示已经存在同名文件,1表示服务器无此文件,然后不管哪种情况都开始上传文件(若存在则覆盖)
send_file(sock,username,filename);
- 若是6:录入并发送待下载的文件的文件名。服务器会判断文件是否存在并给出回应,收到反馈0表示服务器无此文件无法下载,1表示服务器存在文件,然后开始下载服务器传来的文件。
recv_file(sock, username, filename);
服务器端
准备阶段
- 服务器依次建socket、bind、listen后进入while(1)循环,在循环内部accept每接收到一个新的accept就调用pthread_creat创建一个新的线程来做处理。然后主进程依旧做accept。
成功连接后,进入登陆阶段
录入请求(while(1))(对应客户端的菜单1)
- 若是0:
断开连接 - 若是1:
收取用户名
调用seek_someone(username)
查询用户信息表,看看是否存在这个用户
发送反馈(存在则登陆成功发1然后break、失败发0然后continue) - 若是2:
录入用户名
调用seek_someone(username)
判断是否已经存在同名用户
存在同名的话就发送反馈0表示注册失败,然后continue
不存在同名的话就新建用户信息文件夹mkdir(abs_name,0777)
(表示注册成功)然后发送反馈1表示成功,然后continue
登录成功后,进入交互阶段
服务器循环录入标志位(对应客户端的菜单2)
- 若是3:
调用发送文件信息表的函数file_list
发送用户创建的文件和其他用户共享给他的文件两类信息给客户端 - 若是4:
录入客户待共享的文件名,查找是否存在此文件seek_file函数
,不存在则发送反馈0。存在则发送反馈1,然后接着收取待分享的目标用户,然后利用系统的cp
命令将待共享的文件从原用户的文件夹中复制到目标用户的share
文件夹中。 - 若是5:
接受文件名,然后利用seek_file
判断文件是否存在,发送反馈信息给客户端后调用recv_file(sock,username,filename);
来接收文件 - 若是6:
接受文件名,然后利用seek_file
判断文件是否存在,如果不存在发送反馈0给客户端,如果存在发送反馈1给客户端然后调用send_file(sock,username,filename);
发送文件给客户
具体算法:
关于程序如何保存用户和文件信息
服务器所在的机器有一个网盘的主目录:./Server_file_folder
,然后在此主目录下有子目录,每一个子目录对应一个用户,子目录名就是对应的用户名,判断网盘是否有此用户就是判断该目录下是否有以该用户名命名的文件夹,然后对于用户上传的文件,程序都保存在用户对应的文件夹内,对任何一个用户,其对应的文件夹内都有一个子文件夹share
,该文件夹用来保存其他用户分享给自己的文件。
客户端同样的要有一个本地的网盘主目录./local_file
,在该文件夹下是每个本地用户的目录,客户要上传的文件和从服务器下载的文件都保存在该文件夹下。
关于共享文件的实现方法:
本程序将每个用户的文件存在独立的文件夹内, 共享文件时,直接将共享给A的文件复制进入A的share
文件夹即可。这里的实现方法是利用system()函数
调用系统命令cp
,调用格式是cp source_file destination_file,只需要先创建一个字符串然后依次将"cp",“空格”,“源文件”,“空格”,"目标文件"这几个字符串利用strcat()函数
依次连接在一起然后作为参数传给system()
即可。
收发文件结束的标志:
每次发送 BUFFEISIZE+1 个数据
其中第一个位置是标志位
标志位 0 表示后面没有文件了,接收完这个就可以结束了
标志位 1 表示后面还有文件
发送文件列表函数的实现
- 函数原型:
void file_list(int sock, char *username)
参数是需要发送列表的用户的socket和他的用户名
要发送文件列表就要遍历网盘内的用户文件夹,然后发送所有文件名给客户端即可。
遍历输出某个文件夹下的所有文件(包括子文件夹的方法已经在输出linux下给定目录的所有子目录这篇文章给出)
这里再简单提一下:
我们用到
opendir()
打开目录
closedir()
关闭目录
readdir()
读取目录
利用readdir()
返回的句柄entry提取文件中我们需要的信息。
entry->d_name
是我们需要的文件名,entry->d_type
是文件的类型,需要注意的是entry->d_type== 4
表示该文件是一个目录文件,不需要输出。
收发文件函数的实现
- 函数原型:
void send_file(int sock, char *username, char *file_name)
void recv_file(int sock, char *username, char *file_name)
- 参数分别是:收/发对象的socket,用户名,待操作文件的文件名。
发文件流程:
- 首先需要打开文件,对于服务器端,其需要在用户主目录和用户的share两个目录下检索并打开文件,而对于客户端,其只需要再用户主目录打开文件。
- 然后定义一个缓冲区
char send_buf[C_BUFFER_SIZE+1];
大小是C_BUFFER_SIZE+1
其中第一个字节是一个标志位(取值为0/1)剩下的C_BUFFER_SIZE个字节是实际用来存文件的位置。 - 然后需要利用
fread
读取文件,这里我们每次从文件中读取C_BUFFER_SIZE个字节,从缓冲区的第二个字节的位置开始存在缓冲区内。
int fread_len =fread(&send_buf[1], sizeof(char), C_BUFFER_SIZE, fp);
//这里的含义:从fp指向的文件中,读取 C_BUFFER_SIZE个单位的字节
//到以&send_buf[1]为头的区域内,其中每个单位的字节大小为sizeof(char)
//返回值赋给 fread_len,其表示实际从文件内读取到的长度
- 然后就需要根据实际读取的情况判断:如果 fread_len < C_BUFFER_SIZE说明已经读完了整个文件,当前读取的文件中剩下的内容填不满整个缓冲区,这种情况下表示此条消息应该是最后一条,所以将标志位改为0:
send_buf[0] = '0';
否则将标志位改为1send_buf[0] = '1';
表面当前信息后面还有其他消息。
然后将带有标志位的缓冲区消息通过socket发出去即可:send(sock, send_buf, fread_len + 1, 0);
程序实际执行过程中没发出一条消息会在屏幕上显示一句正在进行第x次上传,用于提示发送进度。
收文件类似于发文件的逆过程:
- 首先从socket内读取大小为
C_BUFFER_SIZE+1
的消息到缓冲区
recv(sock, recv_buf, C_BUFFER_SIZE+1, 0);
- 然后从缓冲区的第二位开始将缓冲区内容写入文件
fwrite(&recv_buf[1], sizeof(char), recv_len, fp);
- 然后判断标志位如果是0则终止传输否则继续。
if (recv_buf[0] == '0')
{ //发送完毕
fclose(fp);
break;
}
查找是否存在某个用户或者文件的函数的实现
函数原型
int seek_someone(char *username)
int seek_file(char *username,char *filename)
参数:uesename:待查找的用户名
filename:待查找的文件名
返回值:0表示无此文件/用户
1表示有此文件/用户
这两个函数的实现都比较简单
- seek_someone:查找网盘是否有某个用户等价于查找网盘主目录下是否有以该用户名命名的文件夹。
利用opendir
函数打开用户对应的主目录,若返回NULL 表示无此文件夹,即不存在此用户return 0,否则存在,关闭文件夹return 1 即可。 - seek_file:利用
fopen()
打开用户主目录下的文件,注意打开模式为‘r’
,若返回NULL 表示无此文件,这里需要再次判断share文件夹内是否含有该文件,若仍然返回NULL,那么该用户没有此文件return 0,否则存在文件,则关闭文件并return 1;
需要注意的地方
注意pthread_create时,向处理函数传递参数的方法:
不能直接在第三个参数即函数名 的后面直接写参数,
虽然能过编译,但是参数实际没有传进去
错误的写法:pthread_create(&r_thread, NULL, recv_pthread(new_sock), NULL);
因为第三个函数是个函数指针,传的只是地址
应该利用pthread_create的第四个参数
把处理函数的参数改为void *类型,然后把要传的参数强制类型转换为void 传进去
在处理函数内部再把接收到的参数强制类型转换为需要的类型
(注意类型宽度需要<= sizeof(void) 否则会出错)
正确写法:
pthread_create(&r_thread, NULL, recv_pthread, (void *)&new_sock);
两台机器进行socket通信时,可能在连接时出现错误:
connect error: No route to host(errno:113)
出错原因:server端的防火墙设置了过滤规则
解决办法:使用iptables关闭server端的防火墙
1.暂时打开和关闭
$sudo service iptables stop
$sudo service iptables start
3.永久打开和关闭
$sudo chkconfig iptables on
$sudo chkconfig iptables off