目录
1. 项目功能
1.1 远程功能
1.2 本地功能
2. 项目基本流程
2.1 服务端
2.2 客户端
3. 项目所需 API
3.1 access 函数
3.2 popen 函数
3.3 chdir 函数
3.4 strtok 函数
3.5 strncmp 函数
4. FTP 代码实例
4.1 头文件 ftp.h
4.2 客户端 client.c
4.3 服务端 server.c
4.4 程序运行结果
5. 项目补充
5.1 关于某些命令是用 popen函数 还是 system函数
5.2 关于客户端与服务端的 fork 函数
客户端可以通过 FTP 连接实现如下功能:
功能:判断文件是否存在
int access(const char *pathname, int mode);
参数1:文件名字
参数2:这里用F_OK
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
返回值:不存在返回-1
功能:可以像打开文件一样打开 shell 指令。后续可以使用 fread 读取内容到缓冲区 buf
FILE *popen(const char *command, const char *type);
参数1:shell 指令
参数2:r 或者 w ,一般都是 r
int pclose(FILE *stream); //打开后要用 pclose 关闭文件
功能:跳转至指定目录
int chdir(const char *path);
参数:指向目标目录的指针
为什么不直接调用 system 函数来跳转目录呢?
因为调用 system 函数相当于 fork 出一个子进程来处理跳转目录,子进程执行了 cd 命令后改变了自己的 pwd, 但是子进程执行完后会消亡,而父进程的路径没有改变,所以不能使用 system 函数来跳转目录。
功能:字符串分隔,把参数二的字符变成'\0',返回其前面的字符串地址。
函数原型与 Demo 如下
// char *strtok(char *str, const char *delim);
参数1:要分割的字符串
参数2:分隔字符
#include
#include
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;
}
程序运行结果:
功能:字符串比较。
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
参数1:比较字符串1
参数2:比较字符串2
参数3:比较前几个是否一样
返回值:一样返回0,不一样非0
注:为什么不用 strcmp,因为 strcmp 在判断一些需要带有文件名的指令时,如(cd xxx)不能准确判断,而 strncmp 可以准确判断前面指令的字符个数,不易出错。
还有一种方案是用 strstr 函数,查看命令中是否含有特殊的指令来判断也可以。
#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]; // 存放根据指令进行相关读取操作的 结果
};
#include "ftp.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
char *stringSplit(char *buf)
{
char *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;
}
#include "ftp.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//判断一个 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;
}
LS PWD 因为需要在客户端看到返回的内容,所以要用 popen 函数操作后写入 c_fd 传送给客户端才能看得到。而不能直接调用 system 函数,如果直接调用 system 函数的话,命令的信息就会直接显示在服务器端,我们客户端看不见。
LLS LPWD 是客户端指令,可以直接调用 system 函数来操作,操作完成后直接显示在客户端能看得见。
RM LRM 指令因为处理完不需要看到文件情况,所以也可以直接调用 system 函数来处理,处理完成后只需要提醒客户端再次输入 LS 或者 LLS 即可查看文件变动情况。
关于 fork ,服务器在 listen 完后就 while(1) 一直 accept ,如果成功 accept 则调用 fork 来处理,主进程还是一直 while(1) 尝试 accept 客户端 connect 后无需 fork 直接处理用户输入的数据。