Linux C 之FTP云盘项目(网络编程)
一、项目简介
1.引言
本项目为本人初次入坑linux c 网络编程,该项目比较基础,还有待优化,此外,还存在一个bug:目前的文件传输大小受限于字符数组的初值,未能实现大量拷贝;但五脏俱全,该有的基本功能还是有的。
如图:data用来存放命令,secondBuf用来存放文件内容
2.项目概况
本项目是在ubuntu-12.04.5-64的linux操作系统下运行,c语言架构,客户端和服务器直接通过网络进行交互,采用的是面向连接TCP传输协议,实现客户端远程获取服务器磁盘上的文件内容,同时也能将客户端本地文件进行上传,完成下载文件和文件上传的功能
功能实现如下:
C语言编程,使用socket进行通信,采用TCP协议;服务器端先调用socket()函数初始化网络服务,再使用bind()函数进行信息绑定,如ip地址等;接着启动listen()函数监听,当accept()函数接收到成功连接(完成TCP三次握手)的客户端,每次有一个新的客户端成功连接,服务器就会调用fork()函数创建一个新的子进程进行对接,提供相应服务;服务器会通过getDesDir()字符串处理函数分割客户端指令,进行不同指令的处理;文件下载则是利用文件编程操作获取服务器磁盘文件上的内容,并将内容通过网络发送给客户端,客户端收到数据后创建同名新文件并将数据写入,文件上传也是同理;普通的文件夹操作指令如ls,pwd等,则是通过popen(),system()等函数实现
二、项目源码(含部分解析)
1.服务器端server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
//#include <Msgconfig.h>
#include <sys/stat.h>
#include <fcntl.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
struct Msg{
int type;
char data[128];
char secondBuf[65535];
//msgFileBuf本来想用来传输大量文件信息的,没搞通,暂时不用
char *msgFileBuf;
};
//获取命令比较数ret
int get_cmd_type(char *cmd){
//处理不带参数命令
//strcmp:第一个字符串等于第二个字符串,则返回0
if(!strcmp("ls", cmd)) return LS;
if(!strcmp("quit",cmd)) return QUIT;
if(!strcmp("pwd", cmd)) return PWD;
//处理带参数命令
//strstr:返回值为char * 类型(返回指向str1中第一次出现的str2的指针);
//如果str2不是str1的一部分,则返回空指针
if(strstr(cmd,"cd") != NULL) return CD;
if(strstr(cmd,"get") != NULL) return GET;
if(strstr(cmd,"put") != NULL) return PUT;
return 100;
}
//获取命令后面的参数
char *getDesDir(char *cmsg){
char *p;
//strtok:该函数返回被分解的第一个子字符串指针;若无可检索的字符串,则返回空指针
//而第二次调用strtok的时候,传入的参数应该为NULL
//使得该函数默认使用上一次未分割完的字符串继续分割
p = strtok(cmsg," ");
p = strtok(NULL," ");
return p;
}
void msg_handler(struct Msg msg, int fd){
char dataBuf[1024] = {0};
char *file = NULL;
int fdfile;
printf("cmd:%s\n",msg.data);
//自写函数get_cmd_type(),把不同命令对应到不同的整型数并返回比较数ret,方便后续switch分支
int ret = get_cmd_type(msg.data);
switch(ret){
case LS:
case PWD:
msg.type = 0;
FILE *r;
//popen:获取命令运行的输出结果
r = popen(msg.data,"r");
//fread:从文件中读取若干字节数据到内存缓冲区中
fread(msg.data,sizeof(msg.data),1,r);
//write:将缓冲区内容通过msg结构体发送给客户端
write(fd,&msg,sizeof(msg));
break;
case CD:
msg.type = 1;
//自写函数getDesDir(),分割字符串,获取cd命令后面的参数
char *dir = getDesDir(msg.data);
printf("dir:%s\n",dir);
//chdir函数用于改变当前工作目录,调用参数是指向目录的指针
chdir(dir);
break;
case GET:
file = getDesDir(msg.data);
//access(file,F_OK):判断当前文件是否存在
if(access(file,F_OK) == -1){
strcpy(msg.data,"No This File!");
write(fd,&msg,sizeof(msg));
}
else{
msg.type = DOFILE; //一个标志,提醒客户端有文件可读
//打开文件并读取到dataBuf缓冲区
//fdfile 文件操作标识符
fdfile = open(file,O_RDWR);
read(fdfile,dataBuf,sizeof(dataBuf));
close(fdfile);
strcpy(msg.data,dataBuf);
//将msg消息结构体发送给客户端
write(fd,&msg,sizeof(msg));
break;
}
case PUT:
//在服务端创建客户端同名文件
fdfile = open(getDesDir(msg.data),O_RDWR|O_CREAT,0666);
//写入客户端文件数据,完成文件上传拷贝
write(fdfile,msg.fileBuf,strlen(msg.fileBuf));
//printf("msg.fileBuf:%s\n",msg.fileBuf);
close(fdfile);
break;
case QUIT:
printf("client quit!\n");
exit(-1);
}
}
int main(int argc, char **argv){
//argc -- 命令行参数的总个数,包括 可执行程序名。
//argv[i] -- 第 i 个参数
//argv[0] -- 可执行程序名
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
struct Msg msg;
//argc -- 命令行参数的总个数,包括 可执行程序名
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket 初始化网络服务
//AF_INET 表示使用互联网协议族
//SOCK_STREAM 指定socket类型 流式套接字 TCP协议 0:自适应选择对应的传输协议
//s_fd 网络操作标识符,类似文件操作
s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd == -1){
//启动失败 返回-1 打印错误 结束任务
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
//字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
//htons:返回端口网络字节序的值 (网络字节序=大端字节序:将高序字节存储在起始地址)
//如:0x01020304 大端字节序--> 0000 0001 放在首地址位置(底部)
//atoi: 字符串 转 整型
//argv[i] -- 第 i 个参数
//argv[0] -- 可执行程序名
s_addr.sin_port = htons(atoi(argv[2]));
//inet_aton:把字符串形式的“192.168.1.123”转为网络能识别的格式
inet_aton(argv[1],&s_addr.sin_addr);
//2.bind 进行参数信息绑定,如绑定地址
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));
//3.listen 启动监听
listen(s_fd,10);
//4.accept 进行监听,等待客户端连接
int clen = sizeof(struct sockaddr_in);
while(1){
//accept会一直阻塞,当出现完成TCP三次握手的客户端时,才往下走
//c_fd 成功连接后的网络操作标识符
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("connect");
}
//inet_ntoa:把网络格式的ip地址转为字符串形式
printf("get connect:%s\n",inet_ntoa(c_addr.sin_addr));
//每次有一个新的客户端成功连接,fork()就创建一个新的子进程来对接相应客户端进行服务
//fork()返回进程的标识符,0代表是子进程
if(fork() == 0){
while(1){
//子进程一直阻塞,读取消息
memset(msg.data,0,sizeof(msg.data));
n_read = read(c_fd,&msg,sizeof(msg));
if(n_read == 0){
printf("clinet out\n");
break;
}
else if(n_read > 0){
//如果消息读取成功,执行消息回调函数来处理不同的消息
msg_handler(msg,c_fd);
}
}
}
}
close(c_fd);
close(s_fd);
return 0;
}
2.客户端client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
//#include <Msgconfig.h>
#include <sys/stat.h>
#include <fcntl.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
struct Msg{
int type;
char data[128];
char secondBuf[65535];
//msgFileBuf本来想用来传输大量文件信息的,没搞通,暂时不用
char *msgFileBuf;
};
//获取命令后面的参数
char * getdir(char *cmd){
char *p;
//strtok:该函数返回被分解的第一个子字符串指针;若无可检索的字符串,则返回空指针
//而第二次调用strtok的时候,传入的参数应该为NULL
//使得该函数默认使用上一次未分割完的字符串继续分割
p = strtok(cmd," ");
p = strtok(NULL," ");
return p;
}
//获取命令比较数ret
int get_cmd_type(char *cmd){
//处理不带参数命令
//strcmp:第一个字符串等于第二个字符串,则返回0
if(!strcmp("lls", cmd)) return LLS;
if(!strcmp("ls", cmd)) return LS;
if(!strcmp("quit",cmd)) return QUIT;
if(!strcmp("pwd", cmd)) return PWD;
//处理带参数命令
//strstr:返回值为char * 类型(返回指向str1中第一次出现的str2的指针);
//如果str2不是str1的一部分,则返回空指针
if(strstr(cmd,"lcd") != NULL) return LCD;
if(strstr(cmd,"cd") != NULL) return CD;
if(strstr(cmd,"get") != NULL) return GET;
if(strstr(cmd,"put") != NULL) return PUT;
return -1;
}
int cmd_handler(struct Msg msg, int fd){
char *dir = NULL;
char buf[32];
int ret;
int fdfile;
int size;
//自写函数get_cmd_type(),把不同命令对应到不同的整型数并返回比较数ret,方便后续switch分支
ret = get_cmd_type(msg.data);
switch(ret){
case LS:
case CD:
case PWD:
msg.type = 0;
//write:将缓冲区内容通过msg结构体发送给服务器端
write(fd,&msg,sizeof(msg));
break;
case GET:
msg.type = 2;
write(fd,&msg,sizeof(msg));
break;
case PUT:
strcpy(buf,msg.data);
dir = getdir(buf);
//access(file,F_OK):判断当前文件是否存在
if(access(dir,F_OK) == -1){
printf("%s not exsit\n",dir);
}
else{
//打开文件并读取到dataBuf缓冲区
//fdfile 文件操作标识符
fdfile = open(dir,O_RDWR);
read(fdfile,msg.secondBuf,sizeof(msg.secondBuf));
close(fdfile);
//将msg消息结构体发送给服务器端
write(fd,&msg,sizeof(msg));
}
break;
case LLS:
system("ls");
break;
case LCD:
dir = getdir(msg.data);
chdir(dir);
break;
case QUIT:
printf("client quit!\n");
exit(-1);
}
return ret;
}
//获取服务器的返回内容
void handler_server_message(int c_fd, struct Msg msg){
int n_read;
struct Msg msgget;
int newfilefd;
n_read = read(c_fd,&msgget,sizeof(msgget));
//阻塞读取,读取为0,说明连接已丢失
if(n_read == 0){
printf("server is out,quit!\n");
exit(-1);
}
else if(msgget.type == DOFILE){
char *p = getdir(msg.data);
newfilefd = open(p,O_RDWR|O_CREAT,0600);
write(newfilefd,msgget.data,strlen(msgget.data));
putchar('>');
fflush(stdout);
}
else{
printf("-------------------------------\n");
printf("\n%s\n",msgget.data);
printf("-------------------------------\n");
printf(">");
//fflush(stdout):清空输出缓冲区
fflush(stdout);
}
}
int main(int argc, char **argv){
//argc -- 命令行参数的总个数,包括 可执行程序名。
//argv[i] -- 第 i 个参数
//argv[0] -- 可执行程序名
int c_fd;
struct sockaddr_in c_addr;
struct Msg msg;
//argc -- 命令行参数的总个数,包括 可执行程序名
if(argc != 3){
printf("param is not good\n");
exit(-1);
}
memset(&c_addr,0,sizeof(struct sockaddr_in));
//1.socket 初始化网络服务
//AF_INET 表示使用互联网协议族
//SOCK_STREAM 指定socket类型 流式套接字 TCP协议 0:自适应选择对应的传输协议
//s_fd 网络操作标识符,类似文件操作
c_fd = socket(AF_INET,SOCK_STREAM,0);
if(c_fd == -1){
//启动失败 返回-1 打印错误 结束任务
perror("socket");
exit(-1);
}
c_addr.sin_family = AF_INET;
//字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
//htons:返回端口网络字节序的值 (网络字节序=大端字节序:将高序字节存储在起始地址)
//如:0x01020304 大端字节序--> 0000 0001 放在首地址位置(底部)
//atoi: 字符串 转 整型
//argv[i] -- 第 i 个参数
//argv[0] -- 可执行程序名
c_addr.sin_port = htons(atoi(argv[2]));
//inet_aton:把字符串形式的“192.168.1.123”转为网络能识别的格式
inet_aton(argv[1],&c_addr.sin_addr);
//2.connect 尝试与服务端进行连接
if(connect(c_fd,(struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
//连接失败,打印错误,结束任务
perror("connect");
exit(-1);
}
printf("connect ...\n");
int mark = 0;
while(1){
//msg.data消息缓冲区清零
memset(msg.data,0,sizeof(msg.data));
//“>” 输入美观化
if(mark == 0) printf(">");
//gets:获取用户输入并存放到消息缓冲区
gets(msg.data);
//****输入美观化****
if(strlen(msg.data) == 0){
if(mark == 1){
printf(">");
}
continue;
}
mark = 1;
//******************
//自写函数cmd_handler(),对用户输入的命令进行处理
//给服务器发送相应命令
int ret = cmd_handler(msg,c_fd);
//判断哪些指令还需获取等待服务器返回
if(ret > IFGO){
putchar('>');
//fflush(stdout):清空输出缓冲区
fflush(stdout);
continue; //进入新一轮循环
}
if(ret == -1){
printf("command not \n");
printf(">");
//fflush(stdout):清空输出缓冲区
fflush(stdout);
continue;
}
//获取服务器的返回内容
handler_server_message(c_fd,msg);
}
return 0;
}
三、效果展示
本次效果展示的客户端和服务器均在同一台虚拟机上(局域网下即可),但两者运行在不同的文件夹下
FTP文件夹下运行服务器端(左边),FTP2文件夹下运行客户端(右边)
如图,运行前:
开始运行,连接成功:
尝试命令ls,lls:
客户端上传文件3.txt给服务器:
客户端从服务器端获取文件1.txt: