FTP服务器项目

这里我选取的是西邮Linux兴趣小组的FTP服务器项目。大家可以先去小组官网上查看一下这个项目的要求然后再来看这篇文章。

我会结合代码来解释实现思路和实现原理。

什么是FTP

首先我们要了解FTP,然后我们才能谈怎么实现这个项目。

FTP是一种在网络中进行文件传输的广泛使用的标准协议,可以实现文件的上传,下载,删除,重命名,列出目录,更改路径等功能。大家可以下去更详细的了解一下FTP相关的内容。

但是在小组的项目中,我们只需要实现一些最基本的功能,如文件的上传,文件的下载,列出目录这几个功能。

接下来我们就开始实现这个项目了,我将从实现逻辑来完成这个项目。

FTP服务器的实现。

我了解到FTP服务器的操作都限定在固定的目录下进行操作,这样可以防止文件被破坏,意外修改或是删除。那么这个就是我要实现的第一步。

FTP服务器操作路径的限定

首先我们需要在启动服务器时确定服务器的工作目录,我们可以给出一个工作目录,当然我们没有给出工作目录的时候我们需要一个默认目录。

#define ROOT_PATH "./FTP"

当我们没有给出工作目录时就使用这个默认的路径

复制代码
实现思路
首先判断命令行参数的个数,个数不为1时代表给出了工作目录
    接下来我们就需要获取这个路径的绝对路径(为什么需要绝对路径呢? 这样可以方便我们正确查找路径,避免指向错误的路径)
    我们realpath会自动调用malloc然后在堆上分配内存,所以我们在使用之后需要手动释放内存。
      错误处理,如果realpath函数执行失败,返回1(表示异常)
      成功执行后,我们将获取到的绝对路径拷贝给目标路径数组
当命令行参数的个数为1时代表我们没有给出工作目录
    那么首先我们需要获取当前的工作目录
        获取失败返回1
	    获取成功后我们需要拼接完整的默认工作目录
	        进行检查确保完全写入,没有被截断
	    得到完整的路径后我们就需要创建目标目录并设置权限
	    然后用realpath解析该目录的绝对目录
	        失败返回1
	        成功将该就对路径复制给目标路径数组
	    最后释放内存

这是这部分的代码的实现

c 复制代码
if(argc > 1) {
        char *abs = realpath(argv[1], NULL);
        if(!abs) {
            perror("Invalid root path");
            return 1;
        }
        strcpy(g_path, abs);
        free(abs);
    }
    else {
        char cwd[1024];
        if(!getcwd(cwd, sizeof(cwd))) {
            perror("getcwd");
            return 1;
        }
        // 使用更大的临时缓冲区避免截断警告
        char tmp_root[2048];
        int written = snprintf(tmp_root, sizeof(tmp_root), "%s/%s", cwd, ROOT_PATH);
        if(written < 0 || (size_t)written >= sizeof(tmp_root)) {
            fprintf(stderr, "Root path too long: %s/%s\n", cwd, ROOT_PATH);
            return 1;
        }
        mkdir(tmp_root, 0755);
        char *abs = realpath(tmp_root, NULL);
        if(!abs) {
            perror("realpath root");
            return 1;
    }
    strncpy(g_path, abs, sizeof(g_path) - 1);
    g_path[sizeof(g_path) - 1] = '\0';
    free(abs);
}
printf("Root: %s\n", g_path);

我们在确定了目录之后该干什么,我们就可以创建套接字了

我这里将这部分封装成了一个创建套接字和完成初始化的函数了

创建套接字

int listen_fd = init_listenfd();

复制代码
首先创建一个监听套接字
	进行错误处理
然后设置端口复用,允许套接字重复使用本地地址和端口(注意设置的位置,在socket和bind之间)
创建地址结构体
	进行初始化
绑定地址结构体
设置监听上限
设置边缘触发模式
最后返回这个套接字

这是代码的实现(这部分内容基本就是每一个服务器都必备的形式)

c 复制代码
int init_listenfd(void) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0) {
        exit(1);
    }
    
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(PORT);
    
    if(bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0){
        exit(1);
    }
    
    if(listen(fd, 128) < 0){
        exit(1);
    }
    
    int flags = fcntl(fd, F_GETFL, 0);
    if(flags != -1) {
        fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    }
    
    return fd;
}

我们创建好了一个套接字了,接下来我们需要进行IO多路转接,这里我们采取linux系统下独有的epoll来实现

IO多路转接(单个线程监听多个文件描述符)

复制代码
首先创建一个监听红黑树,epoll实例(这是所有事件监控的"中枢神经")
创建epoll_event结构体(事件的载体)
接下来配置监听事件(设置为ET模式),告诉内核对套接字的具体监听要求(可读  边缘触发ET模式)
将当前监听套接字文件描述符绑定到事件的data字段中
向这个监听红黑树上添加事件

这是代码的实现

c 复制代码
efd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_fd;
epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &ev);

现在IO多路复用实现了,那么我们接下来就该accept客户端事件了,但是在接收之前我们需要实现一个简单的线程池来提高工作函数的效率

这里初始化线程池,预先创好一组工作线程

c 复制代码
pthread_t workers[THREAD_POOL_SIZE];
    for(int i = 0; i < THREAD_POOL_SIZE; i++) {
        pthread_create(&workers[i], NULL, worker_thread, NULL);
}
printf("FTP server running on port %d\n", PORT);

线程工作函数的实现

复制代码
忽略没有参数传入
线程启动循环
	获取任务
	成功获取任务后任务分发处理

代码实现

c 复制代码
void *worker_thread(void *arg) {
    (void)arg;
    while (!g_shutdown) {
        task_t *t = get_task();
        if(t) {
            handle_command(t->session, t->cmd_line);
            free(t);
        }
    }
    return NULL;
}

从任务队列中获取任务

我们在调用线程池的函数的时候我们需要获取任务,然后才能执行其他的命令,这里就是获取任务的实现思路

复制代码
加锁保护线程
阻塞等待任务(服务器没关且没有任务)
	阻塞等待唤醒
当服务器为空,且任务队列为空
	解锁,退出
将任务从任务队列中取出来
解锁,返回取出的任务

这是代码的实现

c 复制代码
task_t *get_task(void) {
    pthread_mutex_lock(&g_task_mutex);
    while (g_task_head == NULL && !g_shutdown)
        pthread_cond_wait(&g_task_cond, &g_task_mutex);
    if(g_shutdown && g_task_head == NULL) {
        pthread_mutex_unlock(&g_task_mutex);
        return NULL;
    }
    task_t *t = g_task_head;
    g_task_head = g_task_head->next;
    if(g_task_head == NULL) {
        g_task_tail = NULL;
    }
    pthread_mutex_unlock(&g_task_mutex);
    return t;
}

将任务分发给处理函数

在线程池任务函数调用之后,我们就需要开始处理任务了,那么我们就需要将获取到的任务函数分发给其他的处理函数,那么接下来就是实现处理函数的思路

复制代码
首先需要识别命令和参数,然后提取出来
将提取的命令和参数格式化,将命令和参数分开存储
将命令同一转换为大写,方便后面比较
	可以添加一些登录处理(我这里的实现是直接无密码验证就成功登录)
	退出处理
		用户发出退出请求回复信息后
		关闭套接字
		将该文件描述符设置为-1
		之后会在事件循环处理中清除这些关闭的事件

这是这部分的代码的实现

c 复制代码
void handle_command(ftp_session_t *s, const char *cmd_line) {
    char cmd[10], arg[256];
    cmd[0] = arg[0] = '\0';
    sscanf(cmd_line, "%9s %255s", cmd, arg);
    for(char *c = cmd; *c; c++) {
        *c = toupper(*c);
    }
    printf("Thread %lu: %s\n", (unsigned long)pthread_self(), cmd_line);

    if(strcmp(cmd, "USER") == 0 || strcmp(cmd, "PASS") == 0) {
        send_response(s->ctrl_fd, "230", "Login successful");
    }
    else if(strcmp(cmd, "QUIT") == 0) {
        send_response(s->ctrl_fd, "221", "Goodbye");
        close(s->ctrl_fd);
        s->ctrl_fd = -1;
    }

我们接收到客户端发来的任务,那么首先就需要进行的是指令的拆分方便后面来识别指令,执行命令。

接下来我们需要干什么?

我们知道当我们受到需要传输数据的指令的时候,(如上传文件,下载文件或是列出目录等命令的时候)就需要知道FTP是有两个通道的,一个就是用来控制链接的,另一条就是用来数据链接的,但是我们的主动模式和被动模式的数据链接方式不同,所以我们要分别处理这两种情况。

从这里开始我们就需要对FTP的主动和被动链接有一个清晰的了解,后面的文件操作都是基于这里的数据通道的链接的。是很重要的部分

复制代码
	主动模式处理,在主动模式下客户端会打开一个端口然后告诉服务器链接这个IP和端口
		提取发来的IP和端口号
		如果之前有被动模式,那么这里清除关闭
		(主动模式下,服务器不需要维持一个监听socket了)
		创建新的地址结构体,解析新的数据IP和端口号
		标记为主动模式
		保存客户端数据接受地址
		发送回应
	被动模式处理(由服务端开启一个随机端口进行监听,并将该地址告知客户端,随后等待客户端主动发起数据连接)
		标记为被动模式
		旧资源清理,将之前遗留的未关闭的被动模式的监听套接字关闭
		创建数据监听通道依赖于创建数据链接套接字的函数
		获取服务器的IP地址
		发送被动模式下的回应
c 复制代码
    else if(strcmp(cmd, "PORT") == 0) {
        int h1,h2,h3,h4,p1,p2;
        if(sscanf(arg, "%d,%d,%d,%d,%d,%d", &h1,&h2,&h3,&h4,&p1,&p2) != 6) {
            send_response(s->ctrl_fd, "501", "Syntax error");
            return;
        }

        if(s->data_listen_fd != -1) { 
            close(s->data_listen_fd);
        }
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons((p1<<8)|p2);
        
        char ip_str[16];
        snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", h1,h2,h3,h4);
        inet_pton(AF_INET, ip_str, &addr.sin_addr);
        s->active_mode = 1;
        s->data_addr = addr;
        send_response(s->ctrl_fd, "200", "PORT command successful");
    }
    else if(strcmp(cmd, "PASV") == 0) {
        s->active_mode = 0;
        if(s->data_listen_fd != -1)
            close(s->data_listen_fd);
        int fd, port;
        if(make_data_socket(&fd, &port) != 0) {
            send_response(s->ctrl_fd, "425", "Cannot open passive connection");
            return;
        }
        s->data_listen_fd = fd;
        struct sockaddr_in local;
        socklen_t len = sizeof(local);
        getsockname(s->ctrl_fd, (struct sockaddr*)&local, &len);
        unsigned char *ip = (unsigned char*)&local.sin_addr.s_addr;
        char resp[256];
        sprintf(resp, "Entering Passive Mode (%d,%d,%d,%d,%d,%d)",
                ip[0], ip[1], ip[2], ip[3], port/256, port%256);
        send_response(s->ctrl_fd, "227", resp);
    }
这里是创建数据链接套接字函数

被动模式下我们服务器需要创建一个新的套接字来进行数据的传输这里我直接将这部分封装成了一个函数。

复制代码
创建套接字
端口复用
地址结构体初始化
绑定IP和端口号
获取系统分配的端口号
将主动套接字转换为被动套接字
将创建的套接字文件描述符保存到会话对象的指针变量中
c 复制代码
int make_data_socket(int *listen_fd, int *port) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0) {
        return -1;
    }
    
    int opt = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    struct sockaddr_in addr = {0};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = 0;
    
    if(bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        close(fd);
        return -1;
    }
    
    socklen_t len = sizeof(addr);
    getsockname(fd, (struct sockaddr*)&addr, &len);
    *port = ntohs(addr.sin_port);
    
    if(listen(fd, 1) < 0) {
        close(fd);
        return -1;
    }
    *listen_fd = fd;
    return 0;
}
会话对象结构体
c 复制代码
typedef struct ftp_session {
    int ctrl_fd;                // 控制连接套接字
    int data_listen_fd;         // 被动模式监听套接字
    int active_mode;            // 主动模式
    struct sockaddr_in data_addr; // 主动模式下客户端指定的数据地址
    char recv_buf[BUFFER_SIZE];
    int recv_len;
} ftp_session_t;

主被动模式处理完后,我们就需要进行数据链接,数据连接我们需要理清思路,到底是怎么链接的,谁链接谁。

复制代码
	建立数据链接
		主动模式
			服务器创建一个套接字
			然后用connect连接地址结构体
			检查socket创建是否成功,检查connect链接是否成功
			这里的监听端口是一次性的,用完立即重置
		被动模式
			服务器接受客户端发送的连接
			异常处理和资源释放
			监听套接字使用完之后立即关闭(每次数据传输必须新建链接)
			一次使用
		没有建立通道
			直接发送错误码
	分发执行具体的指令
		列出目录
			发送状态码信息
			执行列出目录函数
			发送状态码,表示数据传输圆满完成
		下载文件
			发送状态码
			执行文件下载函数
			判断文件是否存在
				错误处理
			发送状态码
		上传文件
			状态码
			执行上传文件函数
			判断文件是否存在
			状态码
		其他命令
			返回错误状态码

这里是这一部分数据链接逻辑处理的代码

c 复制代码
    else if(strcmp(cmd, "LIST") == 0 ||
            strcmp(cmd, "RETR") == 0 ||
            strcmp(cmd, "STOR") == 0) {
        int data_fd = -1;
        if(s->active_mode) {
            data_fd = socket(AF_INET, SOCK_STREAM, 0);
            if(data_fd < 0 || connect(data_fd, (struct sockaddr*)&s->data_addr, sizeof(s->data_addr)) < 0) {
                send_response(s->ctrl_fd, "425", "Cannot connect to client");
                if(data_fd >= 0) 
                    close(data_fd);
                s->active_mode = 0;
                return;
            }
            s->active_mode = 0;
        }
        else if(s->data_listen_fd != -1) {
            data_fd = accept(s->data_listen_fd, NULL, NULL);
            if(data_fd < 0) {
                send_response(s->ctrl_fd, "425", "Data connection failed");
                close_data_socket(s->data_listen_fd, -1);
                s->data_listen_fd = -1;
                return;
            }
            close_data_socket(s->data_listen_fd, -1);
            s->data_listen_fd = -1;
        }
        else{
            send_response(s->ctrl_fd, "425", "Use PORT or PASV first");
            return;
        }

        if(strcmp(cmd, "LIST") == 0) {
            send_response(s->ctrl_fd, "150", "Here comes the directory listing");
            list_directory(data_fd);
            send_response(s->ctrl_fd, "226", "Directory send OK");
        }
        // RETR直接用 retrieve_file 返回值判断
        else if(strcmp(cmd, "RETR") == 0) {
            send_response(s->ctrl_fd, "150", "Opening data connection");
            if (retr_file(arg, data_fd) == 0) {
                send_response(s->ctrl_fd, "226", "Transfer complete");
            }
            else{
                send_response(s->ctrl_fd, "550", "File not found or access denied");
            }
        }
        // STOR根据 store_file 返回值决定回复 226 还是 550
        else if(strcmp(cmd, "STOR") == 0) {
            send_response(s->ctrl_fd, "150", "Ready to receive data");
            if(store_file(arg, data_fd) == 0) {
                send_response(s->ctrl_fd, "226", "Transfer complete");
            }
            else {
                send_response(s->ctrl_fd, "550", "Failed to store file");
            }
        }
        close(data_fd);
    }
    else {
        send_response(s->ctrl_fd, "502", "Command not implemented");
    }
}
关闭套接字

这里我们需要关闭新创建的监听套接字,不影响数据的传输。

c 复制代码
void close_data_socket(int listen_fd, int data_fd) {
    if(data_fd != -1) {
        close(data_fd);
    }
    if(listen_fd != -1) {
        close(listen_fd);
    }
}

文件传输和列出目录

我们已经实现了取出任务,解析命令,数据传输通道的链接,那么接下来就该实现数据的传输了

列出目录

我们先来实现第一个功能------列出当前服务器工作目录下的文件

复制代码
首先我们需要打开当前工作目录
	进行失败处理
然后我们就需要循环读取目录下的条目,知道返回NULL,或是到了目录尾(过滤掉隐藏文件)
一次获取一条信息然后返送太慢了,太耗费资源了,我们可以选择直到缓冲区快满了再发送信息,
按照FTP协议的要求,在缓冲区最后添加文件名和结束符,注意这里的结束符是"\r\n"
将剩下的数据全部传输,最后关闭目录。

这是代码实现

c 复制代码
void list_directory(int data_fd) {
    DIR *dir = opendir(g_path);
    if(!dir) {
        return ;
    }
    struct dirent *entry;
    char listing[BUFFER_SIZE] = {0};
    while ((entry = readdir(dir)) != NULL) {
        if(entry->d_name[0] == '.') {
            continue;
        }
        if(strlen(listing) + strlen(entry->d_name) + 4 > BUFFER_SIZE - 256) {
            send(data_fd, listing, strlen(listing), MSG_NOSIGNAL);
            listing[0] = '\0';
        }
        strcat(listing, entry->d_name);
        strcat(listing, "\r\n");
    }
    if(strlen(listing) > 0) send(data_fd, listing, strlen(listing), MSG_NOSIGNAL);
    closedir(dir);
}

接下来就是和文件相关的操作了

文件的下载
复制代码
我们在文件下载的时候是会给出一个文件名的,我们首先要检验这个路径是否有问题
接下来我们就需要打开这个文件
	检验这个文件是否存在,且有权限可以读取
我们成功打开文件之后,就可以进行数据的传输了
	我们为了节省资源的开销,我们可以使用零拷贝传输
		sendfile需要现定义偏移量
		然后循环保证数据全部传输完毕(一次sendfile无法保证数据全部传输完毕)
等到数据全部传输完之后关闭文件

代码的实现

c 复制代码
int retr_file(const char *filename, int data_fd) {
    char full[2048];

    if(safe_path(filename, full, sizeof(full)) != 0) {
        return -1;
    }

    int fd = open(full, O_RDONLY);
    if(fd < 0) return -1;  // 文件不存在或无读权限

    struct stat st;
    if(fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
        close(fd);
        return -1;
    }

    off_t offset = 0;
    while(offset < st.st_size) {
        ssize_t sent = sendfile(data_fd, fd, &offset, st.st_size - offset);
        if(sent <= 0) {
            break;
        }
    }
    close(fd);
    return 0;
}
文件的上传

我们实现了文件的下载,那么就该实现文件的下载了

复制代码
首先我们依旧需要,判断输入的文件的路径并且将该路径保存
然后根据获取的路径,确保文件的父目录存在(父目录不存在则open()无法正常执行)
打开文件准备写入信息
写入信息时为了保证数据全部传输完成,需要双重循环
	外层循环------保证数据完全接收
	内层循环------将收到数据的数据完整的写入
		写入失败,标记,跳出循环,结束数据传输
写入完毕,关闭文件
返回值确定文件上传是否成功,成功返回0, 异常则返回-1;

下面是代码的实现

c 复制代码
int store_file(const char *filename, int data_fd) {
    char full[2048];
    if(safe_path(filename, full, sizeof(full)) != 0) {
        return -1;
    }
    // 确保父目录存在
    char dir_copy[2048];
    strncpy(dir_copy, full, sizeof(dir_copy) - 1);
    dir_copy[sizeof(dir_copy) - 1] = '\0';
    char *p = strrchr(dir_copy, '/');
    if(p) {
        *p = '\0';
        mkdir(dir_copy, 0755);
    }

    int fd = open(full, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if(fd < 0) {
        perror("store_file open");
        return -1;
    }

    char buf[BUFFER_SIZE];
    ssize_t n;
    int write_ok = 1;
    while((n = recv(data_fd, buf, BUFFER_SIZE, 0)) > 0) {
        ssize_t written = 0;
        while(written < n) {
            ssize_t w = write(fd, buf + written, n - written);
            if (w <= 0) { write_ok = 0; break; }
            written += w;
        }
        if(!write_ok) {
            break;
        }
    }
    close(fd);
    return write_ok ? 0 : -1;
}
这里数据传输的相关操作我们动需要检查路径安全
c 复制代码
int safe_path(const char *user_path, char *out_path, size_t out_size) {
    if(!user_path || user_path[0] == '\0') {
        return -1;
    }
    char tmp[2048];
    snprintf(tmp, sizeof(tmp), "%s/%s", g_path, user_path);

    // 先尝试解析完整路径(适用于已存在的文件/目录)
    char resolved[2048];
    if(realpath(tmp, resolved) != NULL) {
        // 确保解析后的路径以根目录开头
        size_t root_len = strlen(g_path);
        if(strncmp(resolved, g_path, root_len) != 0) {
            return -1;
        }
        // 防止 /root_extra 这种前缀匹配错误
        if(resolved[root_len] != '\0' && resolved[root_len] != '/') {
            return -1;
        }    
        strncpy(out_path, resolved, out_size - 1);
        out_path[out_size - 1] = '\0';
        return 0;
    }

    // 文件不存在(如 STOR 新建文件),检查父目录
    char parent[2048];
    strncpy(parent, tmp, sizeof(parent) - 1);
    parent[sizeof(parent) - 1] = '\0';
    char *last_slash = strrchr(parent, '/');
    if(!last_slash || last_slash == parent) {
        return -1;
    }
    *last_slash = '\0';

    char parent_res[2048];
    if(realpath(parent, parent_res) == NULL) {
        return -1;
    }
    size_t root_len = strlen(g_path);
    if(strncmp(parent_res, g_path, root_len) != 0) {
        return -1;
    }
    if(parent_res[root_len] != '\0' && parent_res[root_len] != '/') {
        return -1;
    }
    // 父目录合法,拼接原始文件名作为输出路径
    strncpy(out_path, tmp, out_size - 1);
    out_path[out_size - 1] = '\0';
    return 0;
}

任务线程函数的处理我们全部解决了,那么接下来,我们就需要解决客户端的链接和客户端发送来的任务的处理和添加

事件循环处理

复制代码
首先创建就绪事件数组
服务器循环运行不关闭
	阻塞等待事件到来
	遍历就绪事件
		有新的客户端链接
		没有则处理旧会话
			若有读事件,分发给处理函数,添加任务
			检查会话是否存在,是否标记删除

这是核心的添加事件的代码,也是很主要的部分

c 复制代码
struct epoll_event events[MAX_EVENTS];
    while (!g_shutdown) {
        int nfds = epoll_wait(efd, events, MAX_EVENTS, -1);
        for(int i = 0; i < nfds; i++) {
            if(events[i].data.fd == listen_fd) {
                accept_new_connection(listen_fd);
            }
            else {
                ftp_session_t *s = (ftp_session_t*)events[i].data.ptr;
                if(events[i].events & EPOLLIN)
                    handle_client_read(s);
                if(s->ctrl_fd == -1) {
                    epoll_ctl(efd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    free(s);
                }
            }
        }
    }

添加新客户端链接

复制代码
这里就是按部就班,我们只需要进行客户端连接的操作就行
接受链接
设置边缘触发
创建会话对象
添加到监听红黑树上

这里是代码实现,这部分内容相对来说比较简单

c 复制代码
void accept_new_connection(int listen_fd) {
    struct sockaddr_in clie_addr;
    socklen_t len = sizeof(clie_addr);
    
    int ctrl_fd = accept(listen_fd, (struct sockaddr*)&clie_addr, &len);
    if(ctrl_fd < 0) {
        return;
    }
    
    int flags = fcntl(ctrl_fd, F_GETFL, 0);
    if(flags != -1) { 
        fcntl(ctrl_fd, F_SETFL, flags | O_NONBLOCK);
    }
    ftp_session_t *s = calloc(1, sizeof(ftp_session_t));
    s->ctrl_fd = ctrl_fd;
    s->data_listen_fd = -1;
    
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.ptr = s;
    epoll_ctl(efd, EPOLL_CTL_ADD, ctrl_fd, &ev);
    
    send_response(ctrl_fd, "220", "FTP Server ready");
    printf("New connection from %s:%d\n", inet_ntoa(clie_addr.sin_addr), ntohs(clie_addr.sin_port));
}

我们对于就绪的事件也需要分发处理

就绪事件处理函数

复制代码
首先接受来自客户端发送的数据
	异常处理,跳出循环,删除事件
收到数据后进行数据解析,为了获取完整的数据,数据暂存,拼接后面收到的数据
按照FTP协议的结束符来处理数据
	获取指令,添加到任务队列中去
	移动指针,查找下一条指令
c 复制代码
void handle_client_read(ftp_session_t *s) {
    int fd = s->ctrl_fd;
    char buf[BUFFER_SIZE];
    while (1) {
        int n = recv(fd, buf, sizeof(buf), 0);
        if(n <= 0) {
            if(n < 0 && errno == EAGAIN) {
                break;
            }
            printf("Client disconnected\n");

            epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
            close(fd);
            
            if(s->data_listen_fd != -1)
                close(s->data_listen_fd);
            free(s);
            return;
        }
        
        // 处理数据
        if(s->recv_len + n < BUFFER_SIZE) {
            memcpy(s->recv_buf + s->recv_len, buf, n);
            s->recv_len += n;
        }
        else {
            s->recv_len = 0;
            continue;
        }

        //解析命令
        char *line = s->recv_buf;
        char *crlf;
        while((crlf = strstr(line, "\r\n")) != NULL) {
            *crlf = '\0';
            add_task(s, line);
            line = crlf + 2;
            s->recv_len -= (int)(line - s->recv_buf);
            memmove(s->recv_buf, line, s->recv_len);
            line = s->recv_buf;
        }
    }
}
添加任务
复制代码
创建任务节点
会话对象赋值
加琐保护
添加任务节点
发送信号唤醒工作线程
解锁

这部分的任务添加函数相对简单一些

c 复制代码
void add_task(ftp_session_t *s, const char *cmd) {
    task_t *t = malloc(sizeof(task_t));
    t->session = s;
    strncpy(t->cmd_line, cmd, sizeof(t->cmd_line)-1);
    t->cmd_line[sizeof(t->cmd_line)-1] = '\0';
    t->next = NULL;
    pthread_mutex_lock(&g_task_mutex);
    
    if(g_task_tail) {
        g_task_tail->next = t;
        g_task_tail = t;
    }
    else {
        g_task_head = g_task_tail = t;
    }
    
    pthread_cond_signal(&g_task_cond);
    pthread_mutex_unlock(&g_task_mutex);
}

服务器关闭

c 复制代码
g_shutdown = 1;
pthread_cond_broadcast(&g_task_cond);
for(int i = 0; i < THREAD_POOL_SIZE; i++) {
	pthread_join(workers[i], NULL);
}
close(efd);
close(listen_fd);
return 0;

FTP服务器总结

这就是FTP服务器的实现思路和代码的实现,相对来说是比较有难度的,实现这个服务器可让我们对于网络中的数据的传输有一个简单的了解。

FTP客户端的实现

我接下来将根据前面的FTP服务器然后完成对应的客户端实现,讲解的方式和上面一样都是结合代码和实现逻辑来看。

链接FTP服务器

首先我们在启动客户端的时候如果用户没有输入IP,那么我们就提醒用户输入IP

c 复制代码
char ip[64];
if(argc > 1) {
	strcpy(ip, argv[1]);
}
else {
	printf("Enter server IP: ");
	fgets(ip, sizeof(ip), stdin);
	ip[strcspn(ip, "\n")] = '\0';
}
ctrl_fd = connect_control(ip);
if(ctrl_fd < 0) {
	perror("connect");
	return 1;
}

得到服务器的IP之后我们就可以和服务器进行连接了

复制代码
创建客户端的套接字,初始化地址结构体
将本地IP转换为网络IP
链接connect
返回文件描述符
c 复制代码
int connect_control(const char *ip) {
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd < 0) {
        return -1;
    }

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(CTRL_PORT);
    if(inet_pton(AF_INET, ip, &addr.sin_addr) <= 0) {
        close(fd);
        return -1;
    }

    if(connect(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        close(fd);
        return -1;
    }
    return fd;
}

读取服务器的数据响应

我们既然要实现服务器和客户端的交互,那么我们客户端就需要读取服务器发送来的数据

c 复制代码
int read_response(char *resp, size_t size) {
    char buf[BUFFER_SIZE];
    int n = recv(ctrl_fd, buf, sizeof(buf) - 1, 0);
    if(n <= 0) {
        if(n == 0){
            fprintf(stderr, "Server closed control connection\n");
        }
        else{
            perror("recv");
        }
        return -1;
    }
    buf[n] = '\0';
    if(resp) {
        strncpy(resp, buf, size - 1);
        resp[size - 1] = '\0';
    }
    printf("S: %s", buf);
    return 0;
}

登录处理

我们可以添加一些欢迎消息或是登录验证,当然这些都不是必须的

我们只是为了完善,所以我添加了这些

c 复制代码
// 读取欢迎消息
    if(read_response(NULL, 0) != 0) {
        close(ctrl_fd);
        return 1;
    }

    // 登录
    if(send_command("USER test", NULL, 0) != 0 ||
        send_command("PASS test", NULL, 0) != 0) {
        close(ctrl_fd);
        return 1;
    }
    printf("Connected to %s:%d\n", ip, CTRL_PORT);
    printf("Commands: ls, get <file>, put <file>, quit\n");

交互界面

这里就到了交互界面了,我们需要实现服务器中的所有的指令操作

这里是客户端最主要的部分

c 复制代码
char input[256];
    while (1) {
        printf("ftp> ");
        fflush(stdout);
        
        if(!fgets(input, sizeof(input), stdin)) {
            break;
        }
        
        input[strcspn(input, "\n")] = '\0';
        if(strlen(input) == 0) {
            continue;
        }
        
        if(strcmp(input, "quit") == 0 || strcmp(input, "exit") == 0) {
            send_command("QUIT", NULL, 0);
            break;
        }
        else if (strcmp(input, "ls") == 0 || strcmp(input, "dir") == 0) {
            list_directory();
        }
        else if (strncmp(input, "get ", 4) == 0) {
            download_file(input + 4);
        }
        else if (strncmp(input, "put ", 4) == 0) {
            upload_file(input + 4);
        }
        else {
            printf("Unknown command. Try: ls, get <file>, put <file>, quit\n");
        }
    }

那么我们详细介绍在客户端这些指令的实现

发送指令

拼接客户端信息然后发送信息给服务器

c 复制代码
int send_command(const char *cmd, char *resp, size_t resp_size) {
    char buffer[BUFFER_SIZE];
    snprintf(buffer, sizeof(buffer), "%s\r\n", cmd);
    if(send(ctrl_fd, buffer, strlen(buffer), 0) <= 0) {
        perror("send");
        return -1;
    }
    return read_response(resp, resp_size);
}

解析PASV响应,提取IP和端口号

符合FTP的格式要求

c 复制代码
int parse_pasv(const char *resp, char *ip, int *port) {
    char *start = strchr(resp, '(');
    char *end = strchr(resp, ')');
    if(!start || !end) {
        return -1;
    }
    int h1, h2, h3, h4, p1, p2;
    if(sscanf(start, "(%d,%d,%d,%d,%d,%d)", &h1, &h2, &h3, &h4, &p1, &p2) != 6) {
        return -1;
    }
    sprintf(ip, "%d.%d.%d.%d", h1, h2, h3, h4);
    *port = p1 * 256 + p2;
    return 0;
}

列出目录

复制代码
首先现发送被动链接的指令
从服务器的响应中提取IP和端口号
建立数据连接
发送列出目录的指令
检查服务器是否可以准备进行数据链接
通过建立的通道读取数据流并打印
数据传输完毕之后关闭链接
确认完成等待服务器响应关闭通道

代码实现如下

c 复制代码
void list_directory(void) {
    char resp[BUFFER_SIZE];
    if(send_command("PASV", resp, sizeof(resp)) != 0) {
        printf("Failed to enter passive mode\n");
        return;
    }
    char data_ip[16];
    int data_port;
    if(parse_pasv(resp, data_ip, &data_port) != 0) {
        printf("Failed to parse PASV response\n");
        return;
    }
    int data_fd = connect_data(data_ip, data_port);
    if(data_fd < 0) {
        printf("Data connection failed\n");
        return;
    }
    if(send_command("LIST", resp, sizeof(resp)) != 0) {
        close(data_fd);
        return;
    }
    if(strncmp(resp, "150", 3) != 0) {
        printf("LIST not accepted by server\n");
        close(data_fd);
        return;
    }
    char buf[BUFFER_SIZE];
    int n;
    printf("--- Listing ---\n");
    while ((n = recv(data_fd, buf, sizeof(buf) - 1, 0)) > 0) {
        buf[n] = '\0';
        printf("%s", buf);
    }
    close(data_fd);
    // 接收 226 完成响应
    if(read_response(resp, sizeof(resp)) != 0 || strncmp(resp, "226", 3) != 0) {
        printf("Failed to receive completion response\n");
    }
    printf("--- End ---\n");
}

文件的下载

复制代码
发送被动模式的链接
从服务器的响应中提取IP和端口号
发送文件的下载指令
检查客户端发送的信息,是否可以进行通道的链接
通道链接没有问题,以二进制写的形式打开本地文件
循环从网络之哦你个读取数据写如本地文件
写完之后关闭文件,关闭数据链接,清理资源
确认完成等待服务器响应关闭通道

代码的实现

c 复制代码
void download_file(const char *filename) {
    char resp[BUFFER_SIZE];
    if(send_command("PASV", resp, sizeof(resp)) != 0) {
        return;
    }
    char data_ip[16];
    int data_port;
    if(parse_pasv(resp, data_ip, &data_port) != 0) {
        printf("Failed to parse PASV response\n");
        return;
    }
    int data_fd = connect_data(data_ip, data_port);
    if(data_fd < 0) {
        printf("Data connection failed\n");
        return;
    }
    char cmd[256];
    snprintf(cmd, sizeof(cmd), "RETR %s", filename);
    if(send_command(cmd, resp, sizeof(resp)) != 0) {
        close(data_fd);
        return;
    }
    if(strncmp(resp, "150", 3) != 0) {
        printf("Server refused RETR\n");
        close(data_fd);
        return;
    }
    FILE *fp = fopen(filename, "wb");
    if(!fp) {
        perror("fopen");
        close(data_fd);
        return;
    }
    char buf[BUFFER_SIZE];
    int n;
    while((n = recv(data_fd, buf, sizeof(buf), 0)) > 0) {
        fwrite(buf, 1, n, fp);
    }
    fclose(fp);
    close(data_fd);
    // 接收 226 完成响应
    if(read_response(resp, sizeof(resp)) == 0 && strncmp(resp, "226", 3) == 0) {
        printf("Downloaded %s\n", filename);
    }
    else {
        printf("Download failed: unexpected response\n");
    }
}

文件的上传

复制代码
以二进制的读模式打开文件
发送被动链接,解析服务器发送的IP和端口号
建立数据链接的通道
发送上传文件的指令
检查服务器响应是否建立好数据传输的通道
循环从本地文件读取数据上传发送给服务器
清理资源,关闭文件,关闭链接
确认数据传输完成,等服务器的响应数据传输通道关闭

代码实现如下

c 复制代码
void upload_file(const char *filename) {
    FILE *fp = fopen(filename, "rb");
    if(!fp) {
        perror("fopen");
        return;
    }
    char resp[BUFFER_SIZE];
    if(send_command("PASV", resp, sizeof(resp)) != 0) {
        fclose(fp);
        return;
    }
    char data_ip[16];
    int data_port;
    if(parse_pasv(resp, data_ip, &data_port) != 0) {
        printf("Failed to parse PASV response\n");
        fclose(fp);
        return;
    }
    int data_fd = connect_data(data_ip, data_port);
    if(data_fd < 0) {
        printf("Data connection failed\n");
        fclose(fp);
        return;
    }
    char cmd[256];
    snprintf(cmd, sizeof(cmd), "STOR %s", filename);
    if(send_command(cmd, resp, sizeof(resp)) != 0) {
        close(data_fd);
        fclose(fp);
        return;
    }
    if(strncmp(resp, "150", 3) != 0) {
        printf("Server refused STOR\n");
        close(data_fd);
        fclose(fp);
        return;
    }
    char buf[BUFFER_SIZE];
    size_t n;
    while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
        if(send(data_fd, buf, n, 0) != (ssize_t)n) {
            perror("send");
            break;
        }
    }
    fclose(fp);
    close(data_fd);
    // 接收 226 完成响应
    if(read_response(resp, sizeof(resp)) == 0 && strncmp(resp, "226", 3) == 0) {
        printf("Uploaded %s\n", filename);
    }
    else {
        printf("Upload failed: unexpected response\n");
    }
}

客户端的退出

我们在客户端的指令行上输出quit就可以直接退出客户端了

c 复制代码
if(strcmp(input, "quit") == 0 || strcmp(input, "exit") == 0) {
	send_command("QUIT", NULL, 0);
	break;
}

FTP客户端总结

我实现的客户端相对来说简单一些,只实现了最主要的几个指令和接受服务器发送的信息等最基础的功能。客户端的实现和服务器之间的交互于数据传输让我对网络编程有了更深的了解。

相关推荐
Chris-zz1 小时前
Linux:线程概念与控制
linux·运维
剑神一笑1 小时前
Linux chown 命令详解:从 inode 到实战
linux·运维·服务器
MIXLLRED1 小时前
随笔——在 Ubuntu 22.04 中查看 Markdown (.md) 文件
linux·运维·ubuntu·markdown
STDD2 小时前
Linux cgroup v2 资源控制实战:限制进程 CPU/内存/IO,systemd slice 管理
linux·运维·服务器
Latticy2 小时前
内网渗透-横向移动-密码喷洒攻击和域内用(kerbrute使用)
运维·服务器·网络·内网渗透·内网
kukubuzai3 小时前
Docker Note
linux·运维·docker
网络研究院3 小时前
Proton Drive采用OpenPGP加密,上传速度提升300%
服务器·网络·安全·proton drive·openpgp
Ltd Pikashu3 小时前
insmod 加载内核模块 —— sys_init_module 源码剖析
linux·kernel·insmod