参考:FTP云盘
作者:行稳方能走远
链接:https://blog.youkuaiyun.com/zhuguanlin121/article/details/119838658?
FTP服务器
FTP是用来在两台计算机之间传输文件,是Internet中应用非常广泛的服务之一。它可根据实际需要设置各用户的使用权限,同时还具有跨平台的特性,即在UNIX、Linux和Windows等操作系统中都可实现FTP客户端和服务器,相互之间可跨平台进行文件的传输。因此,FTP服务是网络中经常采用的资源共享方式之一。FTP协议有PORT和PASV两种工作模式,即主动模式和被动模式。
FTP(File Transfer Protocol)即文件传输协议,是一种基于TCP的协议,采用客户/服务器模式。通过FTP协议,用户可以在FTP服务器中进行文件的上传或下载等操作。
项目介绍
作为学习Linux编程的首个小项目,这是在学习完文件、进程、进程间通信、线程、网络编程API后的总结。当然道阻且长,希望自己能转行成功。
这个项目分成FTP的服务端和客户端,模仿Linux开源FTP服务器,只实现部分功能;用户可以根据网络,远程获取服务器上的文件,实现对文件的基本操作。如上传文件、下载文件等。
FTP服务器基于TCP协议,使用基本TCP套接字编程(socket、connect、bind、listen、accept、fork和exec、close函数等)。服务器会一直判断客户端是否连接成功,客户端连接成功时,创建子进程对接连接,接收来自客户端的指令,比如收到get demo.c的指令,表明客户端想要获取demo.c文件,通过预先定义的宏,再通过strtok函数进行字符串分割,获取到文件名,然后判断文件在服务端是否存在,如果文件存在,就读取文件內容,再将內容通过套接字发给客户端,客户端收到数据后,同样通过strtok函数,然后创建文件,并将收到的数据写入文件,即完成文件的远程下载。(说明网络编程,字符串编程,文件编程的功底)
上传文件和下载文件类似,主要还是涉及文件的操作,字符串的操作,以及网络编程。
还支持Is,pwd,cd等Linux系统常用的指令。ls,pwd指令的实现通过popen函数,popen函数会fork一个子进程来调用/bin/sh -c 来执行参数command 的指令(/bin/sh会创建一个子shell),然后读取执行的结果,并发送给客户端。如果不需要获取执行结果,可通过system函数调用实现。(说明popen,system的编程)
功能说明
服务器由服务端和客户端组成,可浏览客户端本地文件及远程服务端的文件,支持对远程服务端文件的删除,存储,归档操作处理,以及客户端对远程服务端文件的上传和下载。
云盘实现的部分功能:
- ls———查看服务端文件
- lls———查看客户端自己的文件
- cd———切换服务端目录
- lcd———切换客户端自己的目录
put———上传文件
get———下载文件
pwd———显示路径
quit———退出
API补充
1、popen
函数原型:
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函数说明:
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c 来执行参数command 的指令。/bin/sh 会创建一个子shell
sh是bash的一种特殊的模式,也就是 /bin/sh 相当于 /bin/bash --posix。说白了sh就是开启了POSIX标准的bash 。
参数type 可使用 "r"代表读取,"w"代表写入。依照此type 值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中
2、access
函数原型:
#include <unistd.h>
int access(const char *pathname, int mode);
参数介绍:
pathname 是文件的路径名+文件名
mode:指定access的作用,取值如下:
F_OK 值为0,判断文件是否存在
X_OK 值为1,判断对文件是可执行权限
W_OK 值为2,判断对文件是否有写权限
R_OK 值为4,判断对文件是否有读权限
注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK
返回值:成功0,失败-1
3、chdir
函数原型:
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
函数说明:chdir()用户将当前的工作目录改变成以参数路径所指的目录。
返回值: 执行成功则返回0,失败返回-1,errno为错误代码。
代码实现
command.h
指令的宏定义,及对指令处理的结构体定义
#define LS 0
#define GET 1
#define PWD 2
#define IFGO 3
#define LCD 4
#define LLS 5
#define CD 6
#define PUT 7
#define QUIT 8
#define DOFILE 9
typedef struct msg{
int type;//类型
char cmd[1024];//存放指令
char dataBuf[128];//存放上传文件内容
}Msg;
服务端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <string.h>
#include "command.h"
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
//处理客户端发送的指令转换为对应宏定义的值返回
int get_cmd_types(char *pcmd)
{
if(!strcmp("ls",pcmd)) return LS;
if(!strcmp("quit",pcmd)) return QUIT;
if(!strcmp("pwd",pcmd)) return PWD;
if(strstr(pcmd,"cd") != NULL) return CD;
if(strstr(pcmd,"get") != NULL) return GET; //6
if(strstr(pcmd,"put") != NULL) return PUT;
}
//分割客户端指令,以便上传或下载时创建文件
char *getDesDir(char *cmsg)
{
char *p;
p = strtok(cmsg," ");
p = strtok(NULL," ");
return p;
}
//通过客户端发送到结构体中的内容进行操作,用到对应已连接客户端的套接字描述符
void msg_handler(Msg msg,int fd)
{
char dataBuf[128];
char *file = NULL;
int fd_file;
int cmd_ret;
printf("client cmd:%s\n",msg.cmd);
//处理客户端指令
cmd_ret = get_cmd_types(msg.cmd);
//根据不同的指令,服务端进行不同操作
switch(cmd_ret){
case LS:
case PWD:{
msg.type = 0;
FILE *r = popen(msg.cmd,"r");//通过popen创建一个子进程,执行客户端发送过来的指令
fread(msg.cmd,sizeof(msg.cmd),1,r);//从r指定的流中读取数据到msg.cmd中
write(fd,&msg,sizeof(msg));//将服务端执行指令后的内容发送给客户端
break;
}
case CD:{
//分割指令,得到文件名,通过chdir进入对应文件的路径
char *file_name = getDesDir(msg.cmd);
printf("file_name:%s\n",file_name);
chdir(file_name);
break;
}
case GET:
file = getDesDir(msg.cmd);
if(access(file,F_OK) == -1){//判断文件在服务端是否存在
strcpy(msg.cmd,"NO This File");
write(fd,&msg,sizeof(msg));
}else{
msg.type = DOFILE;
fd_file = open(file,O_RDWR);
read(fd_file,dataBuf,sizeof(dataBuf));//将指令中对应文件名的文件中的内容读取到缓存中
close(fd_file);
strcpy(msg.cmd,dataBuf);//将缓存中的内容拷贝到msg中
write(fd,&msg,sizeof(msg));
}
break;
case PUT:
//文件存在打开,不存在创建
fd_file = open(getDesDir(msg.cmd),O_RDWR|O_CREAT,0666);
write(fd_file,msg.dataBuf,strlen(msg.dataBuf));//将客户端put操作的内容写到服务端创建的相同文件名的文件中
close(fd_file);
break;
case QUIT:
printf("client quit!\n");
exit(-1);
break;
}
}
int main(int argc, char **argv)
{
int s_fd;
int c_fd;
int n_read;
Msg msg;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
memset(&s_addr,'\0',sizeof(struct sockaddr_in));
memset(&c_addr,'\0',sizeof(struct sockaddr_in));
if(argc != 3){
printf("param error!\n");
exit(-1);
}
//1.socket 创建套接字
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
perror("socket:");
exit(-1);
}
//2.bind 绑定IP及端口号
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&s_addr.sin_addr);
bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
//3.listen 监听;第二个参数为内核应该为相应套接字排队的最大连接个数
//补充:当socket函数创建一个套接字时,它被假设为一个主动套接字,也是一个将调用connect发起连接的客户套接字。
listen(s_fd,10);
//4.accept
int clen = sizeof(struct sockaddr_in);
while(1){
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept:");
exit(-1);
}
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));//printf IP
//有客户端连接,创建子进程,处理客户端的指令及接收数据
if(fork() == 0){
//5.read
while(1){
memset(msg.cmd,'\0',sizeof(msg.cmd));
n_read = read(c_fd,&msg,sizeof(msg));
if(n_read == 0){
printf("client out\n");
exit(-1);
}else if(n_read > 0){
msg_handler(msg,c_fd);
}
}
}
close(c_fd);//新的客户由子进程提供服务,父进程就关闭已连接的套接字
}
close(s_fd);
return 0;
}
客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include "command.h"
#include <sys/stat.h>
#include <fcntl.h>
//处理客户端发送的指令转换为对应宏定义的值返回
int get_cmd_type(char *pcmd)
{
if(!strcmp("ls",pcmd)) return LS;
if(!strcmp("pwd",pcmd)) return PWD;
if(!strcmp("lls",pcmd)) return LLS;
if(!strcmp("quit",pcmd)) return QUIT;
if(strstr(pcmd,"lcd") != NULL) return LCD;
if(strstr(pcmd,"cd") != NULL) return CD;
if(strstr(pcmd,"get") != NULL) return GET;//6
if(strstr(pcmd,"put") != NULL) return PUT;
return -1;
}
//分割客户端指令,以便PUT或LCD操作时创建文件
char *getDir(char *pcmd)
{
char *p;
p = strtok(pcmd," ");
p = strtok(NULL," ");
return p;
}
//处理客户端获取的指令,并将指令内容发送给服务端
int cmd_handler(Msg msg,int fd)
{
int cmd_ret;
int file_fd;
char buf[32];
char *file_name = NULL;
Msg msgBuf;
int n_read;
int g_fd;
//printf("cmd:%s\n",msg.cmd);
cmd_ret = get_cmd_type(msg.cmd);
switch(cmd_ret){
case LS:
case PWD:
case CD:
msg.type = 0;
write(fd,&msg,sizeof(msg));
break;
case GET:
msg.type = 2;
write(fd,&msg,sizeof(msg));
//close(fd);
break;
case PUT:
strcpy(buf,msg.cmd);
file_name = getDir(buf);
if(access(file_name,F_OK) == -1){
printf("%s not exist\n",file_name);
}else{
file_fd = open(file_name,O_RDWR);
read(file_name,msg.dataBuf,sizeof(msg.dataBuf));
close(file_fd);
write(fd,&msg,sizeof(msg));
}
break;
case LLS:
system("ls");
break;
case LCD:
file_name = getDir(msg.cmd);
chdir(file_name);
break;
case QUIT:
strcpy(msg.cmd,"quit");
write(fd,&msg,sizeof(msg));
close(fd);
exit(-1);
}
return cmd_ret;
}
//当ret的值不大于3时,即执行ls,get,pwd指令时,打印服务端发送过来的内容
void handler_server_message(int c_fd, Msg msg)
{
int n_read;
int fd_newfile;
Msg mesget;
n_read = read(c_fd,&mesget,sizeof(mesget));
if(n_read == 0){
printf("server message not;quit!\n");
exit(-1);
}
else if(mesget.type == DOFILE){ //获取服务端的文件内容,并创建相应文件,将数据写入到文件中
char *p = getDir(msg.cmd);
fd_newfile = open(p,O_RDWR|O_CREAT,0600);
write(fd_newfile,mesget.cmd,strlen(mesget.cmd));
putchar('>');
fflush(stdout);
}
else{ //打印ls,pwd指令获取的内容
printf("-----------------------------------\n");
printf("\n%s\n",mesget.cmd);
printf("-----------------------------------\n");
putchar('>');
fflush(stdout);
}
}
int main(int argc, char **argv)
{
int c_fd;
Msg msg;
struct sockaddr_in c_addr;
if(argc != 3){
printf("param error!\n");
exit(-1);
}
//1.socket
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
perror("c_socket:");
exit(-1);
}
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));//通过htons函数将端口号转换成可识别的网络字节序
inet_aton(argv[1],&c_addr.sin_addr);//将字符串转换成一个32位的网络字节序二进制值
//2.connect 客户端连接主机
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr))){
perror("connect:");
exit(-1);
}
printf("connect success...\n");
while(1){
memset(msg.cmd,'\0',sizeof(msg.cmd));
gets(msg.cmd);//获取用户键盘输入
int ret = cmd_handler(msg,c_fd);
if(ret > IFGO){
printf(">");
fflush(stdout);//刷新输出缓冲区;stdout指向一个与标准输出流(standard output stream)相关连的 FILE 对象。
continue;
}
if(ret == -1){
printf("Not This Command\n");
putchar('>');
fflush(stdout);
continue;
}
handler_server_message(c_fd,msg);
}
close(c_fd);
return 0;
}
执行结果
并发服务器
它是在同时有大量的客户连接到同一服务器上时用于提供并发性的一种常用Unix技术。每个客户连接都迫使服务器为它派生(fork)一个新的进程。Unix中编写并发服务器程序最简单的办法就是fork一个子进程来服务每个客户。
当一个连接建立时,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接的套接字描述符),父进程则等待另一连接(通过监听套接字listendfd)。既然新的客户由子进程提供服务,父进程就关闭已连接套接字。
扩展
建立副服务器,通过共享内存使主服务器与副服务器共同管控同一片内存空间,然后再进行对指令的操作