目录
一、项目实现的功能
二、项目实现思路
2.1 socket服务器和客户端开发步骤:
2.2 指令的实现思路:
三、项目用到的函数
3.1 判断文件是否存在函数access()原型和头文件:
3.2 字符串输入函数fgets()原型和头文件:
3.3 字符串分割函数strtok()原型和头文件:
3.4 当前工作目录更改为指定的工作目录函数chdir()原型和头文件:
四、FTP项目实现
一、项目实现的功能
-
在客户端对服务器的操作:
-
获取服务器的文件(get)
-
查看服务器当前路径下的所有文件(ls)
-
进入服务器的某个文件夹(cd)
-
上传文件到服务器(put)
-
查看当前服务器路径(pwd)
-
退出(quit)
-
-
在客户端本地的功能实现:
-
查看客户端当前路径下有哪些文件(lls)
-
进入客户端的某个文件夹(lcd)
-
查看当前客户端路径(lpwd)
-
退出连接(quit)
-
二、项目实现思路
2.1 socket服务器和客户端开发步骤:
-
服务器server:
-
创建套接字(socket)
-
将socket与IP地址和端口绑定(bind)
-
监听被绑定的端口(listen)
-
接收连接请求(accept)
-
当有客户端接入时创建子进程对接并处理客户端发来的请求
-
-
客户端client:
-
创建套接字(socket)
-
连接指定计算机的端口(connect)
-
接收输入向socket中写入信息(write)
-
获取客户端发送的内容像ls,pwd,get指令,需要进行(read)
-
2.2 指令的实现思路:
ls与pwd:调用popen函数,执行命令,并获取执行命令后的输出内容,将内容发回到客户端
cd:将cd指令发送到服务器,在服务器端调用chdir实现路径的切换
get:客户端获取服务器的某个文件,服务器首先通过access函数判断文件是否存在,存在则将文件的内容发送到客户端,客户端创建文件,并将服务器发送的内容保存至文件中,实现get指令。
put:客户端向服务器发送文件,客户端首先通过access函数判断文件是否存在,存在则将文件的内容发送到服务器,服务器创建文件,并将客户端发送的内容保存至文件中,实现put指令。
lls,lpwd:调用system函数即可。
lcd:在客户端直接调用chdir函数即可
三、项目用到的函数
3.1 判断文件是否存在函数access()原型和头文件:
/*Linux下 man 2 access查看手册
*/
#include <unistd.h>int access(const char *pathname, int mode);int 函数返回值,返回值如果成功(所有请求的权限都被授予,或者mode为F_OK并且文件存在),返回0。如果出现错误(mode中至少有一个 位请求的权限被拒绝,或者mode为F_OK而文件不存在,或者发生了其他错误),则返回-1,并适当设置errno。char *pathname 需要检测的文件路径名int mode 需要测试的操作模式
1. R_OK 测试读许可权
2. W_OK 测试写许可权
3. X_OK 测试执行许可权
4. F_OK 测试文件是否存在/*函数说明:判断参数中的文件名是否存在*/
3.2 字符串输入函数fgets()原型和头文件:
/*Linux下 man fgets查看手册
*/
#include <stdio.h>char *fgets(char *s, int size, FILE *stream);char * 函数返回值,fgets()成功时返回s(数组首地址),错误时返回NULL,或者在没有读取任何字符的情况下出现文件结束。char *s 它是一个指向字符数组的指针,用于存储读取的文本行。
int size 表示要读取的最大字符数(包括空字符)
FILE *stream 表示从何种流中读取,可以是标准输入流 stdin,也可以是文件流,即从某个文件中读取/*函数说明:虽然用 gets() 时有空格也可以直接输入,但是 gets() 有一个非常大的缺陷,即它不检查预留存储区是否能够容纳实际输入的数据,换句 话说,如果输入的字符数目大于数组的长度,gets 无法检测到这个问题,就会发生内存越界,所以编程时建议使用 fgets()
*/
3.3 字符串分割函数strtok()原型和头文件:
/*Linux下 man strtok查看手册
*/
#include <string.h>char *strtok(char *str, const char *delim); char * 会返回一个指向被分割出的第一个子字符串(即第一个空格之前的部分)的指针。如果没有更多的分割部分,则返回 NULL。char *str 指向要被分割的字符串的指针最初调用时,为需要分割的字符串。后续调用时,为 NULL 以获取下一部分标记。char *delim 指定用来分割字符串的分隔符,比如说空格" "一个包含分隔符的字符串,这些字符用于分割字符串中的标记。/*函数说明:strtok 函数用于将字符串 str 分割成一系列标记 (token),这些标记由参数 delim 中的字符分隔开来。*/使用方式:
strtok 首次调用时传入需要分割的字符串,并传入分隔符字符串。后续调用时,将 str 参数传递为 NULL,以继续检索同一字符串中的下一部分内容
char str[] = "hello world";
char delim[] = " ";
char *token = strtok(str, delim);while (token != NULL) {printf("Token: %s\n", token);token = strtok(NULL, delim);
}
3.4 当前工作目录更改为指定的工作目录函数chdir()原型和头文件:
/*Linux下 man 2 chdir查看手册
*/
#include <unistd.h>int chdir(const char *path);int 函数返回值,通常,当成功时返回 0,失败时返回 -1,并设置 errno 来指示出错原因。char *path 参数是一个字符串指针 path,表示要改变到的目录路径。该路径可以是绝对路径或相对路径。/*函数说明:是一个标准库函数,它的主要功能是改变当前工作目录。它接收一个路径参数并尝试将当前工作目录更改为指定的路径,在成功时返回 0,失败时返回 -1 并设置错误代码。这个功能对于实现像 FTP 服务器这样的程序非常重要,因为它允许服务器根据客户端请求动态更改当前工作目录。
*/
四、FTP项目实现
/*config.h*/
#define LS 0
#define PWD 1
#define GET 2#define IFGO 3#define CD 4
#define PUT 5
#define LLS 6
#define LCD 7
#define LPWD 8#define QUIT 9
#define DOFILE 10struct MSG
{int type; // message type char cmd[1024]; // message datachar secondBuf[1024]; // second buffer for data
};
宏定义部分:
-
#define LS 0
定义一个宏LS
,其值为0
。 -
#define PWD 1
定义一个宏PWD
,其值为1
。 -
#define GET 2
定义一个宏GET
,其值为2
。 -
#define IFGO 3
定义一个宏IFGO
,其值为3
。 -
#define CD 4
定义一个宏CD
,其值为4
。 -
#define PUT 5
定义一个宏PUT
,其值为5
。 -
#define LLS 6
定义一个宏LLS
,其值为6
。 -
#define LCD 7
定义一个宏LCD
,其值为7
。 -
#define LPWD 8
定义一个宏LPWD
,其值为8
。 -
#define QUIT 9
定义一个宏QUIT
,其值为9
。 -
#define DOFILE 10
定义一个宏DOFILE
,其值为10
。
这些宏定义为一组命令标识符赋予了整数值,以便在代码中更便捷地使用这些命令。
结构体定义部分:
-
struct MSG
定义了一个名为MSG的结构体,其中包含三个成员:
-
int type
: 一个整型变量,用于表示消息类型。 -
char cmd[1024]
: 一个长度为 1024 的字符数组,用于存储命令数据。 -
char secondBuf[1024]
: 一个长度为 1024 的字符数组,用于存储额外的数据。
-
总结:
这段代码的主要功能是定义了一组文件传输协议(FTP)相关的命令及其对应的整数值,同时定义了一个结构体 MSG
用于存储这些命令及其数据。
-
命令标识符:通过宏定义,赋予了
LS
,PWD
,GET
等命令对应的整数值。这些命令表示了常见的 FTP 操作,如列出目录内容、获取当前目录、从服务器获取文件等。 -
消息结构体:
MSG
结构体用于传递 FTP 命令及其相关数据。type
成员存储命令类型,cmd
和secondBuf
成员分别存储命令数据和额外的数据。
这段代码通过宏和结构体的定义,为实现 FTP 客户端与服务器之间的通信打下了基础。
/*serverFTP.c*/
#include <stdio.h> // 包含标准输入输出头文件
#include <stdlib.h> // 包含标准库头文件
#include <sys/types.h> // 包含系统类型定义头文件
#include <sys/socket.h> // 包含套接字编程头文件
#include <netinet/in.h> // 包含网络地址头文件
#include <arpa/inet.h> // 包含网络字节序头文件
#include <string.h> // 包含字符串头文件
#include <unistd.h> // 包含unistd头文件
#include "config.h" // 包含配置文件头文件
#include <sys/stat.h> // 包含文件状态头文件
#include <fcntl.h> // 包含文件控制头文件char* get_cmd_dir(char *cmd) //获取目录名函数
{char *p = NULL;p = strtok(cmd," "); //分割命令字符串p = strtok(NULL," "); //获取目录名return p; //返回目录名
}int cmd_type(char *cmd) //命令类型判断函数
{if(!strcmp("ls", cmd)) return LS; //ls命令if(!strcmp("pwd", cmd)) return PWD; //pwd命令if(!strcmp("quit",cmd)) return QUIT; //quit命令if(strstr(cmd,"cd")) return CD; //cd命令if(strstr(cmd,"get")) return GET; //get命令if(strstr(cmd,"put")) return PUT; //put命令return -1; //未知命令
}void msg_handler(int c_fd, struct MSG msg) //消息处理函数
{int ret; //命令类型int fdfile; //文件描述符char *dir = NULL; //目录名 char *file = NULL; //文件名char dataBuf[1024] = {0}; //数据缓冲区 printf("客户端发送消息:%s\n", msg.cmd); //打印客户端消息ret = cmd_type(msg.cmd); //将客户端命令类型转为int类型switch(ret){ //根据命令类型进行处理case LS:case PWD:msg.type = 0; //设置消息类型为0,表示命令执行成功FILE *fp = popen(msg.cmd, "r"); //打开命令管道fread(msg.cmd,sizeof(msg.cmd),1,fp); //读取命令输出write(c_fd, &msg, sizeof(msg)); //发送命令执行结果pclose(fp); //关闭命令管道break;case CD:msg.type = 1; //设置消息类型为1dir = get_cmd_dir(msg.cmd); //获取目录名chdir(dir); //切换目录 break;case GET:file = get_cmd_dir(msg.cmd); //获取文件名if(access(file, F_OK) == -1){ //判断文件是否存在 strcpy(msg.cmd, "文件不存在!"); //设置命令执行结果为文件不存在write(c_fd, &msg, sizeof(msg)); //发送命令执行结果}else{msg.type = DOFILE; //设置消息类型为DOFILE fdfile = open(file, O_RDWR); //打开文件 read(fdfile, dataBuf, sizeof(dataBuf)); //读取文件内容close(fdfile); //关闭文件strcpy(msg.secondBuf, dataBuf); //设置文件内容到消息结构体的secondBuf write(c_fd, &msg, sizeof(msg)); //发送文件内容}break;case PUT:fdfile = open(get_cmd_dir(msg.cmd), O_RDWR | O_CREAT, 0666); //服务器首先创建并打开要上传的文件write(fdfile,msg.secondBuf,sizeof(msg.secondBuf));//然后将客户端上传给服务器文件里的内容拷贝到新创建的文件中close(fdfile); //关闭文件break;case QUIT:printf("客户端断开连接!\n");exit(-1); //退出程序 }
}int main(int argc, char **argv)
{int s_fd; //socket文件描述符int c_fd; //client文件描述符pid_t pid; //子进程pidint n_read; //读字节数struct MSG msg; //消息结构体struct sockaddr_in server_addr; //服务器地址结构体struct sockaddr_in client_addr; //客户端地址结构体memset(&server_addr, 0, sizeof(server_addr)); //清零服务器地址结构体memset(&client_addr, 0, sizeof(client_addr)); //清零客户端地址结构体if(argc != 3){ //参数个数错误 printf("参数错误!请按照格式输入:./serverFTP IP地址 端口号");exit(-1); //退出程序 } //int socket(int domain, int type, int protocol);s_fd = socket(AF_INET, SOCK_STREAM, 0); //创建socket套接字 if(s_fd == -1){printf("创建socket失败!");exit(-1);}server_addr.sin_family = AF_INET; // 设置服务器地址结构体的地址族为AF_INET,表示使用IPv4协议server_addr.sin_port = htons(atoi(argv[2])); // 设置服务器地址结构体的端口号inet_aton(argv[1], &server_addr.sin_addr); // 设置服务器地址结构体的IP地址//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);int ret = bind(s_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //绑定socketif(ret == -1){printf("绑定socket失败!");exit(-1);}//int listen(int sockfd, int backlog);ret = listen(s_fd, 10); //监听socket if(ret == -1){printf("监听失败!\n");}printf("服务器启动成功!\n");int client_addr_len = sizeof(client_addr); //客户端地址长度while(1){//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);c_fd = accept(s_fd,(struct sockaddr *)&client_addr, &client_addr_len); //接受连接请求 if(c_fd == -1){printf("接受客户端连接请求失败!");exit(-1);}printf("客户端连接成功!\n"); //打印客户端地址printf("客户端地址:%s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); //pid_t fork(void);pid = fork(); //创建子进程if(pid == -1){printf("创建子进程失败!");exit(-1);}else if(pid == 0){while(1){memset(&msg, 0, sizeof(msg)); //清零消息结构体//ssize_t read(int fd, void *buf, size_t count);n_read = read(c_fd, &msg, sizeof(msg)); //读取客户端消息if(n_read == 0){printf("客户端断开连接!\n");exit(0); //退出子进程}else if(n_read > 0){printf("客户端发送消息:%s\n", msg.cmd); //打印客户端消息msg_handler(c_fd, msg); //处理客户端消息}}} }close(s_fd); //关闭socketclose(c_fd); //关闭clientreturn 0;
}
这段代码实现了一个基本的FTP服务器功能,主要包括以下几个方面:
-
创建和绑定套接字:通过
socket()
、bind()
、listen()
等函数完成服务器端的初始化工作。 -
处理客户端连接:使用
accept()
函数接收客户端的连接请求,并创建子进程处理每个客户端。 -
命令处理:通过
msg_handler()
处理客户端发送的命令,包括ls
、pwd
、cd
、get
、put
和quit
等命令。 -
数据传输:根据不同的命令类型进行相应的文件操作,并将结果反馈给客户端。
这个FTP服务器具有基本的文件传输和目录操作功能,适合用于学习和理解FTP服务器的基本实现原理。
/*clientFTP.c*/
#include <stdio.h> // 包含标准输入输出头文件
#include <stdlib.h> // 包含标准库函数头文件
#include <sys/types.h> // 包含数据类型头文件
#include <sys/socket.h> // 包含Socket通信头文件
#include <netinet/in.h> // 包含Internet地址族头文件
#include <arpa/inet.h> // 包含IP地址转换功能头文件
#include <string.h> // 包含字符串操作函数头文件
#include "config.h" // 包含配置文件头文件
#include <unistd.h> // 包含unistd.h头文件
#include <sys/stat.h> // 包含文件状态头文件
#include <fcntl.h> // 包含文件控制头文件char* getDir(char *cmd) //获取文件路径函数
{char *p = NULL; //文件路径指针p = strtok(cmd, " "); //分割命令字p = strtok(NULL," "); //获取文件路径return p;
}int cmd_type(char *cmd) //命令类型判断函数
{if(strstr(cmd, "lcd")) return LCD; //判断是否为LCD命令if(strcmp(cmd, "lpwd") == 0) return LPWD; //判断是否为LPWD命令if(strcmp(cmd, "lls") == 0) return LLS; //判断是否为LLS命令if(strcmp(cmd, "ls") == 0) return LS; //判断是否为LS命令if(strcmp(cmd, "pwd") == 0) return PWD; //判断是否为PWD命令if(strcmp(cmd, "quit") == 0) return QUIT; //判断是否为QUIT命令if(strstr(cmd, "cd")) return CD; //判断是否为CD命令if(strstr(cmd, "get")) return GET; //判断是否为GET命令if(strstr(cmd, "put")) return PUT; //判断是否为PUT命令return -1; //未知命令类型
}int cmd_handler(int c_fd, struct MSG msg) //命令处理函数
{int ret; //返回值char *p = NULL; //命令字指针char buf[128] = {0}; //缓冲区int file_fd; //文件描述符char *dir = NULL; //文件路径指针ret = cmd_type(msg.cmd); //获取命令类型 switch(ret){case LS:case PWD:msg.type = 0; //设置消息类型为0write(c_fd, &msg, sizeof(msg)); //发送消息 break;case CD:msg.type = 1; //设置消息类型为1write(c_fd, &msg, sizeof(msg)); //发送消息 break;case GET:msg.type = 2; //设置消息类型为2 write(c_fd, &msg, sizeof(msg)); //发送消息 break;case PUT:strcpy(buf, msg.cmd); //拷贝命令字到缓冲区p = getDir(buf); //获取文件路径if(access(p, F_OK) == -1){ //判断文件是否存在printf("文件不存在\n");}else{file_fd = open(p, O_RDWR); //可读可写方式打开文件read(file_fd, &msg.secondBuf, sizeof(msg.secondBuf)); //读文件内容到消息结构体的第二个缓冲区close(file_fd); //关闭文件write(c_fd, &msg, sizeof(msg)); //发送消息}break;case LCD:dir = getDir(msg.cmd); //获取文件路径if(chdir(dir) == -1){ //切换目录printf("目录不存在\n");}break;case LLS:system("ls"); //执行系统命令 break;case LPWD:system("pwd"); //执行系统命令 break;case QUIT:strcpy(msg.cmd, "quit"); //拷贝命令字到消息结构体write(c_fd, &msg, sizeof(msg)); //发送消息close(c_fd); //关闭套接字exit(-1); //退出程序break;} return ret; //返回命令类型
}void handler_sever_message(int c_fd, struct MSG msg) //处理服务器消息函数
{int n_read; //读取字节数struct MSG getmsg; //接收消息结构体 char *filename; //文件名指针int newfile_fd; //新文件描述符n_read = read(c_fd, &getmsg, sizeof(getmsg)); //接收消息if(n_read == 0){printf("服务器关闭连接\n");exit(-1);}if(getmsg.type == DOFILE){ //当客户端输入的指令是GET时,需要在客户端创建需要获取的文件filename = getDir(msg.cmd); //获取文件名newfile_fd = open(filename, O_RDWR|O_CREAT, 0666); //创建新文件write(newfile_fd, getmsg.secondBuf, sizeof(getmsg.secondBuf)); //写入文件内容close(newfile_fd); //关闭文件printf("文件%s上传成功\n", filename); //打印提示信息fflush(stdout); //刷新标准输出缓冲区}else{printf("------------------------------\n");printf("服务器消息: %s\n", getmsg.cmd); //打印服务器消息printf("------------------------------\n");}}int main(int argc, char **argv)
{int c_fd; //客户端套接字文件描述符int ret; //返回值struct sockaddr_in client_addr; //客户端地址结构体struct MSG msg; //消息结构体memset(&client_addr, 0, sizeof(client_addr)); //清零客户端地址结构体if(argc != 3){ //判断参数是否正确printf("参数错误,请按照格式输入: clientFTP IP 端口号\n");exit(-1);}//int socket(int domain, int type, int protocol);c_fd = socket(AF_INET, SOCK_STREAM, 0); //创建客户端套接字if(c_fd == -1){printf("创建客户端套接字失败\n");perror("socket");exit(-1);}client_addr.sin_family = AF_INET; //设置客户端地址结构体的地址族为IPv4client_addr.sin_port = htons(atoi(argv[2])); //设置客户端地址结构体的端口号inet_aton(argv[1], &client_addr.sin_addr);; //设置客户端IP地址 //int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);ret = connect(c_fd, (struct sockaddr *)&client_addr, sizeof(client_addr)); //连接服务器if(ret == -1){printf("连接服务器失败\n");perror("connect");exit(-1);}printf("连接服务器成功\n");while(1){memset(&msg.cmd, 0, sizeof(msg.cmd)); //清零消息结构体的命令字printf("请输入命令字: \n");gets(msg.cmd); //从标准输入获取命令字printf("------------------------------\n");printf("客户端消息: %s\n", msg.cmd); //打印客户端消息printf("------------------------------\n");ret = cmd_handler(c_fd, msg); //处理命令字if(ret > IFGO){fflush(stdout); //刷新标准输出缓冲区continue; //继续循环}if(ret == -1){printf("未知命令类型\n");fflush(stdout); //刷新标准输出缓冲区continue; //继续循环}handler_sever_message(c_fd, msg); //处理服务器消息}return 0;
}
这段代码的主要功能是实现一个基本的FTP客户端,能够与FTP服务器建立连接并执行一系列命令,具体包括:
-
与服务器建立连接:
-
创建TCP套接字,并通过IP地址和端口号与指定的FTP服务器建立连接。
-
-
处理用户输入的命令:
-
命令包括本地操作(本地切换目录、本地列出目录内容、本地打印当前目录)、远程操作(远程切换目录、远程查询目录内容)、文件传输(上传和下载文件)以及退出命令等。
-
根据命令类型,通过不同的分支进行处理,并准备好相关的消息发送给服务器。
-
-
与服务器交互:
-
将用户的命令打包成特定格式的消息,通过套接字发送给服务器。
-
接收服务器的回应消息,并根据消息类型进行处理。
-
-
本地文件和目录操作:
-
在本地执行系统命令,如
ls
(列出目录内容)和pwd
(打印当前目录)。 -
切换本地目录。
-
判断本地文件是否存在并进行文件读取,上载(上传)文件内容到服务器。
-
-
处理服务器回应:
-
根据服务器返回的消息类型,执行相应操作。
-
例如,当从服务器下载文件时,在本地创建相应文件并写入内容。
-
总结而言,这段代码实现了一个基本的FTP客户端,通过命令行操作与FTP服务器进行交互,实现文件传输、目录操作以及简单的本地系统命令执行。通过本地和远程命令的处理逻辑,客户端能够进行从服务器下载文件到本地、上传本地文件到服务器、查询本地和远程目录等常见功能。