文章目录
项目简介
- FTP服务器(File Transfer Protocol Server)是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务。 FTP是File Transfer Protocol(文件传输协议)。
- 这个项目分成ftp客户端及服务端,实现的功能和Linux开源的 FTP 服务器类似,客戶端通过网络,远程获取服务端磁盘上的文件夹内容,下载文件,上传文件等功能。
知识点描述
-
FTP服务器用到的是Socket网络通信,当收到客户端接入的时候,服务器创建子进程对接连接,子进程启动后分析来自客户端的指令,服务端可同时处理多个客户端接入并对指令作出解析,并把执行结果返回给客户端。比如:收到get file1的指令,是客户端想要获取file1文件的,我先用strstr()函数进行字符串分割,获取到文件名,在判断文件是否存在,如果文件存在,就读取文件內容,再将內容通过套接字发给客户端,客户端收到数据后,创建文件,并将收到的数据写入文件,完成文件的远程下载。
-
上传文件和下载文件类似,主要还是涉及文件的操作,字符串的操作,以及网络编程。
-
还支持了Is、pwd、cd等Linux系统常用的指令。普通指令的实现用popen来调用系统指令,并读取执行的结构。如果不需要获取执行结果,用system函数调用就可以了。
项目功能指令
远程功能指令
- LS —— 展示服务端文件,用法:ls
- PWD —— 展示服务端的当前路径,用法:pwd
- CD —— 用于切换服务端的路径,用法:cd path
- RM —— 用于删除服务端的文件,用法:rm file
- GET —— 下载服务端文件至客户端本地,用法:get file
- PUT —— 把客户端本地文件上传至服务端,用法:put file
本地功能指令
- LLS —— 展示客户端本地文件,用法:lls
- LPWD —— 展示客户端当前路径,用法:lpwd
- LCD —— 用于切换客户端本地的路径,用法:lcd path
- LRM —— 用于删除客户端本地的文件:用法:lrm file
- QUIT —— 用于客户端的退出,用法:quit
使用的关键函数
access 函数
int access(const char *pathname, int mode);
功能:判断文件是否存在
参数1:
- 文件名字
参数2:(这里用F_OK)
- R_OK 只判断是否有读权限
- W_OK 只判断是否有写权限
- X_OK 判断是否有执行权限
- F_OK 只判断是否存在
返回值:
- 不存在返回-1
popen 函数
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream); //打开后要用 pclose 关闭文件
功能:可以像打开文件一样打开 shell 指令。后续可以使用 fread 读取内容到缓冲区 buf
参数1:
- shell 指令
参数2:
- r 或者 w ,一般都是 r
注意:
- popen与system的区别:popen可以将返回的结果写在文件中,system只能执行指令,返回成功与否
- popen返回的是文件流,可以通过fread读取文件流可以写入到指定缓冲区,指令LS PWD需要用到该函数
- system 函数直接返回结果到控制台,指令LLS LPWD 可以使用该函数,在客户端直接展示
- RM LRM 指令因为处理完不需要看到文件情况,所以也可以直接调用 system 函数来处理,处理完成后只需要提醒客户端再次输入 LS 或者 LLS 即可查看文件变动情况。
chdir 函数
int chdir(const char *path);
功能:跳转至以参数path 指定目录
参数:
- 指向目标目录的指针
返回值执:
- 成功则返回0,
- 失败返回-1, errno 为错误代码.
注意:
- 为什么不直接调用 system 函数来跳转目录呢?因为调用 system 函数相当于 fork 出一个子进程来处理跳转目录,子进程执行了 cd 命令后改变了自己的 pwd, 但是子进程执行完后会消亡,而父进程的路径没有改变,所以不能使用 system 函数来跳转目录。
strtok 函数
char *strtok(char *str, const char *delim);
功能:字符串分隔,把参数二的字符修改为’\0’,返回其前面的字符串地址。
参数1:
- 要分割的字符串
参数2:
- 分隔字符
- 指定临界点
#include <stdio.h>
#include <string.h>
int main()
{
char buf[128] = "hello Linux !!!";
char *p;
printf("buf = %s\n", buf);
p = strtok(buf, " ");
printf("\n第一次处理: 相当于 buf = hello\\0Linux !!! \n");
printf("p1 = %s\n", p);
printf("buf = %s\n", buf);
p = strtok(NULL, " ");
printf("\n第二次处理:相当于 buf = hello\\0Linux\\0!!! \n");
printf("p2 = %s\n", p);
printf("buf = %s\n", buf);
printf("buf + 6 = %s\n", buf + 6);
p = strtok(NULL, " ");
printf("\n第三次处理:从第一个 ! 开始,直到最后遇到\\0结束\n");
printf("p3 = %s\n", p);
printf("buf = %s\n", buf);
printf("buf + 12 = %s\n", buf + 12);
p = strtok(NULL, " ");
printf("\n第四次处理:后面没有字符串,指针指向 NULL \n");
printf("p4 = %s\n", p);
printf("buf = %s\n", buf);
printf("buf + 15 = %s\n", buf + 15);
return 0;
}
输出结果
strncmp 函数
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
功能:字符串比较。strncmp能够精确判断字符的个数
参数1:
- 比较字符串1
参数2:
- 比较字符串2
参数3:
- 比较前几个是否一样
返回值:
- 一样返回0,
- 不一样非0
注意:
- 为什么不用 strcmp,因为 strcmp 在判断一些需要带有文件名的指令时,如(cd xxx)不能准确判断,而 strncmp 可以准确判断前面指令的字符个数,不易出错。
- 还有一种方案是用 strstr 函数,查看命令中是否含有特殊的指令来判断也可以。
linux system函数是否执行成功判断方法
基本流程
服务端
- socket 创建服务端的套接字
- bind 端口号和 IP 地址
- listen 监听客户端的连接
- accept 接受客户端的接入
- fork 创建子进程处理客户端操作
- read 接收客户端发送的 cmd
- 服务端开始处理从客户端接收到的 cmd
- write 服务端处理完的 data 到客户端
客户端
- socket 创建客户端的套接字
- connect 连接上服务端,配置端口号和 IP 地址
- 连接成功后,获取用户键盘输入,处理输入命令 cmd
- write 客户端的 cmd 到服务端
- read 服务端处理完返回的 data
FTP代码实例
头文件 ftp.h
#define LS 0
#define LLS 1
#define PWD 2
#define CD 3
#define RM 5
#define GET 6
#define PUT 7
#define LPWD 8
#define LCD 9
#define LRM 10
#define QUIT 120
struct myFTP
{
int set; // 客户端返回给服务器的宏命令
int mark; // 判断文件是否存在的标识
char cmd[128]; // 用户输入的命令
char data[1024]; // 存放根据指令进行相关读取操作的 结果
};
客户端 client.c
#include "ftp.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
char *stringSplit(char *buf)
{
char *p;
p = (char *)malloc(128);//buf.cmd空间为128字节
p = strtok(buf, " ");
p = strtok(NULL, " ");
return p;
}
/* 判断输入的完整指令的前几个关键指令 */
int getCommandSet(char *cmd)
{
if (strncmp(cmd, "ls", 2) == 0)return LS;
if (strncmp(cmd, "lls", 3) == 0)return LLS;
if (strncmp(cmd, "pwd", 3) == 0)return PWD;
if (strncmp(cmd, "cd", 2) == 0)return CD;
if (strncmp(cmd, "rm", 2) == 0)return RM;
if (strncmp(cmd, "get", 3) == 0)return GET;
if (strncmp(cmd, "put", 3) == 0)return PUT;
if (strncmp(cmd, "lpwd", 4) == 0)return LPWD;
if (strncmp(cmd, "lcd", 3) == 0)return LCD;
if (strncmp(cmd, "lrm", 3) == 0)return LRM;
if (strncmp(cmd, "quit", 4) == 0)return QUIT;
return -1;
}
/* 读取服务器处理完后 buf.data 的内容 */
void readFromServer(int c_fd, struct myFTP buf)
{
int nread = read(c_fd, &buf, sizeof(buf));
if (nread == -1)
{
perror("read");
}
else if (nread == 0)
{
printf("server quit\n");
exit(0);
}
else
{
printf("%s", buf.data);
}
}
void sendCommand(int c_fd, struct myFTP buf)
{
buf.mark = 0; //判断文件是否存在的标识
char *p_tmp = NULL;
int fd;
off_t fileSize;
while (1)
{
memset(&buf, 0, sizeof(buf)); // 每次操作命令前都先把 buf 的内容清空,确保不会被上次操作遗留的内容影响下次操作
printf("\n************************************************************************\n");
printf("*****please input (ls pwd cd rm get put quit lls lpwd lcd lrm lcd)*****\n");
printf("**************************************************************************\n");
printf(">> ");
gets(buf.cmd); // 从键盘获取完整命令
printf("command:%s\n", buf.cmd);
buf.set = getCommandSet(buf.cmd);
switch (buf.set)
{
case LS: // 和 PWD 的处理方式一样,所以不需要 break
case PWD:
write(c_fd, &buf, sizeof(buf)); // 把 buf 结构体发送至 c_fd 给服务器处理
readFromServer(c_fd, buf);
break;
case LLS: // 和 LPWD、LRM 的处理方式一样,所以不需要 break
case LPWD:
case LRM:
p_tmp = buf.cmd;
p_tmp++; // 加一是为了指向第二个字符,以第二个字符开始,屏蔽L
system(p_tmp);
break;
case LCD:
p_tmp = stringSplit(buf.cmd); // 字符串分割提取路径
strcpy(buf.cmd, p_tmp);
int ret = chdir(buf.cmd); // 切换路径
if (ret == -1)
{
perror("chdir");
}
else
{
printf("chdir success\n");
}
break;
case CD:
p_tmp = stringSplit(buf.cmd); // 字符串分割提取路径
strcpy(buf.cmd, p_tmp); // 把路径复制到 buf.cmd
write(c_fd, &buf, sizeof(buf)); // 传整个结构体过去 c_fd 给 server
readFromServer(c_fd, buf); // 读取服务器操作完返回的数据
break;
case RM:
write(c_fd, &buf, sizeof(buf)); // 把 rm xxx 传给客户端用 system 函数处理
readFromServer(c_fd, buf);
break;
case GET:
p_tmp = stringSplit(buf.cmd); // 提取文件名
strcpy(buf.cmd, p_tmp);
write(c_fd, &buf, sizeof(buf));
read(c_fd, &buf, sizeof(buf)); // 把服务器传送过来 c_fd 处理完的内容读取到 buf 结构体
if (buf.mark == 0) // 判断服务器是否因为找不到目标文件而把标志位设置为 -1
{
fd = open(buf.cmd, O_RDWR | O_CREAT, 0777); // 打开 get 的文件,如果没有则创建文件,权限可读可写
write(fd, buf.data, strlen(buf.data)); // 把客户端放进 buf.data 的内容写入 fd 文件
close(fd);
printf("get success\n");
}
else // 服务器因为找不到目标文件而把标志位设置为 -1
{
printf("%s\n", buf.data);
}
break;
case PUT:
p_tmp = stringSplit(buf.cmd); // 提取文件名
strcpy(buf.cmd, p_tmp);
if (access(buf.cmd, F_OK) == -1) // 如果找不到目标文件
{
buf.mark = -1; // 把标志位至 -1
printf("NO this file\n");
strcpy(buf.data, "NO this file\n");
write(c_fd, &buf, sizeof(buf));
}
else
{
fd = open(buf.cmd, O_RDWR); // 打开目标文件,权限可读可写
fileSize = lseek(fd, 0, SEEK_END); // 移动光标至文件最后,返回值是该文件(光标前面)的字节数
lseek(fd, 0, SEEK_SET); // 设置光标至最前
read(fd, buf.data, fileSize); // 把目标文件的内容读取至 buf.data
write(c_fd, &buf, sizeof(buf)); // 把 buf 结构体发送至 c_fd 传给服务器
close(fd); // 关闭目标文件
printf("put success\n");
}
break;
case QUIT:
exit(0);
break;
case -1:
printf("error command\n");
break;
}
}
}
int main(int argc, char *argv[])
{
int c_fd;
int c_Ret;
struct myFTP buf;
struct sockaddr_in c_addr;
memset(&c_addr, 0, sizeof(struct sockaddr_in));
if (argc != 3)
{
printf("input error\n");
}
// 1. socket
c_fd = socket(AF_INET, SOCK_STREAM, 0);
// 防止段错误
if (c_fd == -1)
{
perror("socket");
exit(-1);
}
// 2. bind 配置 struct sockaddr_in 结构体,绑定时再转换成 struct sockaddr * 结构体类型
c_addr.sin_family = AF_INET; /* address family: AF_INET */
c_addr.sin_port = htons(atoi(argv[2])); /* port in network byte order */
inet_aton(argv[1], &c_addr.sin_addr); /* internet address */
// 3. connect
c_Ret = connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr_in));
// 防止段错误
if (c_Ret < 0)
{
perror("connect");
exit(-1);
}
else
{
printf("connect success\n");
sendCommand(c_fd, buf);
}
close(c_fd);
return 0;
}
服务端 server.c
#include "ftp.h"
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
//判断一个 system 函数调用 shell 脚本是否正常结束
int System_Check(int result)
{
if ((-1 != result) && (WIFEXITED(result)) && (!(WEXITSTATUS(result))))
return 0;
else
return -1;
}
void commandHandler(int c_fd, struct myFTP buf, int cmd)
{
int nread;
int result = 0;
int fd;
off_t fileSize; // 文件内容大小
buf.mark = 0; // 0 -> 成功, -1 -> 失败
int ret;
FILE *file;
/* 连接到客户端后一直 while(1) 读取客户端发过来的内容进行处理 */
while (1)
{
memset(&buf, 0, sizeof(buf));
nread = read(c_fd, &buf, sizeof(buf));
if (nread < 0)
{
perror("read");
}
else if (nread == 0)
{
printf("No.%d client quit\n", cmd);
exit(0);
}
else
{
printf("No.%d command:> %s\n\n", cmd, buf.cmd);
switch (buf.set)
{
case LS:
case PWD:
file = popen(buf.cmd, "r"); // popen()可以执行shell命令,并读取此命令的返回值
fread(buf.data, 1024, 1, file);
write(c_fd, &buf, sizeof(buf));
pclose(file);
break;
case CD:
ret = chdir(buf.cmd);
if (ret == -1)
{
perror("chdir");
}
else
{
strcpy(buf.data, "chdir success! You can input ls to check!~\n");
write(c_fd, &buf, sizeof(buf));
}
break;
case RM:
result = system(buf.cmd); // system 函数处理命令 rm xxx
if (!System_Check(result))
{
strcpy(buf.data, "rm success! You can input ls to check!~\n");
write(c_fd, &buf, sizeof(buf));
}
else
{
strcpy(buf.data, "rm fail\n");
write(c_fd, &buf, sizeof(buf));
}
break;
case GET:
if (access(buf.cmd, F_OK) == -1) // 如果找不到目标文件
{
buf.mark = -1; // 把标志位至 -1
strcpy(buf.data, "NO this file\n");
write(c_fd, &buf, sizeof(buf));
}
else
{
fd = open(buf.cmd, O_RDWR); // 打开目标文件,权限可读可写
fileSize = lseek(fd, 0, SEEK_END); // 移动光标至文件最后,返回值是该文件(光标前面)的字节数
lseek(fd, 0, SEEK_SET); // 设置光标至最前
read(fd, buf.data, fileSize); // 把目标文件的内容读取至 buf.data
write(c_fd, &buf, sizeof(buf)); // 把 buf 结构体发送至 c_fd 传给客户端
close(fd); // 关闭目标文件
printf("client command: GET success!~\n");
}
break;
case PUT:
if (buf.mark == 0) // 判断客户端是否因为找不到目标文件而把标志位设置为 -1
{
fd = open(buf.cmd, O_RDWR | O_CREAT, 0777);
write(fd, buf.data, strlen(buf.data));
close(fd);
}
else // 客户端因为找不到目标文件而把标志位设置为 -1
{
printf("%s\n", buf.data);
}
break;
}
}
}
}
int main(int argc, char *argv[])
{
int s_fd;
int c_fd;
int s_Ret;
struct myFTP buf;
int cmd = 0;
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("input error\n");
}
// 1. socket
s_fd = socket(AF_INET, SOCK_STREAM, 0);
// 防止段错误
if (s_fd == -1)
{
perror("socket");
exit(-1);
}
// 2. bind 配置 struct sockaddr_in 结构体,绑定时再转换成 struct sockaddr * 结构体类型
s_addr.sin_family = AF_INET; /* address family: AF_INET */
s_addr.sin_port = htons(atoi(argv[2])); /* port in network byte order */
inet_aton(argv[1], &s_addr.sin_addr); /* internet address */
s_Ret = bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));
// 防止段错误
if (s_Ret == -1)
{
perror("bind");
exit(-1);
}
// 3. listen
s_Ret = listen(s_fd, 10);
// 防止段错误
if (s_Ret == -1)
{
perror("listen");
exit(-1);
}
// 4. accept
int len = sizeof(struct sockaddr_in);
while (1)
{
c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &len);
if (c_fd == -1)
{
perror("accept");
exit(-1);
}
else
{
cmd++;
printf("get connect: No.%d IP:%s\n", cmd, inet_ntoa(c_addr.sin_addr));
}
if (fork() == 0)
{
commandHandler(c_fd, buf, cmd);
}
}
close(s_fd);
close(c_fd);
return 0;
}
执行校验:
md5sum test.txt
#注1:结果为1行2列。第一列是md5值,第二列是md5值对应的文件名
- md5sum命令采用MD5报文摘要算法(128位)计算和检查文件的校验和。
- 一般来说,安装了Linux后,就会有md5sum这个工具,直接在命令行终端直接运行。
- linux中每个文件都会有1个md5值。当两个文件的md5值相同,表示这两个文件完全相同。