FTP客户端实现(文件传输)
文章目录
- 🧱 一、FTP 基础架构回顾
- 🚀 二、FTP 客户端的核心结构
- 🔗 三、连接与登录过程
- 📌 1. `ftp_create()`
- 📌 2. `ftp_connect()`
- 📌 3. `ftp_login()`
- 📁 四、上传文件实现(`ftp_upload_file()`)
- 🚩 流程总览:
- ✨ 细节讲解
- ✅ 1. 判断 remote_path 是目录 or 文件路径
- ✅ 2. 递归创建远程目录
- ✅ 3. 进入 PASV 模式(被动模式)
- ✅ 4. 发起数据连接(客户端主动连接)
- ✅ 5. 发送 STOR 命令
- ✅ 6. 发送文件内容(TCP)
- 🔚 五、退出与清理
- 📢 六、辅助函数详解
- ✅ `send_command()`
- ✅ `read_reply()`
- 📌 七、完整上传流程演示(例子)
- ✅ 总结:功能完整的 FTP 上传模块
这段代码实现了一个 FTP 客户端核心功能模块,可完成如下关键任务:
✅ 建立连接
✅ 登录认证
✅ 递归创建远程目录
✅ 上传文件(支持上传到目录)
✅ 退出登录
下面我将按照功能模块,详细讲解整个 FTP 上传流程的实现原理和关键函数。
🧱 一、FTP 基础架构回顾
FTP 通信包括两个通道:
- 控制通道(control channel):用于发送 FTP 命令和接收响应(TCP 21)
- 数据通道(data channel):用于传输文件、目录等(通过
PASV
或PORT
创建)
本实现采用 PASV(被动模式) 建立数据通道,即客户端主动连接服务端开放的端口。
🚀 二、FTP 客户端的核心结构
typedef struct {int control_sock; // 控制通道 socketstruct sockaddr_in server_addr;
} FTPClient;
🔗 三、连接与登录过程
📌 1. ftp_create()
- 创建控制通道 socket:
socket(AF_INET, SOCK_STREAM, 0)
- 配置远程 IP 和端口(默认 21)
- 返回已初始化的 FTPClient 指针
📌 2. ftp_connect()
- 使用
connect()
发起 TCP 控制连接 - 成功后读取服务端欢迎信息(FTP 状态码应该是
220
)
read_reply(...) == 220 → 成功
📌 3. ftp_login()
- 先发送用户名命令:
USER xxx\r\n
- 响应应为
331
,表示继续输入密码
- 响应应为
- 再发送密码命令:
PASS xxx\r\n
- 响应应为
230
,表示登录成功
- 响应应为
📁 四、上传文件实现(ftp_upload_file()
)
🚩 流程总览:
→ 判断 remote_path 是目录还是完整路径
→ 如果是目录:拼接本地文件名
→ 递归创建远程目录(MKD)
→ 进入 PASV 模式,获取数据连接地址和端口
→ 发起数据连接(客户端连接服务端返回的地址/端口)
→ 发送 STOR 上传命令
→ 传输文件内容
→ 关闭数据连接并读取完成响应(226)
✨ 细节讲解
✅ 1. 判断 remote_path 是目录 or 文件路径
if (remote_path[strlen(remote_path) - 1] == '/') {// 是目录 → 拼接 local_file 的文件名
}
例:
local_file = "/home/user/a.txt"
remote_path = "upload/abc/"
→ final_remote_file = "upload/abc/a.txt"
✅ 2. 递归创建远程目录
ftp_create_remote_path(client, remote_dir);
内部调用:
MKD /upload
MKD /upload/abc
MKD /upload/abc/xyz
错误 550 表示已存在,也视作成功。
✅ 3. 进入 PASV 模式(被动模式)
send_command(..., "PASV\r\n");
read_reply(...); // 解析响应 227→ 解析返回的 IP 和端口:
"227 Entering Passive Mode (192,168,1,10,14,78)"
→ port = 14*256 + 78 = 3678
服务端会在该端口等待数据连接。
✅ 4. 发起数据连接(客户端主动连接)
data_sock = socket(...)
connect(data_sock, ...)
目标 IP = 控制连接 IP,端口 = PASV 返回的端口。
✅ 5. 发送 STOR 命令
STOR upload/abc/a.txt\r\n
→ 服务端响应 150:准备传输数据
✅ 6. 发送文件内容(TCP)
while (fread(...) > 0) {send(data_sock, ...);
}
成功发送完后:
- 关闭
data_sock
- 读取控制通道响应是否为
226
→ 传输完成
🔚 五、退出与清理
ftp_quit(client);
→ 发送 QUIT\r\n,服务端应返回 221ftp_destroy(client);
→ 关闭 socket,释放内存
📢 六、辅助函数详解
✅ send_command()
send(sock, command, strlen(command), 0);
发送命令字符串,例如:
"USER test\r\n"
"STOR /path/file.txt\r\n"
✅ read_reply()
recv(sock, buffer, ...);
→ 返回第一段数字作为 FTP 响应码(例如 220、331、226)
📌 七、完整上传流程演示(例子)
local_file = "/home/user/a.txt"
remote_path = "upload/test/" // 目录
程序实际执行逻辑:
- 拼接路径 → upload/test/a.txt
- 创建路径:MKD /upload、/upload/test
- PASV 模式,服务端返回数据端口
- 建立数据连接
- STOR upload/test/a.txt
- 发送文件数据
- 等待 226 响应
- QUIT → 221
✅ 总结:功能完整的 FTP 上传模块
功能 | 是否支持 |
---|---|
控制连接建立 | ✅ |
用户登录认证 | ✅ |
被动模式数据传输 | ✅ |
自动创建多级目录 | ✅ |
上传任意路径文件 | ✅ |
退出与资源释放 | ✅ |
核心代码:
#include "ftpclient.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUF_SIZE 1024// 内部函数声明
static int send_command(int sock, const char* command);
static int read_reply(int sock, char* buffer, size_t size);
static int ftp_create_dir(FTPClient* client, const char* remote_dir);// 创建客户端
FTPClient* ftp_create(const char* server_ip, int server_port) {FTPClient* client = (FTPClient*)malloc(sizeof(FTPClient));if (!client) return NULL;client->control_sock = socket(AF_INET, SOCK_STREAM, 0);if (client->control_sock < 0) {free(client);return NULL;}memset(&client->server_addr, 0, sizeof(client->server_addr));client->server_addr.sin_family = AF_INET;client->server_addr.sin_port = htons(server_port);inet_pton(AF_INET, server_ip, &client->server_addr.sin_addr);return client;
}void ftp_destroy(FTPClient* client) {if (client) {if (client->control_sock >= 0)close(client->control_sock);free(client);}
}int ftp_connect(FTPClient* client) {if (connect(client->control_sock, (struct sockaddr*)&client->server_addr, sizeof(client->server_addr)) < 0)return -1;char buffer[BUF_SIZE];return read_reply(client->control_sock, buffer, sizeof(buffer)) == 220 ? 0 : -1;
}int ftp_login(FTPClient* client, const char* username, const char* password) {char command[BUF_SIZE];char buffer[BUF_SIZE];snprintf(command, sizeof(command), "USER %s\r\n", username);if (send_command(client->control_sock, command) < 0) return -1;if (read_reply(client->control_sock, buffer, sizeof(buffer)) != 331) return -1;snprintf(command, sizeof(command), "PASS %s\r\n", password);if (send_command(client->control_sock, command) < 0) return -1;if (read_reply(client->control_sock, buffer, sizeof(buffer)) != 230) return -1;return 0;
}int ftp_upload_file(FTPClient* client, const char* local_file, const char* remote_path) {FILE* file = fopen(local_file, "rb");if (!file) return -1;char buffer[BUF_SIZE];// 处理 remote_file:判断是否是目录char final_remote_file[BUF_SIZE];if (remote_path[strlen(remote_path) - 1] == '/') {// 提取文件名const char* file_name = strrchr(local_file, '/');file_name = file_name ? file_name + 1 : local_file;snprintf(final_remote_file, sizeof(final_remote_file), "%s%s", remote_path, file_name);} else {strncpy(final_remote_file, remote_path, sizeof(final_remote_file));final_remote_file[sizeof(final_remote_file) - 1] = '\0';}// 提取目录路径并创建char remote_dir[BUF_SIZE];strncpy(remote_dir, final_remote_file, sizeof(remote_dir));remote_dir[sizeof(remote_dir) - 1] = '\0';char* last_slash = strrchr(remote_dir, '/');if (last_slash) {*last_slash = '\0';ftp_create_remote_path(client, remote_dir); // 忽略失败}// PASV 模式if (send_command(client->control_sock, "PASV\r\n") < 0) {fclose(file);return -1;}if (read_reply(client->control_sock, buffer, sizeof(buffer)) != 227) {fclose(file);return -1;}int ip1, ip2, ip3, ip4, p1, p2;sscanf(buffer, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)",&ip1, &ip2, &ip3, &ip4, &p1, &p2);int data_port = p1 * 256 + p2;int data_sock = socket(AF_INET, SOCK_STREAM, 0);if (data_sock < 0) {fclose(file);return -1;}struct sockaddr_in data_addr;memset(&data_addr, 0, sizeof(data_addr));data_addr.sin_family = AF_INET;data_addr.sin_port = htons(data_port);data_addr.sin_addr = client->server_addr.sin_addr;if (connect(data_sock, (struct sockaddr*)&data_addr, sizeof(data_addr)) < 0) {close(data_sock);fclose(file);return -1;}snprintf(buffer, sizeof(buffer), "STOR %s\r\n", final_remote_file);if (send_command(client->control_sock, buffer) < 0) {close(data_sock);fclose(file);return -1;}if (read_reply(client->control_sock, buffer, sizeof(buffer)) != 150) {close(data_sock);fclose(file);return -1;}size_t bytes_read;while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {if (send(data_sock, buffer, bytes_read, 0) < 0) {close(data_sock);fclose(file);return -1;}}close(data_sock);fclose(file);return (read_reply(client->control_sock, buffer, sizeof(buffer)) == 226) ? 0 : -1;
}void ftp_quit(FTPClient* client) {send_command(client->control_sock, "QUIT\r\n");char buffer[BUF_SIZE];read_reply(client->control_sock, buffer, sizeof(buffer));
}// 创建路径:递归 MKD
int ftp_create_remote_path(FTPClient* client, const char* remote_path) {char path[BUF_SIZE];strncpy(path, remote_path, sizeof(path));path[sizeof(path) - 1] = '\0';char temp[BUF_SIZE] = {0};char* token = strtok(path, "/");while (token) {strcat(temp, "/");strcat(temp, token);ftp_create_dir(client, temp); // 忽略失败token = strtok(NULL, "/");}return 0;
}static int ftp_create_dir(FTPClient* client, const char* remote_dir) {char buffer[BUF_SIZE];snprintf(buffer, sizeof(buffer), "MKD %s\r\n", remote_dir);if (send_command(client->control_sock, buffer) < 0) return -1;int code = read_reply(client->control_sock, buffer, sizeof(buffer));return (code == 257 || code == 550) ? 0 : -1; // 550 表示目录已存在
}static int send_command(int sock, const char* command) {return (send(sock, command, strlen(command), 0) == strlen(command)) ? 0 : -1;
}static int read_reply(int sock, char* buffer, size_t size) {int bytes_read = recv(sock, buffer, size - 1, 0);if (bytes_read <= 0) return -1;buffer[bytes_read] = '\0';return atoi(buffer);
}